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