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)]
44 pub enabled: Option<bool>,
45
46 #[serde(default)]
48 pub max_records: Option<usize>,
49
50 #[serde(default)]
52 #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
53 pub max_total_size: Option<ByteSize>,
54
55 #[serde(default, with = "humantime_serde")]
57 #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
58 pub max_age: Option<Duration>,
59
60 #[serde(default)]
63 #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
64 pub max_output_size: Option<ByteSize>,
65}
66
67#[derive(Clone, Debug, Deserialize)]
72#[serde(rename_all = "kebab-case")]
73pub struct DefaultRecordConfig {
74 pub enabled: bool,
76
77 pub max_records: usize,
79
80 pub max_total_size: ByteSize,
82
83 #[serde(with = "humantime_serde")]
85 pub max_age: Duration,
86
87 pub max_output_size: ByteSize,
89}
90
91#[derive(Clone, Debug, Default, Deserialize)]
96#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
97#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
98#[serde(rename_all = "kebab-case")]
99pub(in crate::user_config) struct DeserializedRecordOverrideData {
100 #[serde(default)]
102 pub(in crate::user_config) enabled: Option<bool>,
103
104 #[serde(default)]
106 pub(in crate::user_config) max_records: Option<usize>,
107
108 #[serde(default)]
110 #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
111 pub(in crate::user_config) max_total_size: Option<ByteSize>,
112
113 #[serde(default, with = "humantime_serde")]
115 #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
116 pub(in crate::user_config) max_age: Option<Duration>,
117
118 #[serde(default)]
121 #[cfg_attr(feature = "config-schema", schemars(with = "Option<String>"))]
122 pub(in crate::user_config) max_output_size: Option<ByteSize>,
123}
124
125#[derive(Clone, Debug)]
130pub(in crate::user_config) struct CompiledRecordOverride {
131 platform_spec: TargetSpec,
132 data: RecordOverrideData,
133}
134
135impl CompiledRecordOverride {
136 pub(in crate::user_config) fn new(
138 platform_spec: TargetSpec,
139 data: DeserializedRecordOverrideData,
140 ) -> Self {
141 Self {
142 platform_spec,
143 data: RecordOverrideData {
144 enabled: data.enabled,
145 max_records: data.max_records,
146 max_total_size: data.max_total_size,
147 max_age: data.max_age,
148 max_output_size: data.max_output_size,
149 },
150 }
151 }
152
153 pub(in crate::user_config) fn matches(&self, build_target: &Platform) -> bool {
158 self.platform_spec
159 .eval(build_target)
160 .unwrap_or(false)
161 }
162
163 pub(in crate::user_config) fn data(&self) -> &RecordOverrideData {
165 &self.data
166 }
167}
168
169#[derive(Clone, Debug, Default)]
171pub(in crate::user_config) struct RecordOverrideData {
172 enabled: Option<bool>,
173 max_records: Option<usize>,
174 max_total_size: Option<ByteSize>,
175 max_age: Option<Duration>,
176 max_output_size: Option<ByteSize>,
177}
178
179impl RecordOverrideData {
180 pub(in crate::user_config) fn enabled(&self) -> Option<&bool> {
182 self.enabled.as_ref()
183 }
184
185 pub(in crate::user_config) fn max_records(&self) -> Option<&usize> {
187 self.max_records.as_ref()
188 }
189
190 pub(in crate::user_config) fn max_total_size(&self) -> Option<&ByteSize> {
192 self.max_total_size.as_ref()
193 }
194
195 pub(in crate::user_config) fn max_age(&self) -> Option<&Duration> {
197 self.max_age.as_ref()
198 }
199
200 pub(in crate::user_config) fn max_output_size(&self) -> Option<&ByteSize> {
202 self.max_output_size.as_ref()
203 }
204}
205
206#[derive(Clone, Debug)]
208pub struct RecordConfig {
209 pub enabled: bool,
214
215 pub max_records: usize,
217
218 pub max_total_size: ByteSize,
220
221 pub max_age: Duration,
223
224 pub max_output_size: ByteSize,
226}
227
228impl RecordConfig {
229 pub(in crate::user_config) fn resolve(
244 default_config: &DefaultRecordConfig,
245 default_overrides: &[CompiledRecordOverride],
246 user_config: Option<&DeserializedRecordConfig>,
247 user_overrides: &[CompiledRecordOverride],
248 build_target: &Platform,
249 ) -> Self {
250 let mut max_output_size = resolve_record_setting(
251 &default_config.max_output_size,
252 default_overrides,
253 user_config.and_then(|c| c.max_output_size.as_ref()),
254 user_overrides,
255 build_target,
256 |data| data.max_output_size(),
257 );
258
259 if max_output_size < MIN_MAX_OUTPUT_SIZE {
261 tracing::warn!(
262 "max-output-size ({}) is below minimum ({}), using minimum",
263 max_output_size,
264 MIN_MAX_OUTPUT_SIZE,
265 );
266 max_output_size = MIN_MAX_OUTPUT_SIZE;
267 } else if max_output_size > MAX_MAX_OUTPUT_SIZE {
268 tracing::warn!(
269 "max-output-size ({}) exceeds maximum ({}), using maximum",
270 max_output_size,
271 MAX_MAX_OUTPUT_SIZE,
272 );
273 max_output_size = MAX_MAX_OUTPUT_SIZE;
274 }
275
276 Self {
277 enabled: resolve_record_setting(
278 &default_config.enabled,
279 default_overrides,
280 user_config.and_then(|c| c.enabled.as_ref()),
281 user_overrides,
282 build_target,
283 |data| data.enabled(),
284 ),
285 max_records: resolve_record_setting(
286 &default_config.max_records,
287 default_overrides,
288 user_config.and_then(|c| c.max_records.as_ref()),
289 user_overrides,
290 build_target,
291 |data| data.max_records(),
292 ),
293 max_total_size: resolve_record_setting(
294 &default_config.max_total_size,
295 default_overrides,
296 user_config.and_then(|c| c.max_total_size.as_ref()),
297 user_overrides,
298 build_target,
299 |data| data.max_total_size(),
300 ),
301 max_age: resolve_record_setting(
302 &default_config.max_age,
303 default_overrides,
304 user_config.and_then(|c| c.max_age.as_ref()),
305 user_overrides,
306 build_target,
307 |data| data.max_age(),
308 ),
309 max_output_size,
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_deserialized_record_config_parsing() {
320 let config: DeserializedRecordConfig = toml::from_str(
322 r#"
323 enabled = true
324 max-records = 50
325 max-total-size = "2GB"
326 max-age = "7d"
327 max-output-size = "5MB"
328 "#,
329 )
330 .unwrap();
331
332 assert_eq!(config.enabled, Some(true));
333 assert_eq!(config.max_records, Some(50));
334 assert_eq!(config.max_total_size, Some(ByteSize::gb(2)));
335 assert_eq!(config.max_age, Some(Duration::from_secs(7 * 24 * 60 * 60)));
336 assert_eq!(config.max_output_size, Some(ByteSize::mb(5)));
337
338 let config: DeserializedRecordConfig = toml::from_str(
340 r#"
341 max-records = 100
342 "#,
343 )
344 .unwrap();
345
346 assert!(config.enabled.is_none());
347 assert_eq!(config.max_records, Some(100));
348 assert!(config.max_total_size.is_none());
349 assert!(config.max_age.is_none());
350 assert!(config.max_output_size.is_none());
351
352 let config: DeserializedRecordConfig = toml::from_str("").unwrap();
354 assert!(config.enabled.is_none());
355 assert!(config.max_records.is_none());
356 assert!(config.max_total_size.is_none());
357 assert!(config.max_age.is_none());
358 assert!(config.max_output_size.is_none());
359 }
360
361 #[test]
362 fn test_default_record_config_parsing() {
363 let config: DefaultRecordConfig = toml::from_str(
364 r#"
365 enabled = true
366 max-records = 100
367 max-total-size = "1GB"
368 max-age = "30d"
369 max-output-size = "10MB"
370 "#,
371 )
372 .unwrap();
373
374 assert!(config.enabled);
375 assert_eq!(config.max_records, 100);
376 assert_eq!(config.max_total_size, ByteSize::gb(1));
377 assert_eq!(config.max_age, Duration::from_secs(30 * 24 * 60 * 60));
378 assert_eq!(config.max_output_size, ByteSize::mb(10));
379 }
380
381 #[test]
382 fn test_resolve_uses_defaults() {
383 let defaults = DefaultRecordConfig {
384 enabled: false,
385 max_records: 100,
386 max_total_size: ByteSize::gb(1),
387 max_age: Duration::from_secs(30 * 24 * 60 * 60),
388 max_output_size: ByteSize::mb(10),
389 };
390
391 let build_target =
392 Platform::build_target().expect("nextest is built for a supported platform");
393 let resolved = RecordConfig::resolve(&defaults, &[], None, &[], &build_target);
394
395 assert!(!resolved.enabled);
396 assert_eq!(resolved.max_records, 100);
397 assert_eq!(resolved.max_total_size, ByteSize::gb(1));
398 assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60));
399 assert_eq!(resolved.max_output_size, ByteSize::mb(10));
400 }
401
402 #[test]
403 fn test_resolve_user_overrides_defaults() {
404 let defaults = DefaultRecordConfig {
405 enabled: false,
406 max_records: 100,
407 max_total_size: ByteSize::gb(1),
408 max_age: Duration::from_secs(30 * 24 * 60 * 60),
409 max_output_size: ByteSize::mb(10),
410 };
411
412 let user_config = DeserializedRecordConfig {
413 enabled: Some(true),
414 max_records: Some(50),
415 max_total_size: None,
416 max_age: Some(Duration::from_secs(7 * 24 * 60 * 60)),
417 max_output_size: Some(ByteSize::mb(5)),
418 };
419
420 let build_target =
421 Platform::build_target().expect("nextest is built for a supported platform");
422 let resolved =
423 RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
424
425 assert!(resolved.enabled); 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)); }
431
432 #[test]
433 fn test_resolve_clamps_small_max_output_size() {
434 let defaults = DefaultRecordConfig {
435 enabled: false,
436 max_records: 100,
437 max_total_size: ByteSize::gb(1),
438 max_age: Duration::from_secs(30 * 24 * 60 * 60),
439 max_output_size: ByteSize::mb(10),
440 };
441
442 let user_config = DeserializedRecordConfig {
444 enabled: None,
445 max_records: None,
446 max_total_size: None,
447 max_age: None,
448 max_output_size: Some(ByteSize::b(100)), };
450
451 let build_target =
452 Platform::build_target().expect("nextest is built for a supported platform");
453 let resolved =
454 RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
455
456 assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
458 }
459
460 #[test]
461 fn test_resolve_accepts_value_at_minimum() {
462 let defaults = DefaultRecordConfig {
463 enabled: false,
464 max_records: 100,
465 max_total_size: ByteSize::gb(1),
466 max_age: Duration::from_secs(30 * 24 * 60 * 60),
467 max_output_size: ByteSize::mb(10),
468 };
469
470 let user_config = DeserializedRecordConfig {
472 enabled: None,
473 max_records: None,
474 max_total_size: None,
475 max_age: None,
476 max_output_size: Some(MIN_MAX_OUTPUT_SIZE),
477 };
478
479 let build_target =
480 Platform::build_target().expect("nextest is built for a supported platform");
481 let resolved =
482 RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
483
484 assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
486 }
487
488 #[test]
489 fn test_resolve_clamps_large_max_output_size() {
490 let defaults = DefaultRecordConfig {
491 enabled: false,
492 max_records: 100,
493 max_total_size: ByteSize::gb(1),
494 max_age: Duration::from_secs(30 * 24 * 60 * 60),
495 max_output_size: ByteSize::mb(10),
496 };
497
498 let user_config = DeserializedRecordConfig {
500 enabled: None,
501 max_records: None,
502 max_total_size: None,
503 max_age: None,
504 max_output_size: Some(ByteSize::gib(1)), };
506
507 let build_target =
508 Platform::build_target().expect("nextest is built for a supported platform");
509 let resolved =
510 RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
511
512 assert_eq!(resolved.max_output_size, MAX_MAX_OUTPUT_SIZE);
514 }
515
516 #[test]
517 fn test_resolve_accepts_value_at_maximum() {
518 let defaults = DefaultRecordConfig {
519 enabled: false,
520 max_records: 100,
521 max_total_size: ByteSize::gb(1),
522 max_age: Duration::from_secs(30 * 24 * 60 * 60),
523 max_output_size: ByteSize::mb(10),
524 };
525
526 let user_config = DeserializedRecordConfig {
528 enabled: None,
529 max_records: None,
530 max_total_size: None,
531 max_age: None,
532 max_output_size: Some(MAX_MAX_OUTPUT_SIZE),
533 };
534
535 let build_target =
536 Platform::build_target().expect("nextest is built for a supported platform");
537 let resolved =
538 RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
539
540 assert_eq!(resolved.max_output_size, MAX_MAX_OUTPUT_SIZE);
542 }
543
544 fn make_override(
546 platform: &str,
547 data: DeserializedRecordOverrideData,
548 ) -> CompiledRecordOverride {
549 let platform_spec =
550 TargetSpec::new(platform.to_string()).expect("valid platform spec in test");
551 CompiledRecordOverride::new(platform_spec, data)
552 }
553
554 #[test]
555 fn test_resolve_user_override_applies() {
556 let defaults = DefaultRecordConfig {
557 enabled: false,
558 max_records: 100,
559 max_total_size: ByteSize::gb(1),
560 max_age: Duration::from_secs(30 * 24 * 60 * 60),
561 max_output_size: ByteSize::mb(10),
562 };
563
564 let override_ = make_override(
566 "cfg(all())",
567 DeserializedRecordOverrideData {
568 enabled: Some(true),
569 max_records: Some(50),
570 ..Default::default()
571 },
572 );
573
574 let build_target =
575 Platform::build_target().expect("nextest is built for a supported platform");
576 let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &build_target);
577
578 assert!(resolved.enabled);
579 assert_eq!(resolved.max_records, 50);
580 assert_eq!(resolved.max_total_size, ByteSize::gb(1)); assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60)); assert_eq!(resolved.max_output_size, ByteSize::mb(10)); }
584
585 #[test]
586 fn test_resolve_default_override_applies() {
587 let defaults = DefaultRecordConfig {
588 enabled: false,
589 max_records: 100,
590 max_total_size: ByteSize::gb(1),
591 max_age: Duration::from_secs(30 * 24 * 60 * 60),
592 max_output_size: ByteSize::mb(10),
593 };
594
595 let override_ = make_override(
597 "cfg(all())",
598 DeserializedRecordOverrideData {
599 enabled: Some(true),
600 max_records: Some(50),
601 ..Default::default()
602 },
603 );
604
605 let build_target =
606 Platform::build_target().expect("nextest is built for a supported platform");
607 let resolved = RecordConfig::resolve(&defaults, &[override_], None, &[], &build_target);
608
609 assert!(resolved.enabled);
610 assert_eq!(resolved.max_records, 50);
611 assert_eq!(resolved.max_total_size, ByteSize::gb(1)); }
613
614 #[test]
615 fn test_resolve_platform_override_no_match() {
616 let defaults = DefaultRecordConfig {
617 enabled: false,
618 max_records: 100,
619 max_total_size: ByteSize::gb(1),
620 max_age: Duration::from_secs(30 * 24 * 60 * 60),
621 max_output_size: ByteSize::mb(10),
622 };
623
624 let override_ = make_override(
627 "cfg(any())",
628 DeserializedRecordOverrideData {
629 enabled: Some(true),
630 max_records: Some(50),
631 max_total_size: Some(ByteSize::gb(2)),
632 max_age: Some(Duration::from_secs(7 * 24 * 60 * 60)),
633 max_output_size: Some(ByteSize::mb(5)),
634 },
635 );
636
637 let build_target =
638 Platform::build_target().expect("nextest is built for a supported platform");
639 let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &build_target);
640
641 assert!(!resolved.enabled);
643 assert_eq!(resolved.max_records, 100);
644 assert_eq!(resolved.max_total_size, ByteSize::gb(1));
645 assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60));
646 assert_eq!(resolved.max_output_size, ByteSize::mb(10));
647 }
648
649 #[test]
650 fn test_resolve_first_matching_user_override_wins() {
651 let defaults = DefaultRecordConfig {
652 enabled: false,
653 max_records: 100,
654 max_total_size: ByteSize::gb(1),
655 max_age: Duration::from_secs(30 * 24 * 60 * 60),
656 max_output_size: ByteSize::mb(10),
657 };
658
659 let override1 = make_override(
661 "cfg(all())",
662 DeserializedRecordOverrideData {
663 enabled: Some(true),
664 ..Default::default()
665 },
666 );
667
668 let override2 = make_override(
669 "cfg(all())",
670 DeserializedRecordOverrideData {
671 enabled: Some(false), max_records: Some(50),
673 ..Default::default()
674 },
675 );
676
677 let build_target =
678 Platform::build_target().expect("nextest is built for a supported platform");
679 let resolved =
680 RecordConfig::resolve(&defaults, &[], None, &[override1, override2], &build_target);
681
682 assert!(resolved.enabled);
684 assert_eq!(resolved.max_records, 50);
686 }
687
688 #[test]
689 fn test_resolve_user_override_beats_default_override() {
690 let defaults = DefaultRecordConfig {
691 enabled: false,
692 max_records: 100,
693 max_total_size: ByteSize::gb(1),
694 max_age: Duration::from_secs(30 * 24 * 60 * 60),
695 max_output_size: ByteSize::mb(10),
696 };
697
698 let user_override = make_override(
700 "cfg(all())",
701 DeserializedRecordOverrideData {
702 enabled: Some(true),
703 ..Default::default()
704 },
705 );
706
707 let default_override = make_override(
709 "cfg(all())",
710 DeserializedRecordOverrideData {
711 enabled: Some(false), max_records: Some(50),
713 ..Default::default()
714 },
715 );
716
717 let build_target =
718 Platform::build_target().expect("nextest is built for a supported platform");
719 let resolved = RecordConfig::resolve(
720 &defaults,
721 &[default_override],
722 None,
723 &[user_override],
724 &build_target,
725 );
726
727 assert!(resolved.enabled);
729 assert_eq!(resolved.max_records, 50);
731 }
732
733 #[test]
734 fn test_resolve_override_beats_user_base() {
735 let defaults = DefaultRecordConfig {
736 enabled: false,
737 max_records: 100,
738 max_total_size: ByteSize::gb(1),
739 max_age: Duration::from_secs(30 * 24 * 60 * 60),
740 max_output_size: ByteSize::mb(10),
741 };
742
743 let user_config = DeserializedRecordConfig {
745 enabled: Some(false),
746 max_records: Some(25),
747 ..Default::default()
748 };
749
750 let default_override = make_override(
752 "cfg(all())",
753 DeserializedRecordOverrideData {
754 enabled: Some(true),
755 ..Default::default()
756 },
757 );
758
759 let build_target =
760 Platform::build_target().expect("nextest is built for a supported platform");
761 let resolved = RecordConfig::resolve(
762 &defaults,
763 &[default_override],
764 Some(&user_config),
765 &[],
766 &build_target,
767 );
768
769 assert!(resolved.enabled);
771 assert_eq!(resolved.max_records, 25);
773 }
774
775 #[test]
776 fn test_resolve_override_clamps_max_output_size() {
777 let defaults = DefaultRecordConfig {
778 enabled: false,
779 max_records: 100,
780 max_total_size: ByteSize::gb(1),
781 max_age: Duration::from_secs(30 * 24 * 60 * 60),
782 max_output_size: ByteSize::mb(10),
783 };
784
785 let override_ = make_override(
787 "cfg(all())",
788 DeserializedRecordOverrideData {
789 max_output_size: Some(ByteSize::b(100)), ..Default::default()
791 },
792 );
793
794 let build_target =
795 Platform::build_target().expect("nextest is built for a supported platform");
796 let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &build_target);
797
798 assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
800 }
801}