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 host 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, host_platform: &Platform) -> bool {
141        self.platform_spec
142            .eval(host_platform)
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 host
213    /// platform.
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        host_platform: &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            host_platform,
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                host_platform,
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                host_platform,
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                host_platform,
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                host_platform,
290                |data| data.max_age(),
291            ),
292            max_output_size,
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use crate::platform::detect_host_platform_for_tests;
301
302    #[test]
303    fn test_deserialized_record_config_parsing() {
304        // Test full config.
305        let config: DeserializedRecordConfig = toml::from_str(
306            r#"
307            enabled = true
308            max-records = 50
309            max-total-size = "2GB"
310            max-age = "7d"
311            max-output-size = "5MB"
312            "#,
313        )
314        .unwrap();
315
316        assert_eq!(config.enabled, Some(true));
317        assert_eq!(config.max_records, Some(50));
318        assert_eq!(config.max_total_size, Some(ByteSize::gb(2)));
319        assert_eq!(config.max_age, Some(Duration::from_secs(7 * 24 * 60 * 60)));
320        assert_eq!(config.max_output_size, Some(ByteSize::mb(5)));
321
322        // Test partial config.
323        let config: DeserializedRecordConfig = toml::from_str(
324            r#"
325            max-records = 100
326            "#,
327        )
328        .unwrap();
329
330        assert!(config.enabled.is_none());
331        assert_eq!(config.max_records, Some(100));
332        assert!(config.max_total_size.is_none());
333        assert!(config.max_age.is_none());
334        assert!(config.max_output_size.is_none());
335
336        // Test empty config.
337        let config: DeserializedRecordConfig = toml::from_str("").unwrap();
338        assert!(config.enabled.is_none());
339        assert!(config.max_records.is_none());
340        assert!(config.max_total_size.is_none());
341        assert!(config.max_age.is_none());
342        assert!(config.max_output_size.is_none());
343    }
344
345    #[test]
346    fn test_default_record_config_parsing() {
347        let config: DefaultRecordConfig = toml::from_str(
348            r#"
349            enabled = true
350            max-records = 100
351            max-total-size = "1GB"
352            max-age = "30d"
353            max-output-size = "10MB"
354            "#,
355        )
356        .unwrap();
357
358        assert!(config.enabled);
359        assert_eq!(config.max_records, 100);
360        assert_eq!(config.max_total_size, ByteSize::gb(1));
361        assert_eq!(config.max_age, Duration::from_secs(30 * 24 * 60 * 60));
362        assert_eq!(config.max_output_size, ByteSize::mb(10));
363    }
364
365    #[test]
366    fn test_resolve_uses_defaults() {
367        let defaults = DefaultRecordConfig {
368            enabled: false,
369            max_records: 100,
370            max_total_size: ByteSize::gb(1),
371            max_age: Duration::from_secs(30 * 24 * 60 * 60),
372            max_output_size: ByteSize::mb(10),
373        };
374
375        let host = detect_host_platform_for_tests();
376        let resolved = RecordConfig::resolve(&defaults, &[], None, &[], &host);
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 host = detect_host_platform_for_tests();
404        let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
405
406        assert!(resolved.enabled); // From user.
407        assert_eq!(resolved.max_records, 50); // From user.
408        assert_eq!(resolved.max_total_size, ByteSize::gb(1)); // From defaults.
409        assert_eq!(resolved.max_age, Duration::from_secs(7 * 24 * 60 * 60)); // From user.
410        assert_eq!(resolved.max_output_size, ByteSize::mb(5)); // From user.
411    }
412
413    #[test]
414    fn test_resolve_clamps_small_max_output_size() {
415        let defaults = DefaultRecordConfig {
416            enabled: false,
417            max_records: 100,
418            max_total_size: ByteSize::gb(1),
419            max_age: Duration::from_secs(30 * 24 * 60 * 60),
420            max_output_size: ByteSize::mb(10),
421        };
422
423        // User specifies a value below the minimum.
424        let user_config = DeserializedRecordConfig {
425            enabled: None,
426            max_records: None,
427            max_total_size: None,
428            max_age: None,
429            max_output_size: Some(ByteSize::b(100)), // Way below minimum.
430        };
431
432        let host = detect_host_platform_for_tests();
433        let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
434
435        // Should be clamped to the minimum.
436        assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
437    }
438
439    #[test]
440    fn test_resolve_accepts_value_at_minimum() {
441        let defaults = DefaultRecordConfig {
442            enabled: false,
443            max_records: 100,
444            max_total_size: ByteSize::gb(1),
445            max_age: Duration::from_secs(30 * 24 * 60 * 60),
446            max_output_size: ByteSize::mb(10),
447        };
448
449        // User specifies exactly the minimum.
450        let user_config = DeserializedRecordConfig {
451            enabled: None,
452            max_records: None,
453            max_total_size: None,
454            max_age: None,
455            max_output_size: Some(MIN_MAX_OUTPUT_SIZE),
456        };
457
458        let host = detect_host_platform_for_tests();
459        let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
460
461        // Should be accepted as-is.
462        assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
463    }
464
465    #[test]
466    fn test_resolve_clamps_large_max_output_size() {
467        let defaults = DefaultRecordConfig {
468            enabled: false,
469            max_records: 100,
470            max_total_size: ByteSize::gb(1),
471            max_age: Duration::from_secs(30 * 24 * 60 * 60),
472            max_output_size: ByteSize::mb(10),
473        };
474
475        // User specifies a value above the maximum.
476        let user_config = DeserializedRecordConfig {
477            enabled: None,
478            max_records: None,
479            max_total_size: None,
480            max_age: None,
481            max_output_size: Some(ByteSize::gib(1)), // Way above maximum.
482        };
483
484        let host = detect_host_platform_for_tests();
485        let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
486
487        // Should be clamped to the maximum.
488        assert_eq!(resolved.max_output_size, MAX_MAX_OUTPUT_SIZE);
489    }
490
491    #[test]
492    fn test_resolve_accepts_value_at_maximum() {
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 exactly 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(MAX_MAX_OUTPUT_SIZE),
508        };
509
510        let host = detect_host_platform_for_tests();
511        let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
512
513        // Should be accepted as-is.
514        assert_eq!(resolved.max_output_size, MAX_MAX_OUTPUT_SIZE);
515    }
516
517    /// Helper to create a CompiledRecordOverride for tests.
518    fn make_override(
519        platform: &str,
520        data: DeserializedRecordOverrideData,
521    ) -> CompiledRecordOverride {
522        let platform_spec =
523            TargetSpec::new(platform.to_string()).expect("valid platform spec in test");
524        CompiledRecordOverride::new(platform_spec, data)
525    }
526
527    #[test]
528    fn test_resolve_user_override_applies() {
529        let defaults = DefaultRecordConfig {
530            enabled: false,
531            max_records: 100,
532            max_total_size: ByteSize::gb(1),
533            max_age: Duration::from_secs(30 * 24 * 60 * 60),
534            max_output_size: ByteSize::mb(10),
535        };
536
537        // Create a user override that matches any platform.
538        let override_ = make_override(
539            "cfg(all())",
540            DeserializedRecordOverrideData {
541                enabled: Some(true),
542                max_records: Some(50),
543                ..Default::default()
544            },
545        );
546
547        let host = detect_host_platform_for_tests();
548        let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &host);
549
550        assert!(resolved.enabled);
551        assert_eq!(resolved.max_records, 50);
552        assert_eq!(resolved.max_total_size, ByteSize::gb(1)); // From defaults.
553        assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60)); // From defaults.
554        assert_eq!(resolved.max_output_size, ByteSize::mb(10)); // From defaults.
555    }
556
557    #[test]
558    fn test_resolve_default_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 default 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 host = detect_host_platform_for_tests();
578        let resolved = RecordConfig::resolve(&defaults, &[override_], None, &[], &host);
579
580        assert!(resolved.enabled);
581        assert_eq!(resolved.max_records, 50);
582        assert_eq!(resolved.max_total_size, ByteSize::gb(1)); // From defaults.
583    }
584
585    #[test]
586    fn test_resolve_platform_override_no_match() {
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 an override that never matches (cfg(any()) with no arguments
596        // is false).
597        let override_ = make_override(
598            "cfg(any())",
599            DeserializedRecordOverrideData {
600                enabled: Some(true),
601                max_records: Some(50),
602                max_total_size: Some(ByteSize::gb(2)),
603                max_age: Some(Duration::from_secs(7 * 24 * 60 * 60)),
604                max_output_size: Some(ByteSize::mb(5)),
605            },
606        );
607
608        let host = detect_host_platform_for_tests();
609        let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &host);
610
611        // Nothing should be overridden - all values should match defaults.
612        assert!(!resolved.enabled);
613        assert_eq!(resolved.max_records, 100);
614        assert_eq!(resolved.max_total_size, ByteSize::gb(1));
615        assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60));
616        assert_eq!(resolved.max_output_size, ByteSize::mb(10));
617    }
618
619    #[test]
620    fn test_resolve_first_matching_user_override_wins() {
621        let defaults = DefaultRecordConfig {
622            enabled: false,
623            max_records: 100,
624            max_total_size: ByteSize::gb(1),
625            max_age: Duration::from_secs(30 * 24 * 60 * 60),
626            max_output_size: ByteSize::mb(10),
627        };
628
629        // Create two user overrides that both match (cfg(all()) is always true).
630        let override1 = make_override(
631            "cfg(all())",
632            DeserializedRecordOverrideData {
633                enabled: Some(true),
634                ..Default::default()
635            },
636        );
637
638        let override2 = make_override(
639            "cfg(all())",
640            DeserializedRecordOverrideData {
641                enabled: Some(false), // Should be ignored.
642                max_records: Some(50),
643                ..Default::default()
644            },
645        );
646
647        let host = detect_host_platform_for_tests();
648        let resolved = RecordConfig::resolve(&defaults, &[], None, &[override1, override2], &host);
649
650        // First override wins for enabled.
651        assert!(resolved.enabled);
652        // Second override's max_records applies (first didn't set it).
653        assert_eq!(resolved.max_records, 50);
654    }
655
656    #[test]
657    fn test_resolve_user_override_beats_default_override() {
658        let defaults = DefaultRecordConfig {
659            enabled: false,
660            max_records: 100,
661            max_total_size: ByteSize::gb(1),
662            max_age: Duration::from_secs(30 * 24 * 60 * 60),
663            max_output_size: ByteSize::mb(10),
664        };
665
666        // User override sets enabled.
667        let user_override = make_override(
668            "cfg(all())",
669            DeserializedRecordOverrideData {
670                enabled: Some(true),
671                ..Default::default()
672            },
673        );
674
675        // Default override sets enabled and max_records.
676        let default_override = make_override(
677            "cfg(all())",
678            DeserializedRecordOverrideData {
679                enabled: Some(false), // Should be ignored.
680                max_records: Some(50),
681                ..Default::default()
682            },
683        );
684
685        let host = detect_host_platform_for_tests();
686        let resolved = RecordConfig::resolve(
687            &defaults,
688            &[default_override],
689            None,
690            &[user_override],
691            &host,
692        );
693
694        // User override wins for enabled.
695        assert!(resolved.enabled);
696        // Default override applies for max_records (user didn't set it).
697        assert_eq!(resolved.max_records, 50);
698    }
699
700    #[test]
701    fn test_resolve_override_beats_user_base() {
702        let defaults = DefaultRecordConfig {
703            enabled: false,
704            max_records: 100,
705            max_total_size: ByteSize::gb(1),
706            max_age: Duration::from_secs(30 * 24 * 60 * 60),
707            max_output_size: ByteSize::mb(10),
708        };
709
710        // User base config sets enabled.
711        let user_config = DeserializedRecordConfig {
712            enabled: Some(false),
713            max_records: Some(25),
714            ..Default::default()
715        };
716
717        // Default override sets enabled (should beat user base).
718        let default_override = make_override(
719            "cfg(all())",
720            DeserializedRecordOverrideData {
721                enabled: Some(true),
722                ..Default::default()
723            },
724        );
725
726        let host = detect_host_platform_for_tests();
727        let resolved = RecordConfig::resolve(
728            &defaults,
729            &[default_override],
730            Some(&user_config),
731            &[],
732            &host,
733        );
734
735        // Default override is chosen over user base for enabled.
736        assert!(resolved.enabled);
737        // User base applies for max_records (override didn't set it).
738        assert_eq!(resolved.max_records, 25);
739    }
740
741    #[test]
742    fn test_resolve_override_clamps_max_output_size() {
743        let defaults = DefaultRecordConfig {
744            enabled: false,
745            max_records: 100,
746            max_total_size: ByteSize::gb(1),
747            max_age: Duration::from_secs(30 * 24 * 60 * 60),
748            max_output_size: ByteSize::mb(10),
749        };
750
751        // Override specifies a value below the minimum.
752        let override_ = make_override(
753            "cfg(all())",
754            DeserializedRecordOverrideData {
755                max_output_size: Some(ByteSize::b(100)), // Way below minimum.
756                ..Default::default()
757            },
758        );
759
760        let host = detect_host_platform_for_tests();
761        let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &host);
762
763        // Should be clamped to the minimum.
764        assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
765    }
766}