Skip to main content

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