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