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