Skip to main content

nextest_runner/user_config/elements/
record.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Record-related user configuration.
5
6use crate::user_config::helpers::resolve_record_setting;
7use bytesize::ByteSize;
8use serde::Deserialize;
9use std::time::Duration;
10use target_spec::{Platform, TargetSpec};
11
12/// Minimum allowed value for `max_output_size`.
13///
14/// This ensures there's enough space for the truncation marker plus some
15/// actual content. The truncation marker is approximately 40 bytes, so 1000
16/// bytes provides reasonable headroom.
17pub const MIN_MAX_OUTPUT_SIZE: ByteSize = ByteSize::b(1000);
18
19/// Maximum allowed value for `max_output_size`.
20///
21/// This caps how much output can be stored per test, bounding memory usage
22/// when reading archives. Values above this are clamped with a warning.
23///
24/// This constant is also used as the maximum size for reading any file from
25/// a recorded archive, preventing malicious archives from causing OOM.
26pub const MAX_MAX_OUTPUT_SIZE: ByteSize = ByteSize::mib(256);
27
28/// Retention policy for recorded test runs.
29///
30/// Recording only happens when the `record` experimental feature is enabled
31/// _and_ `enabled` here is true. The least-recently-used runs are evicted to
32/// keep within `max-records`, `max-total-size`, and `max-age`.
33#[derive(Clone, Debug, Default, Deserialize)]
34#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
35#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
36#[serde(rename_all = "kebab-case")]
37pub struct DeserializedRecordConfig {
38    /// Whether to record test runs.
39    ///
40    /// Set to false to temporarily disable recording without removing the rest
41    /// of the configuration. Has no effect unless the `record` experimental
42    /// feature is also enabled.
43    #[serde(default)]
44    pub enabled: Option<bool>,
45
46    /// Maximum number of recorded runs to retain.
47    #[serde(default)]
48    pub max_records: Option<usize>,
49
50    /// Maximum combined size of all recorded runs (e.g. `"1GB"`).
51    #[serde(default)]
52    #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
53    pub max_total_size: Option<ByteSize>,
54
55    /// Maximum idle time before a recorded run is evicted (e.g. `"30d"`).
56    #[serde(default, with = "humantime_serde")]
57    #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
58    pub max_age: Option<Duration>,
59
60    /// Maximum size of a single captured stdout or stderr stream before it is
61    /// truncated (e.g. `"10MB"`).
62    #[serde(default)]
63    #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
64    pub max_output_size: Option<ByteSize>,
65}
66
67/// Default record configuration with all values required.
68///
69/// This is parsed from the embedded default user config TOML. All fields are
70/// required - if the TOML is missing any field, parsing fails.
71#[derive(Clone, Debug, Deserialize)]
72#[serde(rename_all = "kebab-case")]
73pub struct DefaultRecordConfig {
74    /// Whether recording is enabled by default.
75    pub enabled: bool,
76
77    /// Maximum number of records to keep.
78    pub max_records: usize,
79
80    /// Maximum total size of all records.
81    pub max_total_size: ByteSize,
82
83    /// Maximum age of records.
84    #[serde(with = "humantime_serde")]
85    pub max_age: Duration,
86
87    /// Maximum size of a single output (stdout/stderr) before truncation.
88    pub max_output_size: ByteSize,
89}
90
91/// Per-platform substitutions for `[record]` settings.
92///
93/// Each field has the same meaning as in the `[record]` table. Only the
94/// fields actually set here are substituted on matching platforms.
95#[derive(Clone, Debug, Default, Deserialize)]
96#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
97#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
98#[serde(rename_all = "kebab-case")]
99pub(in crate::user_config) struct DeserializedRecordOverrideData {
100    /// Whether to record test runs.
101    #[serde(default)]
102    pub(in crate::user_config) enabled: Option<bool>,
103
104    /// Maximum number of recorded runs to retain.
105    #[serde(default)]
106    pub(in crate::user_config) max_records: Option<usize>,
107
108    /// Maximum combined size of all recorded runs (e.g. `"1GB"`).
109    #[serde(default)]
110    #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
111    pub(in crate::user_config) max_total_size: Option<ByteSize>,
112
113    /// Maximum idle time before a recorded run is evicted (e.g. `"30d"`).
114    #[serde(default, with = "humantime_serde")]
115    #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
116    pub(in crate::user_config) max_age: Option<Duration>,
117
118    /// Maximum size of a single captured stdout or stderr stream before it is
119    /// truncated (e.g. `"10MB"`).
120    #[serde(default)]
121    #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
122    pub(in crate::user_config) max_output_size: Option<ByteSize>,
123}
124
125/// A compiled record override with parsed platform spec.
126///
127/// This is created after parsing the platform expression from a
128/// `[[overrides]]` entry.
129#[derive(Clone, Debug)]
130pub(in crate::user_config) struct CompiledRecordOverride {
131    platform_spec: TargetSpec,
132    data: RecordOverrideData,
133}
134
135impl CompiledRecordOverride {
136    /// Creates a new compiled override from a platform spec and record data.
137    pub(in crate::user_config) fn new(
138        platform_spec: TargetSpec,
139        data: DeserializedRecordOverrideData,
140    ) -> Self {
141        Self {
142            platform_spec,
143            data: RecordOverrideData {
144                enabled: data.enabled,
145                max_records: data.max_records,
146                max_total_size: data.max_total_size,
147                max_age: data.max_age,
148                max_output_size: data.max_output_size,
149            },
150        }
151    }
152
153    /// Checks if this override matches the given platform.
154    ///
155    /// Unknown results (e.g., unrecognized target features) are treated as
156    /// non-matching to be conservative.
157    pub(in crate::user_config) fn matches(&self, build_target: &Platform) -> bool {
158        self.platform_spec
159            .eval(build_target)
160            .unwrap_or(/* unknown results are mapped to false */ false)
161    }
162
163    /// Returns a reference to the override data.
164    pub(in crate::user_config) fn data(&self) -> &RecordOverrideData {
165        &self.data
166    }
167}
168
169/// Override data for record settings.
170#[derive(Clone, Debug, Default)]
171pub(in crate::user_config) struct RecordOverrideData {
172    enabled: Option<bool>,
173    max_records: Option<usize>,
174    max_total_size: Option<ByteSize>,
175    max_age: Option<Duration>,
176    max_output_size: Option<ByteSize>,
177}
178
179impl RecordOverrideData {
180    /// Returns the enabled setting, if specified.
181    pub(in crate::user_config) fn enabled(&self) -> Option<&bool> {
182        self.enabled.as_ref()
183    }
184
185    /// Returns the max_records setting, if specified.
186    pub(in crate::user_config) fn max_records(&self) -> Option<&usize> {
187        self.max_records.as_ref()
188    }
189
190    /// Returns the max_total_size setting, if specified.
191    pub(in crate::user_config) fn max_total_size(&self) -> Option<&ByteSize> {
192        self.max_total_size.as_ref()
193    }
194
195    /// Returns the max_age setting, if specified.
196    pub(in crate::user_config) fn max_age(&self) -> Option<&Duration> {
197        self.max_age.as_ref()
198    }
199
200    /// Returns the max_output_size setting, if specified.
201    pub(in crate::user_config) fn max_output_size(&self) -> Option<&ByteSize> {
202        self.max_output_size.as_ref()
203    }
204}
205
206/// Resolved record configuration after applying defaults.
207#[derive(Clone, Debug)]
208pub struct RecordConfig {
209    /// Whether recording is enabled.
210    ///
211    /// Recording only occurs when both this is true and the `record`
212    /// experimental feature is enabled.
213    pub enabled: bool,
214
215    /// Maximum number of records to keep.
216    pub max_records: usize,
217
218    /// Maximum total size of all records.
219    pub max_total_size: ByteSize,
220
221    /// Maximum age of records.
222    pub max_age: Duration,
223
224    /// Maximum size of a single output (stdout/stderr) before truncation.
225    pub max_output_size: ByteSize,
226}
227
228impl RecordConfig {
229    /// Resolves record configuration from user configs, defaults, and the
230    /// build target of the nextest binary.
231    ///
232    /// Resolution order (highest to lowest priority):
233    ///
234    /// 1. User overrides (first matching override for each setting)
235    /// 2. Default overrides (first matching override for each setting)
236    /// 3. User base config
237    /// 4. Default base config
238    ///
239    /// This matches the resolution order used by repo config.
240    ///
241    /// If `max_output_size` is below [`MIN_MAX_OUTPUT_SIZE`], it is clamped
242    /// to the minimum and a warning is logged.
243    pub(in crate::user_config) fn resolve(
244        default_config: &DefaultRecordConfig,
245        default_overrides: &[CompiledRecordOverride],
246        user_config: Option<&DeserializedRecordConfig>,
247        user_overrides: &[CompiledRecordOverride],
248        build_target: &Platform,
249    ) -> Self {
250        let mut max_output_size = resolve_record_setting(
251            &default_config.max_output_size,
252            default_overrides,
253            user_config.and_then(|c| c.max_output_size.as_ref()),
254            user_overrides,
255            build_target,
256            |data| data.max_output_size(),
257        );
258
259        // Enforce minimum to ensure truncation marker fits.
260        if max_output_size < MIN_MAX_OUTPUT_SIZE {
261            tracing::warn!(
262                "max-output-size ({}) is below minimum ({}), using minimum",
263                max_output_size,
264                MIN_MAX_OUTPUT_SIZE,
265            );
266            max_output_size = MIN_MAX_OUTPUT_SIZE;
267        } else if max_output_size > MAX_MAX_OUTPUT_SIZE {
268            tracing::warn!(
269                "max-output-size ({}) exceeds maximum ({}), using maximum",
270                max_output_size,
271                MAX_MAX_OUTPUT_SIZE,
272            );
273            max_output_size = MAX_MAX_OUTPUT_SIZE;
274        }
275
276        Self {
277            enabled: resolve_record_setting(
278                &default_config.enabled,
279                default_overrides,
280                user_config.and_then(|c| c.enabled.as_ref()),
281                user_overrides,
282                build_target,
283                |data| data.enabled(),
284            ),
285            max_records: resolve_record_setting(
286                &default_config.max_records,
287                default_overrides,
288                user_config.and_then(|c| c.max_records.as_ref()),
289                user_overrides,
290                build_target,
291                |data| data.max_records(),
292            ),
293            max_total_size: resolve_record_setting(
294                &default_config.max_total_size,
295                default_overrides,
296                user_config.and_then(|c| c.max_total_size.as_ref()),
297                user_overrides,
298                build_target,
299                |data| data.max_total_size(),
300            ),
301            max_age: resolve_record_setting(
302                &default_config.max_age,
303                default_overrides,
304                user_config.and_then(|c| c.max_age.as_ref()),
305                user_overrides,
306                build_target,
307                |data| data.max_age(),
308            ),
309            max_output_size,
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_deserialized_record_config_parsing() {
320        // Test full config.
321        let config: DeserializedRecordConfig = toml::from_str(
322            r#"
323            enabled = true
324            max-records = 50
325            max-total-size = "2GB"
326            max-age = "7d"
327            max-output-size = "5MB"
328            "#,
329        )
330        .unwrap();
331
332        assert_eq!(config.enabled, Some(true));
333        assert_eq!(config.max_records, Some(50));
334        assert_eq!(config.max_total_size, Some(ByteSize::gb(2)));
335        assert_eq!(config.max_age, Some(Duration::from_secs(7 * 24 * 60 * 60)));
336        assert_eq!(config.max_output_size, Some(ByteSize::mb(5)));
337
338        // Test partial config.
339        let config: DeserializedRecordConfig = toml::from_str(
340            r#"
341            max-records = 100
342            "#,
343        )
344        .unwrap();
345
346        assert!(config.enabled.is_none());
347        assert_eq!(config.max_records, Some(100));
348        assert!(config.max_total_size.is_none());
349        assert!(config.max_age.is_none());
350        assert!(config.max_output_size.is_none());
351
352        // Test empty config.
353        let config: DeserializedRecordConfig = toml::from_str("").unwrap();
354        assert!(config.enabled.is_none());
355        assert!(config.max_records.is_none());
356        assert!(config.max_total_size.is_none());
357        assert!(config.max_age.is_none());
358        assert!(config.max_output_size.is_none());
359    }
360
361    #[test]
362    fn test_default_record_config_parsing() {
363        let config: DefaultRecordConfig = toml::from_str(
364            r#"
365            enabled = true
366            max-records = 100
367            max-total-size = "1GB"
368            max-age = "30d"
369            max-output-size = "10MB"
370            "#,
371        )
372        .unwrap();
373
374        assert!(config.enabled);
375        assert_eq!(config.max_records, 100);
376        assert_eq!(config.max_total_size, ByteSize::gb(1));
377        assert_eq!(config.max_age, Duration::from_secs(30 * 24 * 60 * 60));
378        assert_eq!(config.max_output_size, ByteSize::mb(10));
379    }
380
381    #[test]
382    fn test_resolve_uses_defaults() {
383        let defaults = DefaultRecordConfig {
384            enabled: false,
385            max_records: 100,
386            max_total_size: ByteSize::gb(1),
387            max_age: Duration::from_secs(30 * 24 * 60 * 60),
388            max_output_size: ByteSize::mb(10),
389        };
390
391        let build_target =
392            Platform::build_target().expect("nextest is built for a supported platform");
393        let resolved = RecordConfig::resolve(&defaults, &[], None, &[], &build_target);
394
395        assert!(!resolved.enabled);
396        assert_eq!(resolved.max_records, 100);
397        assert_eq!(resolved.max_total_size, ByteSize::gb(1));
398        assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60));
399        assert_eq!(resolved.max_output_size, ByteSize::mb(10));
400    }
401
402    #[test]
403    fn test_resolve_user_overrides_defaults() {
404        let defaults = DefaultRecordConfig {
405            enabled: false,
406            max_records: 100,
407            max_total_size: ByteSize::gb(1),
408            max_age: Duration::from_secs(30 * 24 * 60 * 60),
409            max_output_size: ByteSize::mb(10),
410        };
411
412        let user_config = DeserializedRecordConfig {
413            enabled: Some(true),
414            max_records: Some(50),
415            max_total_size: None,
416            max_age: Some(Duration::from_secs(7 * 24 * 60 * 60)),
417            max_output_size: Some(ByteSize::mb(5)),
418        };
419
420        let build_target =
421            Platform::build_target().expect("nextest is built for a supported platform");
422        let resolved =
423            RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
424
425        assert!(resolved.enabled); // From user.
426        assert_eq!(resolved.max_records, 50); // From user.
427        assert_eq!(resolved.max_total_size, ByteSize::gb(1)); // From defaults.
428        assert_eq!(resolved.max_age, Duration::from_secs(7 * 24 * 60 * 60)); // From user.
429        assert_eq!(resolved.max_output_size, ByteSize::mb(5)); // From user.
430    }
431
432    #[test]
433    fn test_resolve_clamps_small_max_output_size() {
434        let defaults = DefaultRecordConfig {
435            enabled: false,
436            max_records: 100,
437            max_total_size: ByteSize::gb(1),
438            max_age: Duration::from_secs(30 * 24 * 60 * 60),
439            max_output_size: ByteSize::mb(10),
440        };
441
442        // User specifies a value below the minimum.
443        let user_config = DeserializedRecordConfig {
444            enabled: None,
445            max_records: None,
446            max_total_size: None,
447            max_age: None,
448            max_output_size: Some(ByteSize::b(100)), // Way below minimum.
449        };
450
451        let build_target =
452            Platform::build_target().expect("nextest is built for a supported platform");
453        let resolved =
454            RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
455
456        // Should be clamped to the minimum.
457        assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
458    }
459
460    #[test]
461    fn test_resolve_accepts_value_at_minimum() {
462        let defaults = DefaultRecordConfig {
463            enabled: false,
464            max_records: 100,
465            max_total_size: ByteSize::gb(1),
466            max_age: Duration::from_secs(30 * 24 * 60 * 60),
467            max_output_size: ByteSize::mb(10),
468        };
469
470        // User specifies exactly the minimum.
471        let user_config = DeserializedRecordConfig {
472            enabled: None,
473            max_records: None,
474            max_total_size: None,
475            max_age: None,
476            max_output_size: Some(MIN_MAX_OUTPUT_SIZE),
477        };
478
479        let build_target =
480            Platform::build_target().expect("nextest is built for a supported platform");
481        let resolved =
482            RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
483
484        // Should be accepted as-is.
485        assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
486    }
487
488    #[test]
489    fn test_resolve_clamps_large_max_output_size() {
490        let defaults = DefaultRecordConfig {
491            enabled: false,
492            max_records: 100,
493            max_total_size: ByteSize::gb(1),
494            max_age: Duration::from_secs(30 * 24 * 60 * 60),
495            max_output_size: ByteSize::mb(10),
496        };
497
498        // User specifies a value above the maximum.
499        let user_config = DeserializedRecordConfig {
500            enabled: None,
501            max_records: None,
502            max_total_size: None,
503            max_age: None,
504            max_output_size: Some(ByteSize::gib(1)), // Way above maximum.
505        };
506
507        let build_target =
508            Platform::build_target().expect("nextest is built for a supported platform");
509        let resolved =
510            RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
511
512        // Should be clamped to the maximum.
513        assert_eq!(resolved.max_output_size, MAX_MAX_OUTPUT_SIZE);
514    }
515
516    #[test]
517    fn test_resolve_accepts_value_at_maximum() {
518        let defaults = DefaultRecordConfig {
519            enabled: false,
520            max_records: 100,
521            max_total_size: ByteSize::gb(1),
522            max_age: Duration::from_secs(30 * 24 * 60 * 60),
523            max_output_size: ByteSize::mb(10),
524        };
525
526        // User specifies exactly the maximum.
527        let user_config = DeserializedRecordConfig {
528            enabled: None,
529            max_records: None,
530            max_total_size: None,
531            max_age: None,
532            max_output_size: Some(MAX_MAX_OUTPUT_SIZE),
533        };
534
535        let build_target =
536            Platform::build_target().expect("nextest is built for a supported platform");
537        let resolved =
538            RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
539
540        // Should be accepted as-is.
541        assert_eq!(resolved.max_output_size, MAX_MAX_OUTPUT_SIZE);
542    }
543
544    /// Helper to create a CompiledRecordOverride for tests.
545    fn make_override(
546        platform: &str,
547        data: DeserializedRecordOverrideData,
548    ) -> CompiledRecordOverride {
549        let platform_spec =
550            TargetSpec::new(platform.to_string()).expect("valid platform spec in test");
551        CompiledRecordOverride::new(platform_spec, data)
552    }
553
554    #[test]
555    fn test_resolve_user_override_applies() {
556        let defaults = DefaultRecordConfig {
557            enabled: false,
558            max_records: 100,
559            max_total_size: ByteSize::gb(1),
560            max_age: Duration::from_secs(30 * 24 * 60 * 60),
561            max_output_size: ByteSize::mb(10),
562        };
563
564        // Create a user override that matches any platform.
565        let override_ = make_override(
566            "cfg(all())",
567            DeserializedRecordOverrideData {
568                enabled: Some(true),
569                max_records: Some(50),
570                ..Default::default()
571            },
572        );
573
574        let build_target =
575            Platform::build_target().expect("nextest is built for a supported platform");
576        let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &build_target);
577
578        assert!(resolved.enabled);
579        assert_eq!(resolved.max_records, 50);
580        assert_eq!(resolved.max_total_size, ByteSize::gb(1)); // From defaults.
581        assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60)); // From defaults.
582        assert_eq!(resolved.max_output_size, ByteSize::mb(10)); // From defaults.
583    }
584
585    #[test]
586    fn test_resolve_default_override_applies() {
587        let defaults = DefaultRecordConfig {
588            enabled: false,
589            max_records: 100,
590            max_total_size: ByteSize::gb(1),
591            max_age: Duration::from_secs(30 * 24 * 60 * 60),
592            max_output_size: ByteSize::mb(10),
593        };
594
595        // Create a default override that matches any platform.
596        let override_ = make_override(
597            "cfg(all())",
598            DeserializedRecordOverrideData {
599                enabled: Some(true),
600                max_records: Some(50),
601                ..Default::default()
602            },
603        );
604
605        let build_target =
606            Platform::build_target().expect("nextest is built for a supported platform");
607        let resolved = RecordConfig::resolve(&defaults, &[override_], None, &[], &build_target);
608
609        assert!(resolved.enabled);
610        assert_eq!(resolved.max_records, 50);
611        assert_eq!(resolved.max_total_size, ByteSize::gb(1)); // From defaults.
612    }
613
614    #[test]
615    fn test_resolve_platform_override_no_match() {
616        let defaults = DefaultRecordConfig {
617            enabled: false,
618            max_records: 100,
619            max_total_size: ByteSize::gb(1),
620            max_age: Duration::from_secs(30 * 24 * 60 * 60),
621            max_output_size: ByteSize::mb(10),
622        };
623
624        // Create an override that never matches (cfg(any()) with no arguments
625        // is false).
626        let override_ = make_override(
627            "cfg(any())",
628            DeserializedRecordOverrideData {
629                enabled: Some(true),
630                max_records: Some(50),
631                max_total_size: Some(ByteSize::gb(2)),
632                max_age: Some(Duration::from_secs(7 * 24 * 60 * 60)),
633                max_output_size: Some(ByteSize::mb(5)),
634            },
635        );
636
637        let build_target =
638            Platform::build_target().expect("nextest is built for a supported platform");
639        let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &build_target);
640
641        // Nothing should be overridden - all values should match defaults.
642        assert!(!resolved.enabled);
643        assert_eq!(resolved.max_records, 100);
644        assert_eq!(resolved.max_total_size, ByteSize::gb(1));
645        assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60));
646        assert_eq!(resolved.max_output_size, ByteSize::mb(10));
647    }
648
649    #[test]
650    fn test_resolve_first_matching_user_override_wins() {
651        let defaults = DefaultRecordConfig {
652            enabled: false,
653            max_records: 100,
654            max_total_size: ByteSize::gb(1),
655            max_age: Duration::from_secs(30 * 24 * 60 * 60),
656            max_output_size: ByteSize::mb(10),
657        };
658
659        // Create two user overrides that both match (cfg(all()) is always true).
660        let override1 = make_override(
661            "cfg(all())",
662            DeserializedRecordOverrideData {
663                enabled: Some(true),
664                ..Default::default()
665            },
666        );
667
668        let override2 = make_override(
669            "cfg(all())",
670            DeserializedRecordOverrideData {
671                enabled: Some(false), // Should be ignored.
672                max_records: Some(50),
673                ..Default::default()
674            },
675        );
676
677        let build_target =
678            Platform::build_target().expect("nextest is built for a supported platform");
679        let resolved =
680            RecordConfig::resolve(&defaults, &[], None, &[override1, override2], &build_target);
681
682        // First override wins for enabled.
683        assert!(resolved.enabled);
684        // Second override's max_records applies (first didn't set it).
685        assert_eq!(resolved.max_records, 50);
686    }
687
688    #[test]
689    fn test_resolve_user_override_beats_default_override() {
690        let defaults = DefaultRecordConfig {
691            enabled: false,
692            max_records: 100,
693            max_total_size: ByteSize::gb(1),
694            max_age: Duration::from_secs(30 * 24 * 60 * 60),
695            max_output_size: ByteSize::mb(10),
696        };
697
698        // User override sets enabled.
699        let user_override = make_override(
700            "cfg(all())",
701            DeserializedRecordOverrideData {
702                enabled: Some(true),
703                ..Default::default()
704            },
705        );
706
707        // Default override sets enabled and max_records.
708        let default_override = make_override(
709            "cfg(all())",
710            DeserializedRecordOverrideData {
711                enabled: Some(false), // Should be ignored.
712                max_records: Some(50),
713                ..Default::default()
714            },
715        );
716
717        let build_target =
718            Platform::build_target().expect("nextest is built for a supported platform");
719        let resolved = RecordConfig::resolve(
720            &defaults,
721            &[default_override],
722            None,
723            &[user_override],
724            &build_target,
725        );
726
727        // User override wins for enabled.
728        assert!(resolved.enabled);
729        // Default override applies for max_records (user didn't set it).
730        assert_eq!(resolved.max_records, 50);
731    }
732
733    #[test]
734    fn test_resolve_override_beats_user_base() {
735        let defaults = DefaultRecordConfig {
736            enabled: false,
737            max_records: 100,
738            max_total_size: ByteSize::gb(1),
739            max_age: Duration::from_secs(30 * 24 * 60 * 60),
740            max_output_size: ByteSize::mb(10),
741        };
742
743        // User base config sets enabled.
744        let user_config = DeserializedRecordConfig {
745            enabled: Some(false),
746            max_records: Some(25),
747            ..Default::default()
748        };
749
750        // Default override sets enabled (should beat user base).
751        let default_override = make_override(
752            "cfg(all())",
753            DeserializedRecordOverrideData {
754                enabled: Some(true),
755                ..Default::default()
756            },
757        );
758
759        let build_target =
760            Platform::build_target().expect("nextest is built for a supported platform");
761        let resolved = RecordConfig::resolve(
762            &defaults,
763            &[default_override],
764            Some(&user_config),
765            &[],
766            &build_target,
767        );
768
769        // Default override is chosen over user base for enabled.
770        assert!(resolved.enabled);
771        // User base applies for max_records (override didn't set it).
772        assert_eq!(resolved.max_records, 25);
773    }
774
775    #[test]
776    fn test_resolve_override_clamps_max_output_size() {
777        let defaults = DefaultRecordConfig {
778            enabled: false,
779            max_records: 100,
780            max_total_size: ByteSize::gb(1),
781            max_age: Duration::from_secs(30 * 24 * 60 * 60),
782            max_output_size: ByteSize::mb(10),
783        };
784
785        // Override specifies a value below the minimum.
786        let override_ = make_override(
787            "cfg(all())",
788            DeserializedRecordOverrideData {
789                max_output_size: Some(ByteSize::b(100)), // Way below minimum.
790                ..Default::default()
791            },
792        );
793
794        let build_target =
795            Platform::build_target().expect("nextest is built for a supported platform");
796        let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &build_target);
797
798        // Should be clamped to the minimum.
799        assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
800    }
801}