nextest_runner/config/
overrides.rs

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