Skip to main content

nextest_runner/config/overrides/
imp.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    config::{
6        core::{
7            EvaluatableProfile, FinalConfig, NextestConfig, NextestConfigImpl, PreBuildPlatform,
8        },
9        elements::{
10            FlakyResult, JunitFlakyFailStatus, LeakTimeout, ReportSkipPolicy, RetryPolicy,
11            SlowTimeout, TestGroup, TestPriority, ThreadsRequired,
12        },
13        scripts::{
14            CompiledProfileScripts, DeserializedProfileScriptConfig, ScriptId, WrapperScriptConfig,
15        },
16    },
17    errors::{
18        ConfigCompileError, ConfigCompileErrorKind, ConfigCompileSection, ConfigParseErrorKind,
19    },
20    platform::BuildPlatforms,
21    reporter::TestOutputDisplay,
22    run_mode::NextestRunMode,
23};
24use guppy::graph::cargo::BuildPlatform;
25use nextest_filtering::{
26    BinaryQuery, CompiledExpr, Filterset, FiltersetKind, KnownGroups, ParseContext, TestQuery,
27};
28use owo_colors::{OwoColorize, Style};
29use serde::{Deserialize, Deserializer};
30use smol_str::SmolStr;
31use std::collections::HashMap;
32use target_spec::{Platform, TargetSpec};
33
34/// Settings for a test binary.
35#[derive(Clone, Debug)]
36pub struct ListSettings<'p, Source = ()> {
37    list_wrapper: Option<(&'p WrapperScriptConfig, Source)>,
38}
39
40impl<'p, Source: Copy> ListSettings<'p, Source> {
41    pub(in crate::config) fn new(
42        profile: &'p EvaluatableProfile<'_>,
43        query: &BinaryQuery<'_>,
44    ) -> Self
45    where
46        Source: TrackSource<'p>,
47    {
48        let ecx = profile.filterset_ecx();
49
50        let mut list_wrapper = None;
51
52        for override_ in &profile.compiled_data.scripts {
53            if let Some(wrapper) = &override_.list_wrapper
54                && list_wrapper.is_none()
55            {
56                let (wrapper, source) =
57                    map_wrapper_script(profile, Source::track_script(wrapper.clone(), override_));
58
59                if !override_
60                    .is_enabled_binary(query, &ecx)
61                    .expect("test() in list-time scripts should have been rejected")
62                {
63                    continue;
64                }
65
66                list_wrapper = Some((wrapper, source));
67            }
68        }
69
70        Self { list_wrapper }
71    }
72}
73
74impl<'p> ListSettings<'p> {
75    /// Returns a default list-settings without a wrapper script.
76    ///
77    /// Debug command used for testing.
78    pub fn debug_empty() -> Self {
79        Self { list_wrapper: None }
80    }
81
82    /// Sets the wrapper to use for list-time scripts.
83    ///
84    /// Debug command used for testing.
85    pub fn debug_set_list_wrapper(&mut self, wrapper: &'p WrapperScriptConfig) -> &mut Self {
86        self.list_wrapper = Some((wrapper, ()));
87        self
88    }
89
90    /// Returns the list-time wrapper script.
91    pub fn list_wrapper(&self) -> Option<&'p WrapperScriptConfig> {
92        self.list_wrapper.as_ref().map(|(wrapper, _)| *wrapper)
93    }
94}
95
96/// Settings for individual tests.
97///
98/// Returned by [`EvaluatableProfile::settings_for`].
99///
100/// The `Source` parameter tracks an optional source; this isn't used by any public APIs at the
101/// moment.
102#[derive(Clone, Debug)]
103pub struct TestSettings<'p, Source = ()> {
104    priority: (TestPriority, Source),
105    threads_required: (ThreadsRequired, Source),
106    run_wrapper: Option<(&'p WrapperScriptConfig, Source)>,
107    run_extra_args: (&'p [String], Source),
108    retries: (RetryPolicy, Source),
109    flaky_result: (FlakyResult, Source),
110    slow_timeout: (SlowTimeout, Source),
111    leak_timeout: (LeakTimeout, Source),
112    test_group: (TestGroup, Source),
113    success_output: (TestOutputDisplay, Source),
114    failure_output: (TestOutputDisplay, Source),
115    junit_store_success_output: (bool, Source),
116    junit_store_failure_output: (bool, Source),
117    junit_report_skipped: (ReportSkipPolicy, Source),
118    junit_flaky_fail_status: (JunitFlakyFailStatus, Source),
119}
120
121pub(crate) trait TrackSource<'p>: Sized {
122    fn track_default<T>(value: T) -> (T, Self);
123    fn track_profile<T>(value: T) -> (T, Self);
124    fn track_override<T>(value: T, source: &'p CompiledOverride<FinalConfig>) -> (T, Self);
125    fn track_script<T>(value: T, source: &'p CompiledProfileScripts<FinalConfig>) -> (T, Self);
126}
127
128impl<'p> TrackSource<'p> for () {
129    fn track_default<T>(value: T) -> (T, Self) {
130        (value, ())
131    }
132
133    fn track_profile<T>(value: T) -> (T, Self) {
134        (value, ())
135    }
136
137    fn track_override<T>(value: T, _source: &'p CompiledOverride<FinalConfig>) -> (T, Self) {
138        (value, ())
139    }
140
141    fn track_script<T>(value: T, _source: &'p CompiledProfileScripts<FinalConfig>) -> (T, Self) {
142        (value, ())
143    }
144}
145
146#[derive(Copy, Clone, Debug)]
147pub(crate) enum SettingSource<'p> {
148    /// A default configuration not specified in, or possible to override from,
149    /// a profile.
150    Default,
151
152    /// A configuration specified in a profile.
153    Profile,
154
155    /// An override specified in a profile.
156    Override(&'p CompiledOverride<FinalConfig>),
157
158    /// An override specified in the `scripts` section.
159    #[expect(dead_code)]
160    Script(&'p CompiledProfileScripts<FinalConfig>),
161}
162
163impl<'p> TrackSource<'p> for SettingSource<'p> {
164    fn track_default<T>(value: T) -> (T, Self) {
165        (value, SettingSource::Default)
166    }
167
168    fn track_profile<T>(value: T) -> (T, Self) {
169        (value, SettingSource::Profile)
170    }
171
172    fn track_override<T>(value: T, source: &'p CompiledOverride<FinalConfig>) -> (T, Self) {
173        (value, SettingSource::Override(source))
174    }
175
176    fn track_script<T>(value: T, source: &'p CompiledProfileScripts<FinalConfig>) -> (T, Self) {
177        (value, SettingSource::Script(source))
178    }
179}
180
181impl<'p> TestSettings<'p> {
182    /// Returns the test's priority.
183    pub fn priority(&self) -> TestPriority {
184        self.priority.0
185    }
186
187    /// Returns the number of threads required for this test.
188    pub fn threads_required(&self) -> ThreadsRequired {
189        self.threads_required.0
190    }
191
192    /// Returns the run-time wrapper script for this test.
193    pub fn run_wrapper(&self) -> Option<&'p WrapperScriptConfig> {
194        self.run_wrapper.map(|(script, _)| script)
195    }
196
197    /// Returns extra arguments to pass at runtime for this test.
198    pub fn run_extra_args(&self) -> &'p [String] {
199        self.run_extra_args.0
200    }
201
202    /// Returns the number of retries for this test.
203    pub fn retries(&self) -> RetryPolicy {
204        self.retries.0
205    }
206
207    /// Returns the flaky result behavior for this test.
208    pub fn flaky_result(&self) -> FlakyResult {
209        self.flaky_result.0
210    }
211
212    /// Returns the slow timeout for this test.
213    pub fn slow_timeout(&self) -> SlowTimeout {
214        self.slow_timeout.0
215    }
216
217    /// Returns the leak timeout for this test.
218    pub fn leak_timeout(&self) -> LeakTimeout {
219        self.leak_timeout.0
220    }
221
222    /// Returns the test group for this test.
223    pub fn test_group(&self) -> &TestGroup {
224        &self.test_group.0
225    }
226
227    /// Returns the success output setting for this test.
228    pub fn success_output(&self) -> TestOutputDisplay {
229        self.success_output.0
230    }
231
232    /// Returns the failure output setting for this test.
233    pub fn failure_output(&self) -> TestOutputDisplay {
234        self.failure_output.0
235    }
236
237    /// Returns whether success output should be stored in JUnit.
238    pub fn junit_store_success_output(&self) -> bool {
239        self.junit_store_success_output.0
240    }
241
242    /// Returns whether failure output should be stored in JUnit.
243    pub fn junit_store_failure_output(&self) -> bool {
244        self.junit_store_failure_output.0
245    }
246
247    /// Returns which skipped tests should be reported in JUnit.
248    pub fn junit_report_skipped(&self) -> ReportSkipPolicy {
249        self.junit_report_skipped.0
250    }
251
252    /// Returns the JUnit flaky-fail status for this test.
253    pub fn junit_flaky_fail_status(&self) -> JunitFlakyFailStatus {
254        self.junit_flaky_fail_status.0
255    }
256}
257
258#[expect(dead_code)]
259impl<'p, Source: Copy> TestSettings<'p, Source> {
260    pub(in crate::config) fn new(
261        profile: &'p EvaluatableProfile<'_>,
262        run_mode: NextestRunMode,
263        query: &TestQuery<'_>,
264    ) -> Self
265    where
266        Source: TrackSource<'p>,
267    {
268        let ecx = profile.filterset_ecx();
269
270        let mut priority = None;
271        let mut threads_required = None;
272        let mut run_wrapper = None;
273        let mut run_extra_args = None;
274        let mut retries = None;
275        let mut flaky_result = None;
276        let mut slow_timeout = None;
277        let mut leak_timeout = None;
278        let mut test_group = None;
279        let mut success_output = None;
280        let mut failure_output = None;
281        let mut junit_store_success_output = None;
282        let mut junit_store_failure_output = None;
283        let mut junit_report_skipped = None;
284        let mut junit_flaky_fail_status = None;
285
286        for override_ in &profile.compiled_data.overrides {
287            if !override_.matches_test_query(query, &ecx) {
288                continue;
289            }
290
291            if priority.is_none()
292                && let Some(p) = override_.data.priority
293            {
294                priority = Some(Source::track_override(p, override_));
295            }
296            if threads_required.is_none()
297                && let Some(t) = override_.data.threads_required
298            {
299                threads_required = Some(Source::track_override(t, override_));
300            }
301            if run_extra_args.is_none()
302                && let Some(r) = override_.data.run_extra_args.as_deref()
303            {
304                run_extra_args = Some(Source::track_override(r, override_));
305            }
306            if retries.is_none()
307                && let Some(r) = override_.data.retries
308            {
309                retries = Some(Source::track_override(r, override_));
310            }
311            if flaky_result.is_none()
312                && let Some(fr) = override_.data.flaky_result
313            {
314                flaky_result = Some(Source::track_override(fr, override_));
315            }
316            if slow_timeout.is_none() {
317                // Use the appropriate slow timeout based on run mode. Note that
318                // there's no fallback from bench to test timeout.
319                let timeout_for_mode = match run_mode {
320                    NextestRunMode::Test => override_.data.slow_timeout,
321                    NextestRunMode::Benchmark => override_.data.bench_slow_timeout,
322                };
323                if let Some(s) = timeout_for_mode {
324                    slow_timeout = Some(Source::track_override(s, override_));
325                }
326            }
327            if leak_timeout.is_none()
328                && let Some(l) = override_.data.leak_timeout
329            {
330                leak_timeout = Some(Source::track_override(l, override_));
331            }
332            if test_group.is_none()
333                && let Some(t) = &override_.data.test_group
334            {
335                test_group = Some(Source::track_override(t.clone(), override_));
336            }
337            if success_output.is_none()
338                && let Some(s) = override_.data.success_output
339            {
340                success_output = Some(Source::track_override(s, override_));
341            }
342            if failure_output.is_none()
343                && let Some(f) = override_.data.failure_output
344            {
345                failure_output = Some(Source::track_override(f, override_));
346            }
347            if junit_store_success_output.is_none()
348                && let Some(s) = override_.data.junit.store_success_output
349            {
350                junit_store_success_output = Some(Source::track_override(s, override_));
351            }
352            if junit_store_failure_output.is_none()
353                && let Some(f) = override_.data.junit.store_failure_output
354            {
355                junit_store_failure_output = Some(Source::track_override(f, override_));
356            }
357            if junit_report_skipped.is_none()
358                && let Some(s) = override_.data.junit.report_skipped
359            {
360                junit_report_skipped = Some(Source::track_override(s, override_));
361            }
362            if junit_flaky_fail_status.is_none()
363                && let Some(s) = override_.data.junit.flaky_fail_status
364            {
365                junit_flaky_fail_status = Some(Source::track_override(s, override_));
366            }
367        }
368
369        for override_ in &profile.compiled_data.scripts {
370            if !override_.is_enabled(query, &ecx) {
371                continue;
372            }
373
374            if run_wrapper.is_none()
375                && let Some(wrapper) = &override_.run_wrapper
376            {
377                run_wrapper = Some(Source::track_script(wrapper.clone(), override_));
378            }
379        }
380
381        // If no overrides were found, use the profile defaults.
382        let priority = priority.unwrap_or_else(|| Source::track_default(TestPriority::default()));
383        let threads_required =
384            threads_required.unwrap_or_else(|| Source::track_profile(profile.threads_required()));
385        let run_wrapper = run_wrapper.map(|wrapper| map_wrapper_script(profile, wrapper));
386        let run_extra_args =
387            run_extra_args.unwrap_or_else(|| Source::track_profile(profile.run_extra_args()));
388        let retries = retries.unwrap_or_else(|| Source::track_profile(profile.retries()));
389        let flaky_result =
390            flaky_result.unwrap_or_else(|| Source::track_profile(profile.flaky_result()));
391        let slow_timeout =
392            slow_timeout.unwrap_or_else(|| Source::track_profile(profile.slow_timeout(run_mode)));
393        let leak_timeout =
394            leak_timeout.unwrap_or_else(|| Source::track_profile(profile.leak_timeout()));
395        let test_group = test_group.unwrap_or_else(|| Source::track_profile(TestGroup::Global));
396        let success_output =
397            success_output.unwrap_or_else(|| Source::track_profile(profile.success_output()));
398        let failure_output =
399            failure_output.unwrap_or_else(|| Source::track_profile(profile.failure_output()));
400        let junit_store_success_output = junit_store_success_output.unwrap_or_else(|| {
401            // If the profile doesn't have JUnit enabled, success output can just be false.
402            Source::track_profile(profile.junit().is_some_and(|j| j.store_success_output()))
403        });
404        let junit_store_failure_output = junit_store_failure_output.unwrap_or_else(|| {
405            // If the profile doesn't have JUnit enabled, failure output can just be false.
406            Source::track_profile(profile.junit().is_some_and(|j| j.store_failure_output()))
407        });
408        let junit_report_skipped = junit_report_skipped.unwrap_or_else(|| {
409            Source::track_profile(
410                profile
411                    .junit()
412                    .map_or(ReportSkipPolicy::default(), |j| j.report_skipped()),
413            )
414        });
415        let junit_flaky_fail_status = junit_flaky_fail_status.unwrap_or_else(|| {
416            Source::track_profile(
417                profile
418                    .junit()
419                    .map_or(JunitFlakyFailStatus::default(), |j| j.flaky_fail_status()),
420            )
421        });
422
423        TestSettings {
424            threads_required,
425            run_extra_args,
426            run_wrapper,
427            retries,
428            flaky_result,
429            priority,
430            slow_timeout,
431            leak_timeout,
432            test_group,
433            success_output,
434            failure_output,
435            junit_store_success_output,
436            junit_store_failure_output,
437            junit_report_skipped,
438            junit_flaky_fail_status,
439        }
440    }
441
442    /// Returns the number of threads required for this test, with the source attached.
443    pub(crate) fn threads_required_with_source(&self) -> (ThreadsRequired, Source) {
444        self.threads_required
445    }
446
447    /// Returns the number of retries for this test, with the source attached.
448    pub(crate) fn retries_with_source(&self) -> (RetryPolicy, Source) {
449        self.retries
450    }
451
452    /// Returns the slow timeout for this test, with the source attached.
453    pub(crate) fn slow_timeout_with_source(&self) -> (SlowTimeout, Source) {
454        self.slow_timeout
455    }
456
457    /// Returns the leak timeout for this test, with the source attached.
458    pub(crate) fn leak_timeout_with_source(&self) -> (LeakTimeout, Source) {
459        self.leak_timeout
460    }
461
462    /// Returns the test group for this test, with the source attached.
463    pub(crate) fn test_group_with_source(&self) -> &(TestGroup, Source) {
464        &self.test_group
465    }
466}
467
468fn map_wrapper_script<'p, Source>(
469    profile: &'p EvaluatableProfile<'_>,
470    (script, source): (ScriptId, Source),
471) -> (&'p WrapperScriptConfig, Source)
472where
473    Source: TrackSource<'p>,
474{
475    let wrapper_config = profile
476        .script_config()
477        .wrapper
478        .get(&script)
479        .unwrap_or_else(|| {
480            panic!(
481                "wrapper script {script} not found \
482                 (should have been checked while reading config)"
483            )
484        });
485    (wrapper_config, source)
486}
487
488#[derive(Clone, Debug)]
489pub(in crate::config) struct CompiledByProfile {
490    pub(in crate::config) default: CompiledData<PreBuildPlatform>,
491    pub(in crate::config) other: HashMap<String, CompiledData<PreBuildPlatform>>,
492}
493
494impl CompiledByProfile {
495    pub(in crate::config) fn new(
496        pcx: &ParseContext<'_>,
497        config: &NextestConfigImpl,
498    ) -> Result<Self, ConfigParseErrorKind> {
499        let mut errors = vec![];
500        let default = CompiledData::new(
501            pcx,
502            "default",
503            Some(config.default_profile().default_filter()),
504            config.default_profile().overrides(),
505            config.default_profile().setup_scripts(),
506            &mut errors,
507        );
508        let other: HashMap<_, _> = config
509            .other_profiles()
510            .map(|(profile_name, profile)| {
511                (
512                    profile_name.to_owned(),
513                    CompiledData::new(
514                        pcx,
515                        profile_name,
516                        profile.default_filter(),
517                        profile.overrides(),
518                        profile.scripts(),
519                        &mut errors,
520                    ),
521                )
522            })
523            .collect();
524
525        if errors.is_empty() {
526            Ok(Self { default, other })
527        } else {
528            Err(ConfigParseErrorKind::CompileErrors(errors))
529        }
530    }
531
532    /// Returns the compiled data for the default config.
533    ///
534    /// The default config does not depend on the package graph, so we create it separately here.
535    /// But we don't implement `Default` to make sure that the value is for the default _config_,
536    /// not the default _profile_ (which repo config can customize).
537    pub(in crate::config) fn for_default_config() -> Self {
538        Self {
539            default: CompiledData {
540                profile_default_filter: Some(CompiledDefaultFilter::for_default_config()),
541                overrides: vec![],
542                scripts: vec![],
543            },
544            other: HashMap::new(),
545        }
546    }
547}
548
549/// A compiled form of the default filter for a profile.
550///
551/// Returned by [`EvaluatableProfile::default_filter`].
552#[derive(Clone, Debug)]
553pub struct CompiledDefaultFilter {
554    /// The compiled expression.
555    ///
556    /// This is a bit tricky -- in some cases, the default config is constructed without a
557    /// `PackageGraph` being available. But parsing filtersets requires a `PackageGraph`. So we hack
558    /// around it by only storing the compiled expression here, and by setting it to `all()` (which
559    /// matches the config).
560    ///
561    /// This does make the default-filter defined in default-config.toml a bit
562    /// of a lie (since we don't use it directly, but instead replicate it in
563    /// code). But it's not too bad.
564    pub expr: CompiledExpr,
565
566    /// The profile name the default filter originates from.
567    pub profile: String,
568
569    /// The section of the config that the default filter comes from.
570    pub section: CompiledDefaultFilterSection,
571}
572
573impl CompiledDefaultFilter {
574    pub(crate) fn for_default_config() -> Self {
575        Self {
576            expr: CompiledExpr::ALL,
577            profile: NextestConfig::DEFAULT_PROFILE.to_owned(),
578            section: CompiledDefaultFilterSection::Profile,
579        }
580    }
581
582    /// Displays a configuration string for the default filter.
583    pub fn display_config(&self, bold_style: Style) -> String {
584        match &self.section {
585            CompiledDefaultFilterSection::Profile => {
586                format!("profile.{}.default-filter", self.profile)
587                    .style(bold_style)
588                    .to_string()
589            }
590            CompiledDefaultFilterSection::Override(_) => {
591                format!(
592                    "default-filter in {}",
593                    format!("profile.{}.overrides", self.profile).style(bold_style)
594                )
595            }
596        }
597    }
598}
599
600/// Within [`CompiledDefaultFilter`], the part of the config that the default
601/// filter comes from.
602#[derive(Clone, Copy, Debug)]
603pub enum CompiledDefaultFilterSection {
604    /// The config comes from the top-level `profile.<profile-name>.default-filter`.
605    Profile,
606
607    /// The config comes from the override at the given index.
608    Override(usize),
609}
610
611#[derive(Clone, Debug)]
612pub(in crate::config) struct CompiledData<State> {
613    // The default filter specified at the profile level.
614    //
615    // Overrides might also specify their own filters, and in that case the
616    // overrides take priority.
617    pub(in crate::config) profile_default_filter: Option<CompiledDefaultFilter>,
618    pub(in crate::config) overrides: Vec<CompiledOverride<State>>,
619    pub(in crate::config) scripts: Vec<CompiledProfileScripts<State>>,
620}
621
622impl CompiledData<PreBuildPlatform> {
623    fn new(
624        pcx: &ParseContext<'_>,
625        profile_name: &str,
626        profile_default_filter: Option<&str>,
627        overrides: &[DeserializedOverride],
628        scripts: &[DeserializedProfileScriptConfig],
629        errors: &mut Vec<ConfigCompileError>,
630    ) -> Self {
631        let profile_default_filter =
632            profile_default_filter.and_then(|filter| {
633                match Filterset::parse(
634                    filter.to_owned(),
635                    pcx,
636                    FiltersetKind::DefaultFilter,
637                    &KnownGroups::Unavailable,
638                ) {
639                    Ok(expr) => Some(CompiledDefaultFilter {
640                        expr: expr.compiled,
641                        profile: profile_name.to_owned(),
642                        section: CompiledDefaultFilterSection::Profile,
643                    }),
644                    Err(err) => {
645                        errors.push(ConfigCompileError {
646                            profile_name: profile_name.to_owned(),
647                            section: ConfigCompileSection::DefaultFilter,
648                            kind: ConfigCompileErrorKind::Parse {
649                                host_parse_error: None,
650                                target_parse_error: None,
651                                filter_parse_errors: vec![err],
652                            },
653                        });
654                        None
655                    }
656                }
657            });
658
659        let overrides = overrides
660            .iter()
661            .enumerate()
662            .filter_map(|(index, source)| {
663                CompiledOverride::new(pcx, profile_name, index, source, errors)
664            })
665            .collect();
666        let scripts = scripts
667            .iter()
668            .enumerate()
669            .filter_map(|(index, source)| {
670                CompiledProfileScripts::new(pcx, profile_name, index, source, errors)
671            })
672            .collect();
673        Self {
674            profile_default_filter,
675            overrides,
676            scripts,
677        }
678    }
679
680    pub(in crate::config) fn extend_reverse(&mut self, other: Self) {
681        // For the default filter, other wins (it is last, and after reversing, it will be first).
682        if other.profile_default_filter.is_some() {
683            self.profile_default_filter = other.profile_default_filter;
684        }
685        self.overrides.extend(other.overrides.into_iter().rev());
686        self.scripts.extend(other.scripts.into_iter().rev());
687    }
688
689    pub(in crate::config) fn reverse(&mut self) {
690        self.overrides.reverse();
691        self.scripts.reverse();
692    }
693
694    /// Chains this data with another set of data, treating `other` as lower-priority than `self`.
695    pub(in crate::config) fn chain(self, other: Self) -> Self {
696        let profile_default_filter = self.profile_default_filter.or(other.profile_default_filter);
697        let mut overrides = self.overrides;
698        let mut scripts = self.scripts;
699        overrides.extend(other.overrides);
700        scripts.extend(other.scripts);
701        Self {
702            profile_default_filter,
703            overrides,
704            scripts,
705        }
706    }
707
708    pub(in crate::config) fn apply_build_platforms(
709        self,
710        build_platforms: &BuildPlatforms,
711    ) -> CompiledData<FinalConfig> {
712        let profile_default_filter = self.profile_default_filter;
713        let overrides = self
714            .overrides
715            .into_iter()
716            .map(|override_| override_.apply_build_platforms(build_platforms))
717            .collect();
718        let setup_scripts = self
719            .scripts
720            .into_iter()
721            .map(|setup_script| setup_script.apply_build_platforms(build_platforms))
722            .collect();
723        CompiledData {
724            profile_default_filter,
725            overrides,
726            scripts: setup_scripts,
727        }
728    }
729}
730
731#[derive(Clone, Debug)]
732pub(crate) struct CompiledOverride<State> {
733    id: OverrideId,
734    state: State,
735    pub(in crate::config) data: ProfileOverrideData,
736}
737
738impl<State> CompiledOverride<State> {
739    pub(crate) fn id(&self) -> &OverrideId {
740        &self.id
741    }
742}
743
744#[derive(Clone, Debug, Eq, Hash, PartialEq)]
745pub(crate) struct OverrideId {
746    pub(crate) profile_name: SmolStr,
747    index: usize,
748}
749
750#[derive(Clone, Debug)]
751pub(in crate::config) struct ProfileOverrideData {
752    host_spec: MaybeTargetSpec,
753    target_spec: MaybeTargetSpec,
754    filter: Option<FilterOrDefaultFilter>,
755    priority: Option<TestPriority>,
756    threads_required: Option<ThreadsRequired>,
757    run_extra_args: Option<Vec<String>>,
758    retries: Option<RetryPolicy>,
759    flaky_result: Option<FlakyResult>,
760    slow_timeout: Option<SlowTimeout>,
761    bench_slow_timeout: Option<SlowTimeout>,
762    leak_timeout: Option<LeakTimeout>,
763    pub(in crate::config) test_group: Option<TestGroup>,
764    success_output: Option<TestOutputDisplay>,
765    failure_output: Option<TestOutputDisplay>,
766    junit: DeserializedJunitOutput,
767}
768
769impl CompiledOverride<PreBuildPlatform> {
770    fn new(
771        pcx: &ParseContext<'_>,
772        profile_name: &str,
773        index: usize,
774        source: &DeserializedOverride,
775        errors: &mut Vec<ConfigCompileError>,
776    ) -> Option<Self> {
777        if source.platform.host.is_none()
778            && source.platform.target.is_none()
779            && source.filter.is_none()
780        {
781            errors.push(ConfigCompileError {
782                profile_name: profile_name.to_owned(),
783                section: ConfigCompileSection::Override(index),
784                kind: ConfigCompileErrorKind::ConstraintsNotSpecified {
785                    default_filter_specified: source.default_filter.is_some(),
786                },
787            });
788            return None;
789        }
790
791        let host_spec = MaybeTargetSpec::new(source.platform.host.as_deref());
792        let target_spec = MaybeTargetSpec::new(source.platform.target.as_deref());
793        let filter = source.filter.as_ref().map_or(Ok(None), |filter| {
794            Some(Filterset::parse(
795                filter.clone(),
796                pcx,
797                FiltersetKind::OverrideFilter,
798                &KnownGroups::Unavailable,
799            ))
800            .transpose()
801        });
802        let default_filter = source.default_filter.as_ref().map_or(Ok(None), |filter| {
803            Some(Filterset::parse(
804                filter.clone(),
805                pcx,
806                FiltersetKind::DefaultFilter,
807                &KnownGroups::Unavailable,
808            ))
809            .transpose()
810        });
811
812        match (host_spec, target_spec, filter, default_filter) {
813            (Ok(host_spec), Ok(target_spec), Ok(filter), Ok(default_filter)) => {
814                // At most one of filter and default-filter can be specified.
815                let filter = match (filter, default_filter) {
816                    (Some(_), Some(_)) => {
817                        errors.push(ConfigCompileError {
818                            profile_name: profile_name.to_owned(),
819                            section: ConfigCompileSection::Override(index),
820                            kind: ConfigCompileErrorKind::FilterAndDefaultFilterSpecified,
821                        });
822                        return None;
823                    }
824                    (Some(filter), None) => Some(FilterOrDefaultFilter::Filter(filter)),
825                    (None, Some(default_filter)) => {
826                        let compiled = CompiledDefaultFilter {
827                            expr: default_filter.compiled,
828                            profile: profile_name.to_owned(),
829                            section: CompiledDefaultFilterSection::Override(index),
830                        };
831                        Some(FilterOrDefaultFilter::DefaultFilter(compiled))
832                    }
833                    (None, None) => None,
834                };
835
836                Some(Self {
837                    id: OverrideId {
838                        profile_name: profile_name.into(),
839                        index,
840                    },
841                    state: PreBuildPlatform {},
842                    data: ProfileOverrideData {
843                        host_spec,
844                        target_spec,
845                        filter,
846                        priority: source.priority,
847                        threads_required: source.threads_required,
848                        run_extra_args: source.run_extra_args.clone(),
849                        retries: source.retries,
850                        flaky_result: source.flaky_result,
851                        slow_timeout: source.slow_timeout,
852                        bench_slow_timeout: source.bench.slow_timeout,
853                        leak_timeout: source.leak_timeout,
854                        test_group: source.test_group.clone(),
855                        success_output: source.success_output,
856                        failure_output: source.failure_output,
857                        junit: source.junit,
858                    },
859                })
860            }
861            (maybe_host_err, maybe_target_err, maybe_filter_err, maybe_default_filter_err) => {
862                let host_parse_error = maybe_host_err.err();
863                let target_parse_error = maybe_target_err.err();
864                let filter_parse_errors = maybe_filter_err
865                    .err()
866                    .into_iter()
867                    .chain(maybe_default_filter_err.err())
868                    .collect();
869
870                errors.push(ConfigCompileError {
871                    profile_name: profile_name.to_owned(),
872                    section: ConfigCompileSection::Override(index),
873                    kind: ConfigCompileErrorKind::Parse {
874                        host_parse_error,
875                        target_parse_error,
876                        filter_parse_errors,
877                    },
878                });
879                None
880            }
881        }
882    }
883
884    pub(in crate::config) fn apply_build_platforms(
885        self,
886        build_platforms: &BuildPlatforms,
887    ) -> CompiledOverride<FinalConfig> {
888        let host_eval = self.data.host_spec.eval(&build_platforms.host.platform);
889        let host_test_eval = self.data.target_spec.eval(&build_platforms.host.platform);
890        let target_eval = build_platforms
891            .target
892            .as_ref()
893            .map_or(host_test_eval, |target| {
894                self.data.target_spec.eval(&target.triple.platform)
895            });
896
897        CompiledOverride {
898            id: self.id,
899            state: FinalConfig {
900                host_eval,
901                host_test_eval,
902                target_eval,
903            },
904            data: self.data,
905        }
906    }
907}
908
909impl CompiledOverride<FinalConfig> {
910    /// Returns the target spec.
911    pub(crate) fn target_spec(&self) -> &MaybeTargetSpec {
912        &self.data.target_spec
913    }
914
915    /// Returns the filter to apply to overrides, if any.
916    pub(crate) fn filter(&self) -> Option<&Filterset> {
917        match self.data.filter.as_ref() {
918            Some(FilterOrDefaultFilter::Filter(filter)) => Some(filter),
919            _ => None,
920        }
921    }
922
923    /// Returns true if this override's platform and filter constraints
924    /// match the given test query.
925    pub(in crate::config) fn matches_test_query(
926        &self,
927        query: &TestQuery<'_>,
928        ecx: &nextest_filtering::EvalContext<'_>,
929    ) -> bool {
930        if !self.state.host_eval {
931            return false;
932        }
933        if query.binary_query.platform == BuildPlatform::Host && !self.state.host_test_eval {
934            return false;
935        }
936        if query.binary_query.platform == BuildPlatform::Target && !self.state.target_eval {
937            return false;
938        }
939        // If no expression is present, it's equivalent to "all()".
940        if let Some(expr) = self.filter()
941            && !expr.matches_test(query, ecx)
942        {
943            return false;
944        }
945        true
946    }
947
948    /// Returns the default filter if it matches the platform.
949    pub(crate) fn default_filter_if_matches_platform(&self) -> Option<&CompiledDefaultFilter> {
950        match self.data.filter.as_ref() {
951            Some(FilterOrDefaultFilter::DefaultFilter(filter)) => {
952                // Which kind of evaluation to assume: matching the *target*
953                // filter against the *target* platform (host_eval +
954                // target_eval), or matching the *target* filter against the
955                // *host* platform (host_eval + host_test_eval)? The former
956                // makes much more sense, since in a cross-compile scenario you
957                // want to match a (host, target) pair.
958                (self.state.host_eval && self.state.target_eval).then_some(filter)
959            }
960            _ => None,
961        }
962    }
963}
964
965/// Represents a [`TargetSpec`] that might have been provided.
966#[derive(Clone, Debug, Default)]
967pub(crate) enum MaybeTargetSpec {
968    Provided(TargetSpec),
969    #[default]
970    Any,
971}
972
973impl MaybeTargetSpec {
974    pub(in crate::config) fn new(platform_str: Option<&str>) -> Result<Self, target_spec::Error> {
975        Ok(match platform_str {
976            Some(platform_str) => {
977                MaybeTargetSpec::Provided(TargetSpec::new(platform_str.to_owned())?)
978            }
979            None => MaybeTargetSpec::Any,
980        })
981    }
982
983    pub(in crate::config) fn eval(&self, platform: &Platform) -> bool {
984        match self {
985            MaybeTargetSpec::Provided(spec) => spec
986                .eval(platform)
987                .unwrap_or(/* unknown results are mapped to true */ true),
988            MaybeTargetSpec::Any => true,
989        }
990    }
991}
992
993/// Either a filter override or a default filter specified for a platform.
994///
995/// At most one of these can be specified.
996#[derive(Clone, Debug)]
997pub(crate) enum FilterOrDefaultFilter {
998    Filter(Filterset),
999    DefaultFilter(CompiledDefaultFilter),
1000}
1001
1002/// Deserialized form of profile overrides before compilation.
1003#[derive(Clone, Debug, Deserialize)]
1004#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1005#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1006#[serde(rename_all = "kebab-case")]
1007pub(in crate::config) struct DeserializedOverride {
1008    /// Host and/or target platforms this override applies to.
1009    #[serde(default)]
1010    platform: PlatformStrings,
1011    /// Filterset expression selecting tests this override applies to.
1012    #[serde(default)]
1013    filter: Option<String>,
1014    // Overrides start here.
1015    //
1016    // (This used to use serde(flatten) but that has issues:
1017    // https://github.com/serde-rs/serde/issues/2312.)
1018    // ---
1019    /// Priority for matching tests; higher values run sooner.
1020    #[serde(default)]
1021    priority: Option<TestPriority>,
1022    /// Replaces `default-filter` for matching platforms. Requires `platform`
1023    /// and must not be combined with `filter`.
1024    #[serde(default)]
1025    default_filter: Option<String>,
1026    /// Number of threads each matching test reserves from the pool.
1027    #[serde(default)]
1028    threads_required: Option<ThreadsRequired>,
1029    /// Extra arguments to pass to matching test binaries.
1030    #[serde(default)]
1031    run_extra_args: Option<Vec<String>>,
1032    /// Retry policy for matching tests.
1033    #[serde(
1034        default,
1035        deserialize_with = "crate::config::elements::deserialize_retry_policy"
1036    )]
1037    retries: Option<RetryPolicy>,
1038    /// Whether to treat matching flaky tests as passing or failing.
1039    #[serde(default)]
1040    flaky_result: Option<FlakyResult>,
1041    /// Slow timeout for matching tests.
1042    #[serde(
1043        default,
1044        deserialize_with = "crate::config::elements::deserialize_slow_timeout"
1045    )]
1046    slow_timeout: Option<SlowTimeout>,
1047    /// Leak timeout for matching tests.
1048    #[serde(
1049        default,
1050        deserialize_with = "crate::config::elements::deserialize_leak_timeout"
1051    )]
1052    leak_timeout: Option<LeakTimeout>,
1053    /// Test group to put matching tests in.
1054    #[serde(default)]
1055    test_group: Option<TestGroup>,
1056    /// When to display output for matching successful tests.
1057    #[serde(default)]
1058    success_output: Option<TestOutputDisplay>,
1059    /// When to display output for matching failed tests.
1060    #[serde(default)]
1061    failure_output: Option<TestOutputDisplay>,
1062    /// JUnit XML output settings for matching tests.
1063    #[serde(default)]
1064    junit: DeserializedJunitOutput,
1065    /// Benchmark-specific overrides for matching tests.
1066    #[serde(default)]
1067    bench: DeserializedOverrideBench,
1068}
1069
1070#[derive(Copy, Clone, Debug, Default, Deserialize)]
1071#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1072#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1073#[serde(rename_all = "kebab-case")]
1074pub(in crate::config) struct DeserializedJunitOutput {
1075    /// Whether to store successful output for matching tests in the JUnit XML
1076    /// report.
1077    store_success_output: Option<bool>,
1078    /// Whether to store failed output for matching tests in the JUnit XML
1079    /// report.
1080    store_failure_output: Option<bool>,
1081    /// Which skipped tests to emit for matching tests in the JUnit XML report.
1082    report_skipped: Option<ReportSkipPolicy>,
1083    /// How flaky-fail tests are reported in the JUnit XML report.
1084    flaky_fail_status: Option<JunitFlakyFailStatus>,
1085}
1086
1087/// Deserialized form of benchmark-specific overrides.
1088#[derive(Clone, Debug, Default, Deserialize)]
1089#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1090#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1091#[serde(rename_all = "kebab-case")]
1092pub(in crate::config) struct DeserializedOverrideBench {
1093    /// Slow timeout for matching benchmarks.
1094    #[serde(
1095        default,
1096        deserialize_with = "crate::config::elements::deserialize_slow_timeout"
1097    )]
1098    slow_timeout: Option<SlowTimeout>,
1099}
1100
1101#[derive(Clone, Debug, Default)]
1102pub(in crate::config) struct PlatformStrings {
1103    pub(in crate::config) host: Option<String>,
1104    pub(in crate::config) target: Option<String>,
1105}
1106
1107#[cfg(feature = "config-schema")]
1108impl schemars::JsonSchema for PlatformStrings {
1109    fn schema_name() -> std::borrow::Cow<'static, str> {
1110        "PlatformStrings".into()
1111    }
1112
1113    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1114        schemars::json_schema!({
1115            "oneOf": [
1116                generator.subschema_for::<String>(),
1117                {
1118                    "type": "object",
1119                    "properties": {
1120                        "host": {
1121                            "type": ["string", "null"],
1122                        },
1123                        "target": {
1124                            "type": ["string", "null"],
1125                        },
1126                    },
1127                    "additionalProperties": false,
1128                }
1129            ]
1130        })
1131    }
1132}
1133
1134impl<'de> Deserialize<'de> for PlatformStrings {
1135    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1136        struct V;
1137
1138        impl<'de2> serde::de::Visitor<'de2> for V {
1139            type Value = PlatformStrings;
1140
1141            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1142                formatter.write_str(
1143                    "a table ({ host = \"x86_64-apple-darwin\", \
1144                        target = \"cfg(windows)\" }) \
1145                        or a string (\"x86_64-unknown-gnu-linux\")",
1146                )
1147            }
1148
1149            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1150            where
1151                E: serde::de::Error,
1152            {
1153                Ok(PlatformStrings {
1154                    host: None,
1155                    target: Some(v.to_owned()),
1156                })
1157            }
1158
1159            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
1160            where
1161                A: serde::de::MapAccess<'de2>,
1162            {
1163                #[derive(Deserialize)]
1164                struct PlatformStringsInner {
1165                    #[serde(default)]
1166                    host: Option<String>,
1167                    #[serde(default)]
1168                    target: Option<String>,
1169                }
1170
1171                let inner = PlatformStringsInner::deserialize(
1172                    serde::de::value::MapAccessDeserializer::new(map),
1173                )?;
1174                Ok(PlatformStrings {
1175                    host: inner.host,
1176                    target: inner.target,
1177                })
1178            }
1179        }
1180
1181        deserializer.deserialize_any(V)
1182    }
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187    use super::*;
1188    use crate::config::{
1189        core::NextestConfig,
1190        elements::{LeakTimeoutResult, SlowTimeoutResult},
1191        utils::test_helpers::*,
1192    };
1193    use camino_tempfile::tempdir;
1194    use indoc::indoc;
1195    use nextest_metadata::TestCaseName;
1196    use std::{num::NonZeroUsize, time::Duration};
1197    use test_case::test_case;
1198
1199    /// Basic test to ensure overrides work. Add new override parameters to this test.
1200    #[test]
1201    fn test_overrides_basic() {
1202        let config_contents = indoc! {r#"
1203            # Override 1
1204            [[profile.default.overrides]]
1205            platform = 'aarch64-apple-darwin'  # this is the target platform
1206            filter = "test(test)"
1207            retries = { backoff = "exponential", count = 20, delay = "1s", max-delay = "20s" }
1208            slow-timeout = { period = "120s", terminate-after = 1, grace-period = "0s" }
1209            success-output = "immediate-final"
1210            junit = { store-success-output = true }
1211
1212            # Override 2
1213            [[profile.default.overrides]]
1214            filter = "test(test)"
1215            threads-required = 8
1216            retries = 3
1217            slow-timeout = "60s"
1218            leak-timeout = "300ms"
1219            test-group = "my-group"
1220            failure-output = "final"
1221            junit = { store-failure-output = false, report-skipped = "none" }
1222
1223            # Override 3
1224            [[profile.default.overrides]]
1225            platform = { host = "cfg(unix)" }
1226            filter = "test(override3)"
1227            retries = 5
1228
1229            # Override 4 -- host not matched
1230            [[profile.default.overrides]]
1231            platform = { host = 'aarch64-apple-darwin' }
1232            retries = 10
1233
1234            # Override 5 -- no filter provided, just platform
1235            [[profile.default.overrides]]
1236            platform = { host = 'cfg(target_os = "linux")', target = 'aarch64-apple-darwin' }
1237            filter = "test(override5)"
1238            retries = 8
1239
1240            # Override 6 -- timeout result success
1241            [[profile.default.overrides]]
1242            filter = "test(timeout_success)"
1243            slow-timeout = { period = "30s", on-timeout = "pass" }
1244
1245            [profile.default.junit]
1246            path = "my-path.xml"
1247            report-skipped = "all"
1248
1249            [test-groups.my-group]
1250            max-threads = 20
1251        "#};
1252
1253        let workspace_dir = tempdir().unwrap();
1254
1255        let graph = temp_workspace(&workspace_dir, config_contents);
1256        let package_id = graph.workspace().iter().next().unwrap().id();
1257
1258        let pcx = ParseContext::new(&graph);
1259
1260        let nextest_config_result = NextestConfig::from_sources(
1261            graph.workspace().root(),
1262            &pcx,
1263            None,
1264            &[][..],
1265            &Default::default(),
1266        )
1267        .expect("config is valid");
1268        let profile = nextest_config_result
1269            .profile("default")
1270            .expect("valid profile name")
1271            .apply_build_platforms(&build_platforms());
1272
1273        // This query matches override 2.
1274        let host_binary_query =
1275            binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
1276        let test_name = TestCaseName::new("test");
1277        let query = TestQuery {
1278            binary_query: host_binary_query.to_query(),
1279            test_name: &test_name,
1280        };
1281        let overrides = profile.settings_for(NextestRunMode::Test, &query);
1282
1283        assert_eq!(overrides.threads_required(), ThreadsRequired::Count(8));
1284        assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(3));
1285        assert_eq!(
1286            overrides.slow_timeout(),
1287            SlowTimeout {
1288                period: Duration::from_secs(60),
1289                on_timeout: SlowTimeoutResult::default(),
1290                terminate_after: None,
1291                grace_period: Duration::from_secs(10),
1292            }
1293        );
1294        assert_eq!(
1295            overrides.leak_timeout(),
1296            LeakTimeout {
1297                period: Duration::from_millis(300),
1298                result: LeakTimeoutResult::Pass,
1299            }
1300        );
1301        assert_eq!(overrides.test_group(), &test_group("my-group"));
1302        assert_eq!(overrides.success_output(), TestOutputDisplay::Never);
1303        assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
1304        // For clarity.
1305        #[expect(clippy::bool_assert_comparison)]
1306        {
1307            assert_eq!(overrides.junit_store_success_output(), false);
1308            assert_eq!(overrides.junit_store_failure_output(), false);
1309        }
1310        assert_eq!(overrides.junit_report_skipped(), ReportSkipPolicy::None);
1311
1312        // This query matches override 1 and 2.
1313        let target_binary_query = binary_query(
1314            &graph,
1315            package_id,
1316            "lib",
1317            "my-binary",
1318            BuildPlatform::Target,
1319        );
1320        let test_name = TestCaseName::new("test");
1321        let query = TestQuery {
1322            binary_query: target_binary_query.to_query(),
1323            test_name: &test_name,
1324        };
1325        let overrides = profile.settings_for(NextestRunMode::Test, &query);
1326
1327        assert_eq!(overrides.threads_required(), ThreadsRequired::Count(8));
1328        assert_eq!(
1329            overrides.retries(),
1330            RetryPolicy::Exponential {
1331                count: 20,
1332                delay: Duration::from_secs(1),
1333                jitter: false,
1334                max_delay: Some(Duration::from_secs(20)),
1335            }
1336        );
1337        assert_eq!(
1338            overrides.slow_timeout(),
1339            SlowTimeout {
1340                period: Duration::from_secs(120),
1341                terminate_after: Some(NonZeroUsize::new(1).unwrap()),
1342                grace_period: Duration::ZERO,
1343                on_timeout: SlowTimeoutResult::default(),
1344            }
1345        );
1346        assert_eq!(
1347            overrides.leak_timeout(),
1348            LeakTimeout {
1349                period: Duration::from_millis(300),
1350                result: LeakTimeoutResult::Pass,
1351            }
1352        );
1353        assert_eq!(overrides.test_group(), &test_group("my-group"));
1354        assert_eq!(
1355            overrides.success_output(),
1356            TestOutputDisplay::ImmediateFinal
1357        );
1358        assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
1359        // For clarity.
1360        #[expect(clippy::bool_assert_comparison)]
1361        {
1362            assert_eq!(overrides.junit_store_success_output(), true);
1363            assert_eq!(overrides.junit_store_failure_output(), false);
1364        }
1365
1366        // This query matches override 3.
1367        let test_name = TestCaseName::new("override3");
1368        let query = TestQuery {
1369            binary_query: target_binary_query.to_query(),
1370            test_name: &test_name,
1371        };
1372        let overrides = profile.settings_for(NextestRunMode::Test, &query);
1373        assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(5));
1374
1375        // This query matches override 5.
1376        let test_name = TestCaseName::new("override5");
1377        let query = TestQuery {
1378            binary_query: target_binary_query.to_query(),
1379            test_name: &test_name,
1380        };
1381        let overrides = profile.settings_for(NextestRunMode::Test, &query);
1382        assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(8));
1383
1384        // This query matches override 6.
1385        let test_name = TestCaseName::new("timeout_success");
1386        let query = TestQuery {
1387            binary_query: target_binary_query.to_query(),
1388            test_name: &test_name,
1389        };
1390        let overrides = profile.settings_for(NextestRunMode::Test, &query);
1391        assert_eq!(
1392            overrides.slow_timeout(),
1393            SlowTimeout {
1394                period: Duration::from_secs(30),
1395                on_timeout: SlowTimeoutResult::Pass,
1396                terminate_after: None,
1397                grace_period: Duration::from_secs(10),
1398            }
1399        );
1400
1401        // This query does not match any overrides.
1402        let test_name = TestCaseName::new("no_match");
1403        let query = TestQuery {
1404            binary_query: target_binary_query.to_query(),
1405            test_name: &test_name,
1406        };
1407        let overrides = profile.settings_for(NextestRunMode::Test, &query);
1408        assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(0));
1409        assert_eq!(overrides.junit_report_skipped(), ReportSkipPolicy::All);
1410    }
1411
1412    /// Test that bench.slow-timeout works correctly in overrides.
1413    #[test]
1414    fn test_overrides_bench_slow_timeout() {
1415        let config_contents = indoc! {r#"
1416            # Profile-level benchmark slow-timeout (used as fallback).
1417            [profile.default]
1418            bench.slow-timeout = { period = "30y" }
1419
1420            # Override 1: Both test and bench slow-timeout specified.
1421            [[profile.default.overrides]]
1422            filter = "test(both_specified)"
1423            slow-timeout = "60s"
1424            bench.slow-timeout = { period = "5m", terminate-after = 2 }
1425
1426            # Override 2: Only test slow-timeout specified.
1427            [[profile.default.overrides]]
1428            filter = "test(test_only)"
1429            slow-timeout = "90s"
1430
1431            # Override 3: Only bench slow-timeout specified.
1432            [[profile.default.overrides]]
1433            filter = "test(bench_only)"
1434            bench.slow-timeout = "10m"
1435        "#};
1436
1437        let workspace_dir = tempdir().unwrap();
1438        let graph = temp_workspace(&workspace_dir, config_contents);
1439        let package_id = graph.workspace().iter().next().unwrap().id();
1440        let pcx = ParseContext::new(&graph);
1441
1442        let nextest_config_result = NextestConfig::from_sources(
1443            graph.workspace().root(),
1444            &pcx,
1445            None,
1446            &[][..],
1447            &Default::default(),
1448        )
1449        .expect("config is valid");
1450        let profile = nextest_config_result
1451            .profile("default")
1452            .expect("valid profile name")
1453            .apply_build_platforms(&build_platforms());
1454
1455        let host_binary_query =
1456            binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
1457
1458        // Test "both_specified": tests get slow-timeout, benchmarks get
1459        // bench.slow-timeout.
1460        let test_name = TestCaseName::new("both_specified");
1461        let query = TestQuery {
1462            binary_query: host_binary_query.to_query(),
1463            test_name: &test_name,
1464        };
1465
1466        let test_settings = profile.settings_for(NextestRunMode::Test, &query);
1467        assert_eq!(test_settings.slow_timeout().period, Duration::from_secs(60));
1468
1469        let bench_settings = profile.settings_for(NextestRunMode::Benchmark, &query);
1470        assert_eq!(
1471            bench_settings.slow_timeout(),
1472            SlowTimeout {
1473                period: Duration::from_secs(5 * 60),
1474                terminate_after: Some(NonZeroUsize::new(2).unwrap()),
1475                grace_period: Duration::from_secs(10),
1476                on_timeout: SlowTimeoutResult::default(),
1477            }
1478        );
1479
1480        // Test "test_only": tests get the override, benchmarks fall back to
1481        // profile default (no fallback from slow-timeout to
1482        // bench.slow-timeout).
1483        let test_name = TestCaseName::new("test_only");
1484        let query = TestQuery {
1485            binary_query: host_binary_query.to_query(),
1486            test_name: &test_name,
1487        };
1488
1489        let test_settings = profile.settings_for(NextestRunMode::Test, &query);
1490        assert_eq!(test_settings.slow_timeout().period, Duration::from_secs(90));
1491
1492        let bench_settings = profile.settings_for(NextestRunMode::Benchmark, &query);
1493        // Should use profile-level bench.slow-timeout (30 years), not the
1494        // override's slow-timeout. humantime parses "30y" accounting for leap
1495        // years, so we check >= VERY_LARGE rather than an exact value.
1496        assert!(
1497            bench_settings.slow_timeout().period >= SlowTimeout::VERY_LARGE.period,
1498            "should be >= VERY_LARGE, got {:?}",
1499            bench_settings.slow_timeout().period
1500        );
1501
1502        // Test "bench_only": tests get profile default, benchmarks get the
1503        // override.
1504        let test_name = TestCaseName::new("bench_only");
1505        let query = TestQuery {
1506            binary_query: host_binary_query.to_query(),
1507            test_name: &test_name,
1508        };
1509
1510        let test_settings = profile.settings_for(NextestRunMode::Test, &query);
1511        // Tests use the default slow-timeout (60s from default-config.toml).
1512        assert_eq!(test_settings.slow_timeout().period, Duration::from_secs(60));
1513
1514        let bench_settings = profile.settings_for(NextestRunMode::Benchmark, &query);
1515        assert_eq!(
1516            bench_settings.slow_timeout().period,
1517            Duration::from_secs(10 * 60)
1518        );
1519    }
1520
1521    #[test_case(
1522        indoc! {r#"
1523            [[profile.default.overrides]]
1524            retries = 2
1525        "#},
1526        "default",
1527        &[MietteJsonReport {
1528            message: "at least one of `platform` and `filter` must be specified".to_owned(),
1529            labels: vec![],
1530        }]
1531
1532        ; "neither platform nor filter specified"
1533    )]
1534    #[test_case(
1535        indoc! {r#"
1536            [[profile.default.overrides]]
1537            default-filter = "test(test1)"
1538            retries = 2
1539        "#},
1540        "default",
1541        &[MietteJsonReport {
1542            message: "for override with `default-filter`, `platform` must also be specified".to_owned(),
1543            labels: vec![],
1544        }]
1545
1546        ; "default-filter without platform"
1547    )]
1548    #[test_case(
1549        indoc! {r#"
1550            [[profile.default.overrides]]
1551            platform = 'cfg(unix)'
1552            default-filter = "not default()"
1553            retries = 2
1554        "#},
1555        "default",
1556        &[MietteJsonReport {
1557            message: "predicate not allowed in `default-filter` expressions".to_owned(),
1558            labels: vec![
1559                MietteJsonLabel {
1560                    label: "default() causes infinite recursion".to_owned(),
1561                    span: MietteJsonSpan { offset: 4, length: 9 },
1562                },
1563            ],
1564        }]
1565
1566        ; "default filterset in default-filter"
1567    )]
1568    #[test_case(
1569        indoc! {r#"
1570            [[profile.default.overrides]]
1571            filter = 'test(test1)'
1572            default-filter = "test(test2)"
1573            retries = 2
1574        "#},
1575        "default",
1576        &[MietteJsonReport {
1577            message: "at most one of `filter` and `default-filter` must be specified".to_owned(),
1578            labels: vec![],
1579        }]
1580
1581        ; "both filter and default-filter specified"
1582    )]
1583    #[test_case(
1584        indoc! {r#"
1585            [[profile.default.overrides]]
1586            filter = 'test(test1)'
1587            platform = 'cfg(unix)'
1588            default-filter = "test(test2)"
1589            retries = 2
1590        "#},
1591        "default",
1592        &[MietteJsonReport {
1593            message: "at most one of `filter` and `default-filter` must be specified".to_owned(),
1594            labels: vec![],
1595        }]
1596
1597        ; "both filter and default-filter specified with platform"
1598    )]
1599    #[test_case(
1600        indoc! {r#"
1601            [[profile.default.overrides]]
1602            platform = {}
1603            retries = 2
1604        "#},
1605        "default",
1606        &[MietteJsonReport {
1607            message: "at least one of `platform` and `filter` must be specified".to_owned(),
1608            labels: vec![],
1609        }]
1610
1611        ; "empty platform map"
1612    )]
1613    #[test_case(
1614        indoc! {r#"
1615            [[profile.ci.overrides]]
1616            platform = 'cfg(target_os = "macos)'
1617            retries = 2
1618        "#},
1619        "ci",
1620        &[MietteJsonReport {
1621            message: "error parsing cfg() expression".to_owned(),
1622            labels: vec![
1623                MietteJsonLabel { label: "unclosed quotes".to_owned(), span: MietteJsonSpan { offset: 16, length: 6 } }
1624            ]
1625        }]
1626
1627        ; "invalid platform expression"
1628    )]
1629    #[test_case(
1630        indoc! {r#"
1631            [[profile.ci.overrides]]
1632            filter = 'test(/foo)'
1633            retries = 2
1634        "#},
1635        "ci",
1636        &[MietteJsonReport {
1637            message: "expected close regex".to_owned(),
1638            labels: vec![
1639                MietteJsonLabel { label: "missing `/`".to_owned(), span: MietteJsonSpan { offset: 9, length: 0 } }
1640            ]
1641        }]
1642
1643        ; "invalid filterset"
1644    )]
1645    #[test_case(
1646        // Not strictly an override error, but convenient to put here.
1647        indoc! {r#"
1648            [profile.ci]
1649            default-filter = "test(foo) or default()"
1650        "#},
1651        "ci",
1652        &[MietteJsonReport {
1653            message: "predicate not allowed in `default-filter` expressions".to_owned(),
1654            labels: vec![
1655                MietteJsonLabel { label: "default() causes infinite recursion".to_owned(), span: MietteJsonSpan { offset: 13, length: 9 } }
1656            ]
1657        }]
1658
1659        ; "default-filter with default"
1660    )]
1661    fn parse_overrides_invalid(
1662        config_contents: &str,
1663        faulty_profile: &str,
1664        expected_reports: &[MietteJsonReport],
1665    ) {
1666        let workspace_dir = tempdir().unwrap();
1667
1668        let graph = temp_workspace(&workspace_dir, config_contents);
1669        let pcx = ParseContext::new(&graph);
1670
1671        let err = NextestConfig::from_sources(
1672            graph.workspace().root(),
1673            &pcx,
1674            None,
1675            [],
1676            &Default::default(),
1677        )
1678        .expect_err("config is invalid");
1679        match err.kind() {
1680            ConfigParseErrorKind::CompileErrors(compile_errors) => {
1681                assert_eq!(
1682                    compile_errors.len(),
1683                    1,
1684                    "exactly one override error must be produced"
1685                );
1686                let error = compile_errors.first().unwrap();
1687                assert_eq!(
1688                    error.profile_name, faulty_profile,
1689                    "compile error profile matches"
1690                );
1691                let handler = miette::JSONReportHandler::new();
1692                let reports = error
1693                    .kind
1694                    .reports()
1695                    .map(|report| {
1696                        let mut out = String::new();
1697                        handler.render_report(&mut out, report.as_ref()).unwrap();
1698
1699                        let json_report: MietteJsonReport = serde_json::from_str(&out)
1700                            .unwrap_or_else(|err| {
1701                                panic!(
1702                                    "failed to deserialize JSON message produced by miette: {err}"
1703                                )
1704                            });
1705                        json_report
1706                    })
1707                    .collect::<Vec<_>>();
1708                assert_eq!(&reports, expected_reports, "reports match");
1709            }
1710            other => {
1711                panic!(
1712                    "for config error {other:?}, expected ConfigParseErrorKind::FiltersetOrCfgParseError"
1713                );
1714            }
1715        };
1716    }
1717
1718    /// Test that `cfg(unix)` works with a custom platform.
1719    ///
1720    /// This was broken with older versions of target-spec.
1721    #[test]
1722    fn cfg_unix_with_custom_platform() {
1723        let config_contents = indoc! {r#"
1724            [[profile.default.overrides]]
1725            platform = { host = "cfg(unix)" }
1726            filter = "test(test)"
1727            retries = 5
1728        "#};
1729
1730        let workspace_dir = tempdir().unwrap();
1731
1732        let graph = temp_workspace(&workspace_dir, config_contents);
1733        let package_id = graph.workspace().iter().next().unwrap().id();
1734        let pcx = ParseContext::new(&graph);
1735
1736        let nextest_config = NextestConfig::from_sources(
1737            graph.workspace().root(),
1738            &pcx,
1739            None,
1740            &[][..],
1741            &Default::default(),
1742        )
1743        .expect("config is valid");
1744
1745        let build_platforms = custom_build_platforms(workspace_dir.path());
1746
1747        let profile = nextest_config
1748            .profile("default")
1749            .expect("valid profile name")
1750            .apply_build_platforms(&build_platforms);
1751
1752        // Check that the override is correctly applied.
1753        let target_binary_query = binary_query(
1754            &graph,
1755            package_id,
1756            "lib",
1757            "my-binary",
1758            BuildPlatform::Target,
1759        );
1760        let test_name = TestCaseName::new("test");
1761        let query = TestQuery {
1762            binary_query: target_binary_query.to_query(),
1763            test_name: &test_name,
1764        };
1765        let overrides = profile.settings_for(NextestRunMode::Test, &query);
1766        assert_eq!(
1767            overrides.retries(),
1768            RetryPolicy::new_without_delay(5),
1769            "retries applied to custom platform"
1770        );
1771    }
1772}