nextest_runner/config/
overrides.rs

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