1use super::{
5 CompiledProfileScripts, DeserializedProfileScriptConfig, EvaluatableProfile, LeakTimeout,
6 NextestConfig, NextestConfigImpl, TestPriority,
7};
8use crate::{
9 config::{FinalConfig, PreBuildPlatform, RetryPolicy, SlowTimeout, TestGroup, ThreadsRequired},
10 errors::{
11 ConfigCompileError, ConfigCompileErrorKind, ConfigCompileSection, ConfigParseErrorKind,
12 },
13 platform::BuildPlatforms,
14 reporter::TestOutputDisplay,
15};
16use guppy::graph::cargo::BuildPlatform;
17use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, ParseContext, TestQuery};
18use owo_colors::{OwoColorize, Style};
19use serde::{Deserialize, Deserializer};
20use smol_str::SmolStr;
21use std::collections::HashMap;
22use target_spec::{Platform, TargetSpec};
23
24#[derive(Clone, Debug)]
31pub struct TestSettings<'p, Source = ()> {
32 priority: (TestPriority, Source),
33 threads_required: (ThreadsRequired, Source),
34 run_extra_args: (&'p [String], Source),
35 retries: (RetryPolicy, Source),
36 slow_timeout: (SlowTimeout, Source),
37 leak_timeout: (LeakTimeout, Source),
38 test_group: (TestGroup, Source),
39 success_output: (TestOutputDisplay, Source),
40 failure_output: (TestOutputDisplay, Source),
41 junit_store_success_output: (bool, Source),
42 junit_store_failure_output: (bool, Source),
43}
44
45pub(crate) trait TrackSource<'p>: Sized {
46 fn track_default<T>(value: T) -> (T, Self);
47 fn track_profile<T>(value: T) -> (T, Self);
48 fn track_override<T>(value: T, source: &'p CompiledOverride<FinalConfig>) -> (T, Self);
49}
50
51impl<'p> TrackSource<'p> for () {
52 fn track_default<T>(value: T) -> (T, Self) {
53 (value, ())
54 }
55
56 fn track_profile<T>(value: T) -> (T, Self) {
57 (value, ())
58 }
59
60 fn track_override<T>(value: T, _source: &'p CompiledOverride<FinalConfig>) -> (T, Self) {
61 (value, ())
62 }
63}
64
65#[derive(Copy, Clone, Debug)]
66pub(crate) enum SettingSource<'p> {
67 Default,
70
71 Profile,
73
74 Override(&'p CompiledOverride<FinalConfig>),
76}
77
78impl<'p> TrackSource<'p> for SettingSource<'p> {
79 fn track_default<T>(value: T) -> (T, Self) {
80 (value, SettingSource::Default)
81 }
82
83 fn track_profile<T>(value: T) -> (T, Self) {
84 (value, SettingSource::Profile)
85 }
86
87 fn track_override<T>(value: T, source: &'p CompiledOverride<FinalConfig>) -> (T, Self) {
88 (value, SettingSource::Override(source))
89 }
90}
91
92impl<'p> TestSettings<'p> {
93 pub fn priority(&self) -> TestPriority {
95 self.priority.0
96 }
97
98 pub fn threads_required(&self) -> ThreadsRequired {
100 self.threads_required.0
101 }
102
103 pub fn run_extra_args(&self) -> &'p [String] {
105 self.run_extra_args.0
106 }
107
108 pub fn retries(&self) -> RetryPolicy {
110 self.retries.0
111 }
112
113 pub fn slow_timeout(&self) -> SlowTimeout {
115 self.slow_timeout.0
116 }
117
118 pub fn leak_timeout(&self) -> LeakTimeout {
120 self.leak_timeout.0
121 }
122
123 pub fn test_group(&self) -> &TestGroup {
125 &self.test_group.0
126 }
127
128 pub fn success_output(&self) -> TestOutputDisplay {
130 self.success_output.0
131 }
132
133 pub fn failure_output(&self) -> TestOutputDisplay {
135 self.failure_output.0
136 }
137
138 pub fn junit_store_success_output(&self) -> bool {
140 self.junit_store_success_output.0
141 }
142
143 pub fn junit_store_failure_output(&self) -> bool {
145 self.junit_store_failure_output.0
146 }
147}
148
149#[expect(dead_code)]
150impl<'p, Source: Copy> TestSettings<'p, Source> {
151 pub(super) fn new(profile: &'p EvaluatableProfile<'_>, query: &TestQuery<'_>) -> Self
152 where
153 Source: TrackSource<'p>,
154 {
155 let ecx = profile.filterset_ecx();
156
157 let mut priority = None;
158 let mut threads_required = None;
159 let mut run_extra_args = None;
160 let mut retries = None;
161 let mut slow_timeout = None;
162 let mut leak_timeout = None;
163 let mut test_group = None;
164 let mut success_output = None;
165 let mut failure_output = None;
166 let mut junit_store_success_output = None;
167 let mut junit_store_failure_output = None;
168
169 for override_ in &profile.compiled_data.overrides {
170 if !override_.state.host_eval {
171 continue;
172 }
173 if query.binary_query.platform == BuildPlatform::Host && !override_.state.host_test_eval
174 {
175 continue;
176 }
177 if query.binary_query.platform == BuildPlatform::Target && !override_.state.target_eval
178 {
179 continue;
180 }
181
182 if let Some(expr) = &override_.filter() {
183 if !expr.matches_test(query, &ecx) {
184 continue;
185 }
186 }
188
189 if priority.is_none() {
190 if let Some(p) = override_.data.priority {
191 priority = Some(Source::track_override(p, override_));
192 }
193 }
194 if threads_required.is_none() {
195 if let Some(t) = override_.data.threads_required {
196 threads_required = Some(Source::track_override(t, override_));
197 }
198 }
199 if run_extra_args.is_none() {
200 if let Some(r) = override_.data.run_extra_args.as_deref() {
201 run_extra_args = Some(Source::track_override(r, override_));
202 }
203 }
204 if retries.is_none() {
205 if let Some(r) = override_.data.retries {
206 retries = Some(Source::track_override(r, override_));
207 }
208 }
209 if slow_timeout.is_none() {
210 if let Some(s) = override_.data.slow_timeout {
211 slow_timeout = Some(Source::track_override(s, override_));
212 }
213 }
214 if leak_timeout.is_none() {
215 if let Some(l) = override_.data.leak_timeout {
216 leak_timeout = Some(Source::track_override(l, override_));
217 }
218 }
219 if test_group.is_none() {
220 if let Some(t) = &override_.data.test_group {
221 test_group = Some(Source::track_override(t.clone(), override_));
222 }
223 }
224 if success_output.is_none() {
225 if let Some(s) = override_.data.success_output {
226 success_output = Some(Source::track_override(s, override_));
227 }
228 }
229 if failure_output.is_none() {
230 if let Some(f) = override_.data.failure_output {
231 failure_output = Some(Source::track_override(f, override_));
232 }
233 }
234 if junit_store_success_output.is_none() {
235 if let Some(s) = override_.data.junit.store_success_output {
236 junit_store_success_output = Some(Source::track_override(s, override_));
237 }
238 }
239 if junit_store_failure_output.is_none() {
240 if let Some(f) = override_.data.junit.store_failure_output {
241 junit_store_failure_output = Some(Source::track_override(f, override_));
242 }
243 }
244 }
245
246 let priority = priority.unwrap_or_else(|| Source::track_default(TestPriority::default()));
248 let threads_required =
249 threads_required.unwrap_or_else(|| Source::track_profile(profile.threads_required()));
250 let run_extra_args =
251 run_extra_args.unwrap_or_else(|| Source::track_profile(profile.run_extra_args()));
252 let retries = retries.unwrap_or_else(|| Source::track_profile(profile.retries()));
253 let slow_timeout =
254 slow_timeout.unwrap_or_else(|| Source::track_profile(profile.slow_timeout()));
255 let leak_timeout =
256 leak_timeout.unwrap_or_else(|| Source::track_profile(profile.leak_timeout()));
257 let test_group = test_group.unwrap_or_else(|| Source::track_profile(TestGroup::Global));
258 let success_output =
259 success_output.unwrap_or_else(|| Source::track_profile(profile.success_output()));
260 let failure_output =
261 failure_output.unwrap_or_else(|| Source::track_profile(profile.failure_output()));
262 let junit_store_success_output = junit_store_success_output.unwrap_or_else(|| {
263 Source::track_profile(profile.junit().is_some_and(|j| j.store_success_output()))
265 });
266 let junit_store_failure_output = junit_store_failure_output.unwrap_or_else(|| {
267 Source::track_profile(profile.junit().is_some_and(|j| j.store_failure_output()))
269 });
270
271 TestSettings {
272 threads_required,
273 run_extra_args,
274 retries,
275 priority,
276 slow_timeout,
277 leak_timeout,
278 test_group,
279 success_output,
280 failure_output,
281 junit_store_success_output,
282 junit_store_failure_output,
283 }
284 }
285
286 pub(crate) fn threads_required_with_source(&self) -> (ThreadsRequired, Source) {
288 self.threads_required
289 }
290
291 pub(crate) fn retries_with_source(&self) -> (RetryPolicy, Source) {
293 self.retries
294 }
295
296 pub(crate) fn slow_timeout_with_source(&self) -> (SlowTimeout, Source) {
298 self.slow_timeout
299 }
300
301 pub(crate) fn leak_timeout_with_source(&self) -> (LeakTimeout, Source) {
303 self.leak_timeout
304 }
305
306 pub(crate) fn test_group_with_source(&self) -> &(TestGroup, Source) {
308 &self.test_group
309 }
310}
311
312#[derive(Clone, Debug)]
313pub(super) struct CompiledByProfile {
314 pub(super) default: CompiledData<PreBuildPlatform>,
315 pub(super) other: HashMap<String, CompiledData<PreBuildPlatform>>,
316}
317
318impl CompiledByProfile {
319 pub(super) fn new(
320 pcx: &ParseContext<'_>,
321 config: &NextestConfigImpl,
322 ) -> Result<Self, ConfigParseErrorKind> {
323 let mut errors = vec![];
324 let default = CompiledData::new(
325 pcx,
326 "default",
327 Some(config.default_profile().default_filter()),
328 config.default_profile().overrides(),
329 config.default_profile().setup_scripts(),
330 &mut errors,
331 );
332 let other: HashMap<_, _> = config
333 .other_profiles()
334 .map(|(profile_name, profile)| {
335 (
336 profile_name.to_owned(),
337 CompiledData::new(
338 pcx,
339 profile_name,
340 profile.default_filter(),
341 profile.overrides(),
342 profile.scripts(),
343 &mut errors,
344 ),
345 )
346 })
347 .collect();
348
349 if errors.is_empty() {
350 Ok(Self { default, other })
351 } else {
352 Err(ConfigParseErrorKind::CompileErrors(errors))
353 }
354 }
355
356 pub(super) fn for_default_config() -> Self {
362 Self {
363 default: CompiledData {
364 profile_default_filter: Some(CompiledDefaultFilter::for_default_config()),
365 overrides: vec![],
366 scripts: vec![],
367 },
368 other: HashMap::new(),
369 }
370 }
371}
372
373#[derive(Clone, Debug)]
377pub struct CompiledDefaultFilter {
378 pub expr: CompiledExpr,
389
390 pub profile: String,
392
393 pub section: CompiledDefaultFilterSection,
395}
396
397impl CompiledDefaultFilter {
398 pub(crate) fn for_default_config() -> Self {
399 Self {
400 expr: CompiledExpr::ALL,
401 profile: NextestConfig::DEFAULT_PROFILE.to_owned(),
402 section: CompiledDefaultFilterSection::Profile,
403 }
404 }
405
406 pub fn display_config(&self, bold_style: Style) -> String {
408 match &self.section {
409 CompiledDefaultFilterSection::Profile => {
410 format!("profile.{}.default-filter", self.profile)
411 .style(bold_style)
412 .to_string()
413 }
414 CompiledDefaultFilterSection::Override(_) => {
415 format!(
416 "default-filter in {}",
417 format!("profile.{}.overrides", self.profile).style(bold_style)
418 )
419 }
420 }
421 }
422}
423
424#[derive(Clone, Copy, Debug)]
427pub enum CompiledDefaultFilterSection {
428 Profile,
430
431 Override(usize),
433}
434
435#[derive(Clone, Debug)]
436pub(super) struct CompiledData<State> {
437 pub(super) profile_default_filter: Option<CompiledDefaultFilter>,
442 pub(super) overrides: Vec<CompiledOverride<State>>,
443 pub(super) scripts: Vec<CompiledProfileScripts<State>>,
444}
445
446impl CompiledData<PreBuildPlatform> {
447 fn new(
448 pcx: &ParseContext<'_>,
449 profile_name: &str,
450 profile_default_filter: Option<&str>,
451 overrides: &[DeserializedOverride],
452 scripts: &[DeserializedProfileScriptConfig],
453 errors: &mut Vec<ConfigCompileError>,
454 ) -> Self {
455 let profile_default_filter =
456 profile_default_filter.and_then(|filter| {
457 match Filterset::parse(filter.to_owned(), pcx, FiltersetKind::DefaultFilter) {
458 Ok(expr) => Some(CompiledDefaultFilter {
459 expr: expr.compiled,
460 profile: profile_name.to_owned(),
461 section: CompiledDefaultFilterSection::Profile,
462 }),
463 Err(err) => {
464 errors.push(ConfigCompileError {
465 profile_name: profile_name.to_owned(),
466 section: ConfigCompileSection::DefaultFilter,
467 kind: ConfigCompileErrorKind::Parse {
468 host_parse_error: None,
469 target_parse_error: None,
470 filter_parse_errors: vec![err],
471 },
472 });
473 None
474 }
475 }
476 });
477
478 let overrides = overrides
479 .iter()
480 .enumerate()
481 .filter_map(|(index, source)| {
482 CompiledOverride::new(pcx, profile_name, index, source, errors)
483 })
484 .collect();
485 let scripts = scripts
486 .iter()
487 .enumerate()
488 .filter_map(|(index, source)| {
489 CompiledProfileScripts::new(pcx, profile_name, index, source, errors)
490 })
491 .collect();
492 Self {
493 profile_default_filter,
494 overrides,
495 scripts,
496 }
497 }
498
499 pub(super) fn extend_reverse(&mut self, other: Self) {
500 if other.profile_default_filter.is_some() {
502 self.profile_default_filter = other.profile_default_filter;
503 }
504 self.overrides.extend(other.overrides.into_iter().rev());
505 self.scripts.extend(other.scripts.into_iter().rev());
506 }
507
508 pub(super) fn reverse(&mut self) {
509 self.overrides.reverse();
510 self.scripts.reverse();
511 }
512
513 pub(super) fn chain(self, other: Self) -> Self {
515 let profile_default_filter = self.profile_default_filter.or(other.profile_default_filter);
516 let mut overrides = self.overrides;
517 let mut setup_scripts = self.scripts;
518 overrides.extend(other.overrides);
519 setup_scripts.extend(other.scripts);
520 Self {
521 profile_default_filter,
522 overrides,
523 scripts: setup_scripts,
524 }
525 }
526
527 pub(super) fn apply_build_platforms(
528 self,
529 build_platforms: &BuildPlatforms,
530 ) -> CompiledData<FinalConfig> {
531 let profile_default_filter = self.profile_default_filter;
532 let overrides = self
533 .overrides
534 .into_iter()
535 .map(|override_| override_.apply_build_platforms(build_platforms))
536 .collect();
537 let setup_scripts = self
538 .scripts
539 .into_iter()
540 .map(|setup_script| setup_script.apply_build_platforms(build_platforms))
541 .collect();
542 CompiledData {
543 profile_default_filter,
544 overrides,
545 scripts: setup_scripts,
546 }
547 }
548}
549
550#[derive(Clone, Debug)]
551pub(crate) struct CompiledOverride<State> {
552 id: OverrideId,
553 state: State,
554 pub(super) data: ProfileOverrideData,
555}
556
557impl<State> CompiledOverride<State> {
558 pub(crate) fn id(&self) -> &OverrideId {
559 &self.id
560 }
561}
562
563#[derive(Clone, Debug, Eq, Hash, PartialEq)]
564pub(crate) struct OverrideId {
565 pub(crate) profile_name: SmolStr,
566 index: usize,
567}
568
569#[derive(Clone, Debug)]
570pub(super) struct ProfileOverrideData {
571 host_spec: MaybeTargetSpec,
572 target_spec: MaybeTargetSpec,
573 filter: Option<FilterOrDefaultFilter>,
574 priority: Option<TestPriority>,
575 threads_required: Option<ThreadsRequired>,
576 run_extra_args: Option<Vec<String>>,
577 retries: Option<RetryPolicy>,
578 slow_timeout: Option<SlowTimeout>,
579 leak_timeout: Option<LeakTimeout>,
580 pub(super) test_group: Option<TestGroup>,
581 success_output: Option<TestOutputDisplay>,
582 failure_output: Option<TestOutputDisplay>,
583 junit: DeserializedJunitOutput,
584}
585
586impl CompiledOverride<PreBuildPlatform> {
587 fn new(
588 pcx: &ParseContext<'_>,
589 profile_name: &str,
590 index: usize,
591 source: &DeserializedOverride,
592 errors: &mut Vec<ConfigCompileError>,
593 ) -> Option<Self> {
594 if source.platform.host.is_none()
595 && source.platform.target.is_none()
596 && source.filter.is_none()
597 {
598 errors.push(ConfigCompileError {
599 profile_name: profile_name.to_owned(),
600 section: ConfigCompileSection::Override(index),
601 kind: ConfigCompileErrorKind::ConstraintsNotSpecified {
602 default_filter_specified: source.default_filter.is_some(),
603 },
604 });
605 return None;
606 }
607
608 let host_spec = MaybeTargetSpec::new(source.platform.host.as_deref());
609 let target_spec = MaybeTargetSpec::new(source.platform.target.as_deref());
610 let filter = source.filter.as_ref().map_or(Ok(None), |filter| {
611 Some(Filterset::parse(filter.clone(), pcx, FiltersetKind::Test)).transpose()
612 });
613 let default_filter = source.default_filter.as_ref().map_or(Ok(None), |filter| {
614 Some(Filterset::parse(
615 filter.clone(),
616 pcx,
617 FiltersetKind::DefaultFilter,
618 ))
619 .transpose()
620 });
621
622 match (host_spec, target_spec, filter, default_filter) {
623 (Ok(host_spec), Ok(target_spec), Ok(filter), Ok(default_filter)) => {
624 let filter = match (filter, default_filter) {
626 (Some(_), Some(_)) => {
627 errors.push(ConfigCompileError {
628 profile_name: profile_name.to_owned(),
629 section: ConfigCompileSection::Override(index),
630 kind: ConfigCompileErrorKind::FilterAndDefaultFilterSpecified,
631 });
632 return None;
633 }
634 (Some(filter), None) => Some(FilterOrDefaultFilter::Filter(filter)),
635 (None, Some(default_filter)) => {
636 let compiled = CompiledDefaultFilter {
637 expr: default_filter.compiled,
638 profile: profile_name.to_owned(),
639 section: CompiledDefaultFilterSection::Override(index),
640 };
641 Some(FilterOrDefaultFilter::DefaultFilter(compiled))
642 }
643 (None, None) => None,
644 };
645
646 Some(Self {
647 id: OverrideId {
648 profile_name: profile_name.into(),
649 index,
650 },
651 state: PreBuildPlatform {},
652 data: ProfileOverrideData {
653 host_spec,
654 target_spec,
655 filter,
656 priority: source.priority,
657 threads_required: source.threads_required,
658 run_extra_args: source.run_extra_args.clone(),
659 retries: source.retries,
660 slow_timeout: source.slow_timeout,
661 leak_timeout: source.leak_timeout,
662 test_group: source.test_group.clone(),
663 success_output: source.success_output,
664 failure_output: source.failure_output,
665 junit: source.junit,
666 },
667 })
668 }
669 (maybe_host_err, maybe_target_err, maybe_filter_err, maybe_default_filter_err) => {
670 let host_parse_error = maybe_host_err.err();
671 let target_parse_error = maybe_target_err.err();
672 let filter_parse_errors = maybe_filter_err
673 .err()
674 .into_iter()
675 .chain(maybe_default_filter_err.err())
676 .collect();
677
678 errors.push(ConfigCompileError {
679 profile_name: profile_name.to_owned(),
680 section: ConfigCompileSection::Override(index),
681 kind: ConfigCompileErrorKind::Parse {
682 host_parse_error,
683 target_parse_error,
684 filter_parse_errors,
685 },
686 });
687 None
688 }
689 }
690 }
691
692 pub(super) fn apply_build_platforms(
693 self,
694 build_platforms: &BuildPlatforms,
695 ) -> CompiledOverride<FinalConfig> {
696 let host_eval = self.data.host_spec.eval(&build_platforms.host.platform);
697 let host_test_eval = self.data.target_spec.eval(&build_platforms.host.platform);
698 let target_eval = build_platforms
699 .target
700 .as_ref()
701 .map_or(host_test_eval, |target| {
702 self.data.target_spec.eval(&target.triple.platform)
703 });
704
705 CompiledOverride {
706 id: self.id,
707 state: FinalConfig {
708 host_eval,
709 host_test_eval,
710 target_eval,
711 },
712 data: self.data,
713 }
714 }
715}
716
717impl CompiledOverride<FinalConfig> {
718 pub(crate) fn target_spec(&self) -> &MaybeTargetSpec {
720 &self.data.target_spec
721 }
722
723 pub(crate) fn filter(&self) -> Option<&Filterset> {
725 match self.data.filter.as_ref() {
726 Some(FilterOrDefaultFilter::Filter(filter)) => Some(filter),
727 _ => None,
728 }
729 }
730
731 pub(crate) fn default_filter_if_matches_platform(&self) -> Option<&CompiledDefaultFilter> {
733 match self.data.filter.as_ref() {
734 Some(FilterOrDefaultFilter::DefaultFilter(filter)) => {
735 (self.state.host_eval && self.state.target_eval).then_some(filter)
742 }
743 _ => None,
744 }
745 }
746}
747
748#[derive(Clone, Debug, Default)]
750pub(crate) enum MaybeTargetSpec {
751 Provided(TargetSpec),
752 #[default]
753 Any,
754}
755
756impl MaybeTargetSpec {
757 pub(super) fn new(platform_str: Option<&str>) -> Result<Self, target_spec::Error> {
758 Ok(match platform_str {
759 Some(platform_str) => {
760 MaybeTargetSpec::Provided(TargetSpec::new(platform_str.to_owned())?)
761 }
762 None => MaybeTargetSpec::Any,
763 })
764 }
765
766 pub(super) fn eval(&self, platform: &Platform) -> bool {
767 match self {
768 MaybeTargetSpec::Provided(spec) => spec
769 .eval(platform)
770 .unwrap_or(true),
771 MaybeTargetSpec::Any => true,
772 }
773 }
774}
775
776#[derive(Clone, Debug)]
780pub(crate) enum FilterOrDefaultFilter {
781 Filter(Filterset),
782 DefaultFilter(CompiledDefaultFilter),
783}
784
785#[derive(Clone, Debug, Deserialize)]
787#[serde(rename_all = "kebab-case")]
788pub(super) struct DeserializedOverride {
789 #[serde(default)]
791 platform: PlatformStrings,
792 #[serde(default)]
794 filter: Option<String>,
795 #[serde(default)]
798 priority: Option<TestPriority>,
799 #[serde(default)]
800 default_filter: Option<String>,
801 #[serde(default)]
802 threads_required: Option<ThreadsRequired>,
803 #[serde(default)]
804 run_extra_args: Option<Vec<String>>,
805 #[serde(default, deserialize_with = "super::deserialize_retry_policy")]
806 retries: Option<RetryPolicy>,
807 #[serde(default, deserialize_with = "super::deserialize_slow_timeout")]
808 slow_timeout: Option<SlowTimeout>,
809 #[serde(default, deserialize_with = "super::deserialize_leak_timeout")]
810 leak_timeout: Option<LeakTimeout>,
811 #[serde(default)]
812 test_group: Option<TestGroup>,
813 #[serde(default)]
814 success_output: Option<TestOutputDisplay>,
815 #[serde(default)]
816 failure_output: Option<TestOutputDisplay>,
817 #[serde(default)]
818 junit: DeserializedJunitOutput,
819}
820
821#[derive(Copy, Clone, Debug, Default, Deserialize)]
822#[serde(rename_all = "kebab-case")]
823pub(super) struct DeserializedJunitOutput {
824 store_success_output: Option<bool>,
825 store_failure_output: Option<bool>,
826}
827
828#[derive(Clone, Debug, Default)]
829pub(super) struct PlatformStrings {
830 pub(super) host: Option<String>,
831 pub(super) target: Option<String>,
832}
833
834impl<'de> Deserialize<'de> for PlatformStrings {
835 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
836 struct V;
837
838 impl<'de2> serde::de::Visitor<'de2> for V {
839 type Value = PlatformStrings;
840
841 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
842 formatter.write_str(
843 "a table ({ host = \"x86_64-apple-darwin\", \
844 target = \"cfg(windows)\" }) \
845 or a string (\"x86_64-unknown-gnu-linux\")",
846 )
847 }
848
849 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
850 where
851 E: serde::de::Error,
852 {
853 Ok(PlatformStrings {
854 host: None,
855 target: Some(v.to_owned()),
856 })
857 }
858
859 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
860 where
861 A: serde::de::MapAccess<'de2>,
862 {
863 #[derive(Deserialize)]
864 struct PlatformStringsInner {
865 #[serde(default)]
866 host: Option<String>,
867 #[serde(default)]
868 target: Option<String>,
869 }
870
871 let inner = PlatformStringsInner::deserialize(
872 serde::de::value::MapAccessDeserializer::new(map),
873 )?;
874 Ok(PlatformStrings {
875 host: inner.host,
876 target: inner.target,
877 })
878 }
879 }
880
881 deserializer.deserialize_any(V)
882 }
883}
884
885#[cfg(test)]
886mod tests {
887 use super::*;
888 use crate::config::{LeakTimeoutResult, NextestConfig, test_helpers::*};
889 use camino_tempfile::tempdir;
890 use indoc::indoc;
891 use std::{num::NonZeroUsize, time::Duration};
892 use test_case::test_case;
893
894 #[test]
896 fn test_overrides_basic() {
897 let config_contents = indoc! {r#"
898 # Override 1
899 [[profile.default.overrides]]
900 platform = 'aarch64-apple-darwin' # this is the target platform
901 filter = "test(test)"
902 retries = { backoff = "exponential", count = 20, delay = "1s", max-delay = "20s" }
903 slow-timeout = { period = "120s", terminate-after = 1, grace-period = "0s" }
904 success-output = "immediate-final"
905 junit = { store-success-output = true }
906
907 # Override 2
908 [[profile.default.overrides]]
909 filter = "test(test)"
910 threads-required = 8
911 retries = 3
912 slow-timeout = "60s"
913 leak-timeout = "300ms"
914 test-group = "my-group"
915 failure-output = "final"
916 junit = { store-failure-output = false }
917
918 # Override 3
919 [[profile.default.overrides]]
920 platform = { host = "cfg(unix)" }
921 filter = "test(override3)"
922 retries = 5
923
924 # Override 4 -- host not matched
925 [[profile.default.overrides]]
926 platform = { host = 'aarch64-apple-darwin' }
927 retries = 10
928
929 # Override 5 -- no filter provided, just platform
930 [[profile.default.overrides]]
931 platform = { host = 'cfg(target_os = "linux")', target = 'aarch64-apple-darwin' }
932 filter = "test(override5)"
933 retries = 8
934
935 [profile.default.junit]
936 path = "my-path.xml"
937
938 [test-groups.my-group]
939 max-threads = 20
940 "#};
941
942 let workspace_dir = tempdir().unwrap();
943
944 let graph = temp_workspace(&workspace_dir, config_contents);
945 let package_id = graph.workspace().iter().next().unwrap().id();
946
947 let pcx = ParseContext::new(&graph);
948
949 let nextest_config_result = NextestConfig::from_sources(
950 graph.workspace().root(),
951 &pcx,
952 None,
953 &[][..],
954 &Default::default(),
955 )
956 .expect("config is valid");
957 let profile = nextest_config_result
958 .profile("default")
959 .expect("valid profile name")
960 .apply_build_platforms(&build_platforms());
961
962 let host_binary_query =
964 binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
965 let query = TestQuery {
966 binary_query: host_binary_query.to_query(),
967 test_name: "test",
968 };
969 let overrides = profile.settings_for(&query);
970
971 assert_eq!(overrides.threads_required(), ThreadsRequired::Count(8));
972 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(3));
973 assert_eq!(
974 overrides.slow_timeout(),
975 SlowTimeout {
976 period: Duration::from_secs(60),
977 terminate_after: None,
978 grace_period: Duration::from_secs(10),
979 }
980 );
981 assert_eq!(
982 overrides.leak_timeout(),
983 LeakTimeout {
984 period: Duration::from_millis(300),
985 result: LeakTimeoutResult::Pass,
986 }
987 );
988 assert_eq!(overrides.test_group(), &test_group("my-group"));
989 assert_eq!(overrides.success_output(), TestOutputDisplay::Never);
990 assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
991 #[expect(clippy::bool_assert_comparison)]
993 {
994 assert_eq!(overrides.junit_store_success_output(), false);
995 assert_eq!(overrides.junit_store_failure_output(), false);
996 }
997
998 let target_binary_query = binary_query(
1000 &graph,
1001 package_id,
1002 "lib",
1003 "my-binary",
1004 BuildPlatform::Target,
1005 );
1006 let query = TestQuery {
1007 binary_query: target_binary_query.to_query(),
1008 test_name: "test",
1009 };
1010 let overrides = profile.settings_for(&query);
1011
1012 assert_eq!(overrides.threads_required(), ThreadsRequired::Count(8));
1013 assert_eq!(
1014 overrides.retries(),
1015 RetryPolicy::Exponential {
1016 count: 20,
1017 delay: Duration::from_secs(1),
1018 jitter: false,
1019 max_delay: Some(Duration::from_secs(20)),
1020 }
1021 );
1022 assert_eq!(
1023 overrides.slow_timeout(),
1024 SlowTimeout {
1025 period: Duration::from_secs(120),
1026 terminate_after: Some(NonZeroUsize::new(1).unwrap()),
1027 grace_period: Duration::ZERO,
1028 }
1029 );
1030 assert_eq!(
1031 overrides.leak_timeout(),
1032 LeakTimeout {
1033 period: Duration::from_millis(300),
1034 result: LeakTimeoutResult::Pass,
1035 }
1036 );
1037 assert_eq!(overrides.test_group(), &test_group("my-group"));
1038 assert_eq!(
1039 overrides.success_output(),
1040 TestOutputDisplay::ImmediateFinal
1041 );
1042 assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
1043 #[expect(clippy::bool_assert_comparison)]
1045 {
1046 assert_eq!(overrides.junit_store_success_output(), true);
1047 assert_eq!(overrides.junit_store_failure_output(), false);
1048 }
1049
1050 let query = TestQuery {
1052 binary_query: target_binary_query.to_query(),
1053 test_name: "override3",
1054 };
1055 let overrides = profile.settings_for(&query);
1056 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(5));
1057
1058 let query = TestQuery {
1060 binary_query: target_binary_query.to_query(),
1061 test_name: "override5",
1062 };
1063 let overrides = profile.settings_for(&query);
1064 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(8));
1065
1066 let query = TestQuery {
1068 binary_query: target_binary_query.to_query(),
1069 test_name: "no_match",
1070 };
1071 let overrides = profile.settings_for(&query);
1072 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(0));
1073 }
1074
1075 #[test_case(
1076 indoc! {r#"
1077 [[profile.default.overrides]]
1078 retries = 2
1079 "#},
1080 "default",
1081 &[MietteJsonReport {
1082 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1083 labels: vec![],
1084 }]
1085
1086 ; "neither platform nor filter specified"
1087 )]
1088 #[test_case(
1089 indoc! {r#"
1090 [[profile.default.overrides]]
1091 default-filter = "test(test1)"
1092 retries = 2
1093 "#},
1094 "default",
1095 &[MietteJsonReport {
1096 message: "for override with `default-filter`, `platform` must also be specified".to_owned(),
1097 labels: vec![],
1098 }]
1099
1100 ; "default-filter without platform"
1101 )]
1102 #[test_case(
1103 indoc! {r#"
1104 [[profile.default.overrides]]
1105 platform = 'cfg(unix)'
1106 default-filter = "not default()"
1107 retries = 2
1108 "#},
1109 "default",
1110 &[MietteJsonReport {
1111 message: "predicate not allowed in `default-filter` expressions".to_owned(),
1112 labels: vec![
1113 MietteJsonLabel {
1114 label: "this predicate causes infinite recursion".to_owned(),
1115 span: MietteJsonSpan { offset: 4, length: 9 },
1116 },
1117 ],
1118 }]
1119
1120 ; "default filterset in default-filter"
1121 )]
1122 #[test_case(
1123 indoc! {r#"
1124 [[profile.default.overrides]]
1125 filter = 'test(test1)'
1126 default-filter = "test(test2)"
1127 retries = 2
1128 "#},
1129 "default",
1130 &[MietteJsonReport {
1131 message: "at most one of `filter` and `default-filter` must be specified".to_owned(),
1132 labels: vec![],
1133 }]
1134
1135 ; "both filter and default-filter specified"
1136 )]
1137 #[test_case(
1138 indoc! {r#"
1139 [[profile.default.overrides]]
1140 filter = 'test(test1)'
1141 platform = 'cfg(unix)'
1142 default-filter = "test(test2)"
1143 retries = 2
1144 "#},
1145 "default",
1146 &[MietteJsonReport {
1147 message: "at most one of `filter` and `default-filter` must be specified".to_owned(),
1148 labels: vec![],
1149 }]
1150
1151 ; "both filter and default-filter specified with platform"
1152 )]
1153 #[test_case(
1154 indoc! {r#"
1155 [[profile.default.overrides]]
1156 platform = {}
1157 retries = 2
1158 "#},
1159 "default",
1160 &[MietteJsonReport {
1161 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1162 labels: vec![],
1163 }]
1164
1165 ; "empty platform map"
1166 )]
1167 #[test_case(
1168 indoc! {r#"
1169 [[profile.ci.overrides]]
1170 platform = 'cfg(target_os = "macos)'
1171 retries = 2
1172 "#},
1173 "ci",
1174 &[MietteJsonReport {
1175 message: "error parsing cfg() expression".to_owned(),
1176 labels: vec![
1177 MietteJsonLabel { label: "unclosed quotes".to_owned(), span: MietteJsonSpan { offset: 16, length: 6 } }
1178 ]
1179 }]
1180
1181 ; "invalid platform expression"
1182 )]
1183 #[test_case(
1184 indoc! {r#"
1185 [[profile.ci.overrides]]
1186 filter = 'test(/foo)'
1187 retries = 2
1188 "#},
1189 "ci",
1190 &[MietteJsonReport {
1191 message: "expected close regex".to_owned(),
1192 labels: vec![
1193 MietteJsonLabel { label: "missing `/`".to_owned(), span: MietteJsonSpan { offset: 9, length: 0 } }
1194 ]
1195 }]
1196
1197 ; "invalid filterset"
1198 )]
1199 #[test_case(
1200 indoc! {r#"
1202 [profile.ci]
1203 default-filter = "test(foo) or default()"
1204 "#},
1205 "ci",
1206 &[MietteJsonReport {
1207 message: "predicate not allowed in `default-filter` expressions".to_owned(),
1208 labels: vec![
1209 MietteJsonLabel { label: "this predicate causes infinite recursion".to_owned(), span: MietteJsonSpan { offset: 13, length: 9 } }
1210 ]
1211 }]
1212
1213 ; "default-filter with default"
1214 )]
1215 fn parse_overrides_invalid(
1216 config_contents: &str,
1217 faulty_profile: &str,
1218 expected_reports: &[MietteJsonReport],
1219 ) {
1220 let workspace_dir = tempdir().unwrap();
1221
1222 let graph = temp_workspace(&workspace_dir, config_contents);
1223 let pcx = ParseContext::new(&graph);
1224
1225 let err = NextestConfig::from_sources(
1226 graph.workspace().root(),
1227 &pcx,
1228 None,
1229 [],
1230 &Default::default(),
1231 )
1232 .expect_err("config is invalid");
1233 match err.kind() {
1234 ConfigParseErrorKind::CompileErrors(compile_errors) => {
1235 assert_eq!(
1236 compile_errors.len(),
1237 1,
1238 "exactly one override error must be produced"
1239 );
1240 let error = compile_errors.first().unwrap();
1241 assert_eq!(
1242 error.profile_name, faulty_profile,
1243 "compile error profile matches"
1244 );
1245 let handler = miette::JSONReportHandler::new();
1246 let reports = error
1247 .kind
1248 .reports()
1249 .map(|report| {
1250 let mut out = String::new();
1251 handler.render_report(&mut out, report.as_ref()).unwrap();
1252
1253 let json_report: MietteJsonReport = serde_json::from_str(&out)
1254 .unwrap_or_else(|err| {
1255 panic!(
1256 "failed to deserialize JSON message produced by miette: {err}"
1257 )
1258 });
1259 json_report
1260 })
1261 .collect::<Vec<_>>();
1262 assert_eq!(&reports, expected_reports, "reports match");
1263 }
1264 other => {
1265 panic!(
1266 "for config error {other:?}, expected ConfigParseErrorKind::FiltersetOrCfgParseError"
1267 );
1268 }
1269 };
1270 }
1271
1272 #[test]
1276 fn cfg_unix_with_custom_platform() {
1277 let config_contents = indoc! {r#"
1278 [[profile.default.overrides]]
1279 platform = { host = "cfg(unix)" }
1280 filter = "test(test)"
1281 retries = 5
1282 "#};
1283
1284 let workspace_dir = tempdir().unwrap();
1285
1286 let graph = temp_workspace(&workspace_dir, config_contents);
1287 let package_id = graph.workspace().iter().next().unwrap().id();
1288 let pcx = ParseContext::new(&graph);
1289
1290 let nextest_config = NextestConfig::from_sources(
1291 graph.workspace().root(),
1292 &pcx,
1293 None,
1294 &[][..],
1295 &Default::default(),
1296 )
1297 .expect("config is valid");
1298
1299 let build_platforms = custom_build_platforms(workspace_dir.path());
1300
1301 let profile = nextest_config
1302 .profile("default")
1303 .expect("valid profile name")
1304 .apply_build_platforms(&build_platforms);
1305
1306 let target_binary_query = binary_query(
1308 &graph,
1309 package_id,
1310 "lib",
1311 "my-binary",
1312 BuildPlatform::Target,
1313 );
1314 let query = TestQuery {
1315 binary_query: target_binary_query.to_query(),
1316 test_name: "test",
1317 };
1318 let overrides = profile.settings_for(&query);
1319 assert_eq!(
1320 overrides.retries(),
1321 RetryPolicy::new_without_delay(5),
1322 "retries applied to custom platform"
1323 );
1324 }
1325}