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