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