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