1use crate::user_config::helpers::resolve_record_setting;
7use bytesize::ByteSize;
8use serde::Deserialize;
9use std::time::Duration;
10use target_spec::{Platform, TargetSpec};
11
12pub const MIN_MAX_OUTPUT_SIZE: ByteSize = ByteSize::b(1000);
18
19pub const MAX_MAX_OUTPUT_SIZE: ByteSize = ByteSize::mib(256);
27
28#[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 #[serde(default)]
41 pub enabled: Option<bool>,
42
43 #[serde(default)]
45 pub max_records: Option<usize>,
46
47 #[serde(default)]
50 #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
51 pub max_total_size: Option<ByteSize>,
52
53 #[serde(default, with = "humantime_serde")]
55 #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
56 pub max_age: Option<Duration>,
57
58 #[serde(default)]
61 #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
62 pub max_output_size: Option<ByteSize>,
63}
64
65#[derive(Clone, Debug, Deserialize)]
70#[serde(rename_all = "kebab-case")]
71pub struct DefaultRecordConfig {
72 pub enabled: bool,
75
76 pub max_records: usize,
78
79 pub max_total_size: ByteSize,
82
83 #[serde(with = "humantime_serde")]
85 pub max_age: Duration,
86
87 pub max_output_size: ByteSize,
90}
91
92#[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 #[serde(default)]
104 pub(in crate::user_config) enabled: Option<bool>,
105
106 #[serde(default)]
108 pub(in crate::user_config) max_records: Option<usize>,
109
110 #[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 #[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 #[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#[derive(Clone, Debug)]
133pub(in crate::user_config) struct CompiledRecordOverride {
134 platform_spec: TargetSpec,
135 data: RecordOverrideData,
136}
137
138impl CompiledRecordOverride {
139 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 pub(in crate::user_config) fn matches(&self, build_target: &Platform) -> bool {
161 self.platform_spec
162 .eval(build_target)
163 .unwrap_or(false)
164 }
165
166 pub(in crate::user_config) fn data(&self) -> &RecordOverrideData {
168 &self.data
169 }
170}
171
172#[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 pub(in crate::user_config) fn enabled(&self) -> Option<&bool> {
185 self.enabled.as_ref()
186 }
187
188 pub(in crate::user_config) fn max_records(&self) -> Option<&usize> {
190 self.max_records.as_ref()
191 }
192
193 pub(in crate::user_config) fn max_total_size(&self) -> Option<&ByteSize> {
195 self.max_total_size.as_ref()
196 }
197
198 pub(in crate::user_config) fn max_age(&self) -> Option<&Duration> {
200 self.max_age.as_ref()
201 }
202
203 pub(in crate::user_config) fn max_output_size(&self) -> Option<&ByteSize> {
205 self.max_output_size.as_ref()
206 }
207}
208
209#[derive(Clone, Debug)]
211pub struct RecordConfig {
212 pub enabled: bool,
217
218 pub max_records: usize,
220
221 pub max_total_size: ByteSize,
223
224 pub max_age: Duration,
226
227 pub max_output_size: ByteSize,
229}
230
231impl RecordConfig {
232 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 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 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 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 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); assert_eq!(resolved.max_records, 50); assert_eq!(resolved.max_total_size, ByteSize::gb(1)); assert_eq!(resolved.max_age, Duration::from_secs(7 * 24 * 60 * 60)); assert_eq!(resolved.max_output_size, ByteSize::mb(5)); }
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 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)), };
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 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 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 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 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)), };
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 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 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 assert_eq!(resolved.max_output_size, MAX_MAX_OUTPUT_SIZE);
545 }
546
547 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 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)); assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60)); assert_eq!(resolved.max_output_size, ByteSize::mb(10)); }
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 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)); }
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 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 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 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), 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 assert!(resolved.enabled);
687 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 let user_override = make_override(
703 "cfg(all())",
704 DeserializedRecordOverrideData {
705 enabled: Some(true),
706 ..Default::default()
707 },
708 );
709
710 let default_override = make_override(
712 "cfg(all())",
713 DeserializedRecordOverrideData {
714 enabled: Some(false), 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 assert!(resolved.enabled);
732 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 let user_config = DeserializedRecordConfig {
748 enabled: Some(false),
749 max_records: Some(25),
750 ..Default::default()
751 };
752
753 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 assert!(resolved.enabled);
774 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 let override_ = make_override(
790 "cfg(all())",
791 DeserializedRecordOverrideData {
792 max_output_size: Some(ByteSize::b(100)), ..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 assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
803 }
804}