Skip to main content

nextest_runner/config/core/
imp.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use super::{
5    ConfigFileSelection, ConfigPaths, ConfigSource, ConfigStyles, ExperimentalDeserialize,
6    NextestVersionDeserialize, ToolConfigFile,
7};
8use crate::{
9    config::{
10        core::ConfigExperimental,
11        elements::{
12            ArchiveConfig, BenchConfig, CustomTestGroup, DefaultBenchConfig, DefaultJunitImpl,
13            FlakyResult, GlobalTimeout, Inherits, JunitConfig, JunitImpl, JunitSettings,
14            LeakTimeout, MaxFail, RetryPolicy, SlowTimeout, TestGroup, TestGroupConfig,
15            TestThreads, ThreadsRequired, deserialize_fail_fast, deserialize_leak_timeout,
16            deserialize_retry_policy, deserialize_slow_timeout,
17        },
18        overrides::{
19            CompiledByProfile, CompiledData, CompiledDefaultFilter, DeserializedOverride,
20            ListSettings, ProfileDefaultFilter, SettingSource, TestSettings,
21            group_membership::PrecomputedGroupMembership,
22        },
23        scripts::{
24            DeserializedProfileScriptConfig, ProfileScriptType, ScriptConfig, ScriptId, ScriptInfo,
25            SetupScriptConfig, SetupScripts,
26        },
27    },
28    errors::{
29        ConfigParseError, ConfigParseErrorKind, InheritsError,
30        ProfileListScriptUsesRunFiltersError, ProfileNotFound, ProfileScriptErrors,
31        ProfileUnknownScriptError, ProfileWrongConfigScriptTypeError, UnknownTestGroupError,
32        provided_by_tool,
33    },
34    helpers::plural,
35    list::{TestInstanceId, TestList},
36    platform::BuildPlatforms,
37    reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplay},
38    run_mode::NextestRunMode,
39};
40use camino::{Utf8Path, Utf8PathBuf};
41use config::{Config, ConfigBuilder, ConfigError, File, FileFormat, builder::DefaultState};
42use iddqd::IdOrdMap;
43use indexmap::IndexMap;
44use nextest_filtering::{
45    BinaryQuery, EvalContext, Filterset, KnownGroups, ParseContext, TestQuery,
46};
47use owo_colors::OwoColorize;
48use petgraph::{Directed, Graph, algo::scc::kosaraju_scc, graph::NodeIndex};
49use serde::Deserialize;
50use std::{
51    collections::{BTreeMap, BTreeSet, HashMap, hash_map},
52    sync::LazyLock,
53};
54use tracing::warn;
55
56/// Trait for handling configuration warnings.
57///
58/// This trait allows for different warning handling strategies, such as logging warnings
59/// (the default behavior) or collecting them for testing purposes.
60pub trait ConfigWarnings {
61    /// Handle unknown configuration keys found in a config file.
62    fn unknown_config_keys(&mut self, source: &ConfigSource, unknown: &BTreeSet<String>);
63
64    /// Handle unknown profiles found in the reserved `default-` namespace.
65    fn unknown_reserved_profiles(&mut self, source: &ConfigSource, profiles: &[&str]);
66
67    /// Handle deprecated `[script.*]` configuration.
68    fn deprecated_script_config(&mut self, source: &ConfigSource);
69
70    /// Handle warning about empty script sections with neither setup nor
71    /// wrapper scripts.
72    fn empty_script_sections(
73        &mut self,
74        source: &ConfigSource,
75        profile_name: &str,
76        empty_count: usize,
77    );
78}
79
80/// Default implementation of ConfigWarnings that logs warnings using the
81/// tracing crate.
82#[derive(Clone, Debug, Default)]
83pub struct DefaultConfigWarnings {
84    styles: ConfigStyles,
85}
86
87impl DefaultConfigWarnings {
88    /// Creates an instance of self that style config paths and tool names with
89    /// `styles`.
90    pub fn new(styles: ConfigStyles) -> Self {
91        Self { styles }
92    }
93}
94
95impl ConfigWarnings for DefaultConfigWarnings {
96    fn unknown_config_keys(&mut self, source: &ConfigSource, unknown: &BTreeSet<String>) {
97        let mut unknown_str = String::new();
98        if unknown.len() == 1 {
99            // Print this on the same line.
100            unknown_str.push_str("key: ");
101            unknown_str.push_str(unknown.iter().next().unwrap());
102        } else {
103            unknown_str.push_str("keys:\n");
104            for ignored_key in unknown {
105                unknown_str.push('\n');
106                unknown_str.push_str("  - ");
107                unknown_str.push_str(ignored_key);
108            }
109        }
110
111        warn!(
112            "in config file {}{}, ignoring unknown configuration {unknown_str}",
113            source.path().display().style(self.styles.path),
114            provided_by_tool(source.tool(), self.styles.tool),
115        )
116    }
117
118    fn unknown_reserved_profiles(&mut self, source: &ConfigSource, profiles: &[&str]) {
119        warn!(
120            "in config file {}{}, ignoring unknown profiles in the reserved `default-` namespace:",
121            source.path().display().style(self.styles.path),
122            provided_by_tool(source.tool(), self.styles.tool),
123        );
124
125        for profile in profiles {
126            warn!("  {profile}");
127        }
128    }
129
130    fn deprecated_script_config(&mut self, source: &ConfigSource) {
131        warn!(
132            "in config file {}{}, [script.*] is deprecated and will be removed in a \
133             future version of nextest; use the `scripts.setup` table instead",
134            source.path().display().style(self.styles.path),
135            provided_by_tool(source.tool(), self.styles.tool),
136        );
137    }
138
139    fn empty_script_sections(
140        &mut self,
141        source: &ConfigSource,
142        profile_name: &str,
143        empty_count: usize,
144    ) {
145        warn!(
146            "in config file {}{}, [[profile.{}.scripts]] has {} {} \
147             with neither setup nor wrapper scripts",
148            source.path().display().style(self.styles.path),
149            provided_by_tool(source.tool(), self.styles.tool),
150            profile_name,
151            empty_count,
152            plural::sections_str(empty_count),
153        );
154    }
155}
156
157/// Gets the number of available CPUs and caches the value.
158#[inline]
159pub fn get_num_cpus() -> usize {
160    static NUM_CPUS: LazyLock<usize> =
161        LazyLock::new(|| match std::thread::available_parallelism() {
162            Ok(count) => count.into(),
163            Err(err) => {
164                warn!("unable to determine num-cpus ({err}), assuming 1 logical CPU");
165                1
166            }
167        });
168
169    *NUM_CPUS
170}
171
172/// Overall configuration for nextest.
173///
174/// This is the root data structure for nextest configuration. Most runner-specific configuration is
175/// managed through [profiles](EvaluatableProfile), obtained through the [`profile`](Self::profile)
176/// method.
177///
178/// For more about configuration, see [_Configuration_](https://nexte.st/docs/configuration) in the
179/// nextest book.
180#[derive(Clone, Debug)]
181pub struct NextestConfig {
182    workspace_root: Utf8PathBuf,
183    inner: NextestConfigImpl,
184    compiled: CompiledByProfile,
185}
186
187/// The config path to the default profile's default-filter.
188const DEFAULT_PROFILE_DEFAULT_FILTER_KEY: &str = "profile.default.default-filter";
189
190impl NextestConfig {
191    /// The default location of the config within the path: `.config/nextest.toml`, used to read the
192    /// config from the given directory.
193    pub const CONFIG_PATH: &'static str = ".config/nextest.toml";
194
195    /// Contains the default config as a TOML file.
196    ///
197    /// Repository-specific configuration is layered on top of the default config.
198    pub const DEFAULT_CONFIG: &'static str = include_str!("../../../default-config.toml");
199
200    /// Contains the canonical repository config reference markdown.
201    pub const REFERENCE_MD: &'static str = include_str!("../../../repo-config-reference.md");
202
203    /// The pregenerated JSON Schema for `.config/nextest.toml`.
204    ///
205    /// The schema is checked into the repository at
206    /// `nextest-runner/jsonschemas/repo-config.json`. (If you're working within
207    /// the nextest repository, regenerate the schema with `just
208    /// generate-schemas`.)
209    pub const SCHEMA: &'static str = include_str!("../../../jsonschemas/repo-config.json");
210
211    /// Environment configuration uses this prefix, plus a _.
212    pub const ENVIRONMENT_PREFIX: &'static str = "NEXTEST";
213
214    /// The name of the default profile.
215    pub const DEFAULT_PROFILE: &'static str = "default";
216
217    /// The name of the default profile used for miri.
218    pub const DEFAULT_MIRI_PROFILE: &'static str = "default-miri";
219
220    /// A list containing the names of the Nextest defined reserved profile names.
221    pub const DEFAULT_PROFILES: &'static [&'static str] =
222        &[Self::DEFAULT_PROFILE, Self::DEFAULT_MIRI_PROFILE];
223
224    /// Reads the nextest config from the given file, or if not specified from `.config/nextest.toml`
225    /// in the workspace root.
226    ///
227    /// `tool_config_files` are lower priority than `config_file` but higher priority than the
228    /// default config. Files in `tool_config_files` that come earlier are higher priority than those
229    /// that come later.
230    ///
231    /// If no config files are specified and this file doesn't have `.config/nextest.toml`, uses the
232    /// default config options.
233    pub fn from_sources<'a, I>(
234        workspace_root: impl Into<Utf8PathBuf>,
235        pcx: &ParseContext<'_>,
236        config_file: Option<&Utf8Path>,
237        tool_config_files: impl IntoIterator<IntoIter = I>,
238        experimental: &BTreeSet<ConfigExperimental>,
239    ) -> Result<Self, ConfigParseError>
240    where
241        I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
242    {
243        Self::from_sources_with_warnings(
244            workspace_root,
245            pcx,
246            config_file,
247            tool_config_files,
248            experimental,
249            &mut DefaultConfigWarnings::default(),
250        )
251    }
252
253    /// Load configuration from the given sources with custom warning handling.
254    pub fn from_sources_with_warnings<'a, I>(
255        workspace_root: impl Into<Utf8PathBuf>,
256        pcx: &ParseContext<'_>,
257        config_file: Option<&Utf8Path>,
258        tool_config_files: impl IntoIterator<IntoIter = I>,
259        experimental: &BTreeSet<ConfigExperimental>,
260        warnings: &mut impl ConfigWarnings,
261    ) -> Result<Self, ConfigParseError>
262    where
263        I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
264    {
265        Self::from_sources_with_selection(
266            workspace_root,
267            pcx,
268            ConfigFileSelection::new(config_file),
269            tool_config_files,
270            experimental,
271            warnings,
272        )
273    }
274
275    /// Reads configuration from the given workspace root and file selection.
276    pub fn from_sources_with_selection<'a, I>(
277        workspace_root: impl Into<Utf8PathBuf>,
278        pcx: &ParseContext<'_>,
279        selection: ConfigFileSelection<'_>,
280        tool_config_files: impl IntoIterator<IntoIter = I>,
281        experimental: &BTreeSet<ConfigExperimental>,
282        warnings: &mut impl ConfigWarnings,
283    ) -> Result<Self, ConfigParseError>
284    where
285        I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
286    {
287        let workspace_root = workspace_root.into();
288        Self::from_sources_with_paths(
289            &ConfigPaths::capture(&workspace_root).map_err(|error| {
290                ConfigParseError::from_paths_capture_error(
291                    &workspace_root,
292                    selection.explicit_config_file(),
293                    error,
294                )
295            })?,
296            pcx,
297            selection,
298            tool_config_files,
299            experimental,
300            warnings,
301        )
302    }
303
304    /// Reads configuration from the given paths and file selection.
305    pub fn from_sources_with_paths<'a, I>(
306        paths: &ConfigPaths,
307        pcx: &ParseContext<'_>,
308        selection: ConfigFileSelection<'_>,
309        tool_config_files: impl IntoIterator<IntoIter = I>,
310        experimental: &BTreeSet<ConfigExperimental>,
311        warnings: &mut impl ConfigWarnings,
312    ) -> Result<Self, ConfigParseError>
313    where
314        I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
315    {
316        let workspace_root = paths.workspace_root().as_path().to_owned();
317        let tool_config_files_rev = tool_config_files.into_iter().rev();
318        let (inner, compiled) = Self::read_from_sources(
319            pcx,
320            paths,
321            selection,
322            tool_config_files_rev,
323            experimental,
324            warnings,
325        )?;
326        Ok(Self {
327            workspace_root,
328            inner,
329            compiled,
330        })
331    }
332
333    /// Returns the default nextest config.
334    #[cfg(test)]
335    pub(crate) fn default_config(workspace_root: impl Into<Utf8PathBuf>) -> Self {
336        use itertools::Itertools;
337
338        let config = Self::make_default_config()
339            .build()
340            .expect("default config is always valid");
341
342        let mut unknown = BTreeSet::new();
343        let deserialized = Self::deserialize_config(config, |path| {
344            unknown.insert(path.to_string());
345        })
346        .expect("default config is always valid");
347
348        // Make sure there aren't any unknown keys in the default config, since it is
349        // embedded/shipped with this binary.
350        if !unknown.is_empty() {
351            panic!(
352                "found unknown keys in default config: {}",
353                unknown.iter().join(", ")
354            );
355        }
356
357        Self {
358            workspace_root: workspace_root.into(),
359            inner: deserialized.into_config_impl(),
360            // The default config has no overrides or special settings.
361            compiled: CompiledByProfile::for_default_config(),
362        }
363    }
364
365    /// Returns the profile with the given name, or an error if a profile was
366    /// specified but not found.
367    pub fn profile(&self, name: impl AsRef<str>) -> Result<EarlyProfile<'_>, ProfileNotFound> {
368        self.make_profile(name.as_ref())
369    }
370
371    // ---
372    // Helper methods
373    // ---
374
375    fn read_from_sources<'a>(
376        pcx: &ParseContext<'_>,
377        paths: &ConfigPaths,
378        selection: ConfigFileSelection<'_>,
379        tool_config_files_rev: impl Iterator<Item = &'a ToolConfigFile>,
380        experimental: &BTreeSet<ConfigExperimental>,
381        warnings: &mut impl ConfigWarnings,
382    ) -> Result<(NextestConfigImpl, CompiledByProfile), ConfigParseError> {
383        // First, get the default config.
384        let mut composite_builder = Self::make_default_config();
385
386        // Overrides are handled additively.
387        // Note that they're stored in reverse order here, and are flipped over at the end.
388        let mut compiled = CompiledByProfile::for_default_config();
389
390        let mut known_groups = BTreeSet::new();
391        let mut known_scripts = IdOrdMap::new();
392        // Track known profiles for inheritance validation. Profiles can only inherit
393        // from profiles defined in the same file or in previously loaded (lower priority) files.
394        let mut known_profiles = BTreeSet::new();
395
396        for source in selection.sources(paths, tool_config_files_rev)? {
397            let Some(contents) = source.read()? else {
398                continue;
399            };
400            let file_config = Config::builder()
401                .add_source(File::from_str(&contents, FileFormat::Toml))
402                .build()
403                .map_err(|error| ConfigParseError::new(&source, error.into()))?;
404            Self::deserialize_individual_config(
405                pcx,
406                &source,
407                &file_config,
408                &mut compiled,
409                experimental,
410                warnings,
411                &mut known_groups,
412                &mut known_scripts,
413                &mut known_profiles,
414            )?;
415
416            // This is the final, composite builder used at the end.
417            composite_builder = composite_builder.add_source(file_config);
418        }
419
420        let config_file = selection.repo_config_path(paths)?;
421        let composite = composite_builder
422            .build()
423            .map_err(|error| ConfigParseError::from_path(&config_file, error.into()))?;
424        // Unknown keys are ignored here because any values in it have already been reported in
425        // deserialize_individual_config.
426        let config = Self::deserialize_config(composite, |_| {})
427            .map_err(|kind| ConfigParseError::from_path(&config_file, kind))?;
428
429        let config = config.into_config_impl();
430
431        // A higher-priority file can redefine a profile that a lower-priority
432        // file inherits from, so do one final check for cycles in the merged
433        // config.
434        config
435            .sanitize_profile_inherits(&BTreeSet::new())
436            .map_err(|kind| ConfigParseError::from_path(&config_file, kind))?;
437
438        // Reverse all the compiled data at the end.
439        compiled.default.reverse();
440        for data in compiled.other.values_mut() {
441            data.reverse();
442        }
443
444        Ok((config, compiled))
445    }
446
447    #[expect(clippy::too_many_arguments)]
448    fn deserialize_individual_config(
449        pcx: &ParseContext<'_>,
450        source: &ConfigSource,
451        file_config: &Config,
452        compiled_out: &mut CompiledByProfile,
453        experimental: &BTreeSet<ConfigExperimental>,
454        warnings: &mut impl ConfigWarnings,
455        known_groups: &mut BTreeSet<CustomTestGroup>,
456        known_scripts: &mut IdOrdMap<ScriptInfo>,
457        known_profiles: &mut BTreeSet<String>,
458    ) -> Result<(), ConfigParseError> {
459        // Try building default builder + this file to get good error attribution and handle
460        // overrides additively.
461        let layered = Self::make_default_config()
462            .add_source(file_config.clone())
463            .build()
464            .map_err(|error| ConfigParseError::new(source, error.into()))?;
465        let mut unknown = BTreeSet::new();
466        let mut this_config = Self::deserialize_config(layered, |path| {
467            unknown.insert(path.to_string());
468        })
469        .map_err(|kind| ConfigParseError::new(source, kind))?;
470
471        if !unknown.is_empty() {
472            warnings.unknown_config_keys(source, &unknown);
473        }
474
475        let tool = source.tool();
476
477        // Check that test groups are named as expected.
478        let (valid_groups, invalid_groups): (BTreeSet<_>, _) =
479            this_config.test_groups.keys().cloned().partition(|group| {
480                if let Some(tool) = tool {
481                    // The first component must be the tool name.
482                    group
483                        .as_identifier()
484                        .tool_components()
485                        .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
486                } else {
487                    // If a tool is not specified, it must *not* be a tool identifier.
488                    !group.as_identifier().is_tool_identifier()
489                }
490            });
491
492        if !invalid_groups.is_empty() {
493            let kind = if tool.is_some() {
494                ConfigParseErrorKind::InvalidTestGroupsDefinedByTool(invalid_groups)
495            } else {
496                ConfigParseErrorKind::InvalidTestGroupsDefined(invalid_groups)
497            };
498            return Err(ConfigParseError::new(source, kind));
499        }
500
501        known_groups.extend(valid_groups);
502
503        // If both scripts and old_setup_scripts are present, produce an error.
504        if !this_config.scripts.is_empty() && !this_config.old_setup_scripts.is_empty() {
505            return Err(ConfigParseError::new(
506                source,
507                ConfigParseErrorKind::BothScriptAndScriptsDefined,
508            ));
509        }
510
511        // If old_setup_scripts are present, produce a warning.
512        if !this_config.old_setup_scripts.is_empty() {
513            warnings.deprecated_script_config(source);
514            this_config.scripts.setup = this_config.old_setup_scripts.clone();
515        }
516
517        // Check for experimental features that are used but not enabled.
518        {
519            let mut missing_features = BTreeSet::new();
520            if !this_config.scripts.setup.is_empty()
521                && !experimental.contains(&ConfigExperimental::SetupScripts)
522            {
523                missing_features.insert(ConfigExperimental::SetupScripts);
524            }
525            if !this_config.scripts.wrapper.is_empty()
526                && !experimental.contains(&ConfigExperimental::WrapperScripts)
527            {
528                missing_features.insert(ConfigExperimental::WrapperScripts);
529            }
530            if !missing_features.is_empty() {
531                return Err(ConfigParseError::new(
532                    source,
533                    ConfigParseErrorKind::ExperimentalFeaturesNotEnabled { missing_features },
534                ));
535            }
536        }
537
538        this_config
539            .scripts
540            .check_duplicate_ids()
541            .map_err(|kind| ConfigParseError::new(source, kind))?;
542
543        // Check that setup scripts are named as expected.
544        let (valid_scripts, invalid_scripts): (BTreeSet<_>, _) = this_config
545            .scripts
546            .all_script_ids()
547            .cloned()
548            .partition(|script| {
549                if let Some(tool) = tool {
550                    // The first component must be the tool name.
551                    script
552                        .as_identifier()
553                        .tool_components()
554                        .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
555                } else {
556                    // If a tool is not specified, it must *not* be a tool identifier.
557                    !script.as_identifier().is_tool_identifier()
558                }
559            });
560
561        if !invalid_scripts.is_empty() {
562            let kind = if tool.is_some() {
563                ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool(invalid_scripts)
564            } else {
565                ConfigParseErrorKind::InvalidConfigScriptsDefined(invalid_scripts)
566            };
567            return Err(ConfigParseError::new(source, kind));
568        }
569
570        known_scripts.extend(
571            valid_scripts
572                .into_iter()
573                .map(|id| this_config.scripts.script_info(id)),
574        );
575
576        let this_config = this_config.into_config_impl();
577
578        let unknown_default_profiles: Vec<_> = this_config
579            .all_profiles()
580            .filter(|p| p.starts_with("default-") && !NextestConfig::DEFAULT_PROFILES.contains(p))
581            .collect();
582        if !unknown_default_profiles.is_empty() {
583            warnings.unknown_reserved_profiles(source, &unknown_default_profiles);
584        }
585
586        // Check that the profiles correctly use the inherits setting.
587        // Profiles can only inherit from profiles in the same file or in previously
588        // loaded (lower priority) files.
589        this_config
590            .sanitize_profile_inherits(known_profiles)
591            .map_err(|kind| ConfigParseError::new(source, kind))?;
592
593        // Add this file's profiles to known_profiles for subsequent files.
594        known_profiles.extend(
595            this_config
596                .other_profiles()
597                .map(|(name, _)| name.to_owned()),
598        );
599
600        // Compile the overrides for this file.
601        //
602        // The default-config.toml shipped with nextest sets default-filter only
603        // on profile.default, so that is the only profile that can carry a
604        // filter that this file did not write.
605        let file_default_filter =
606            match file_config.get::<String>(DEFAULT_PROFILE_DEFAULT_FILTER_KEY) {
607                Ok(filter) => Some(filter),
608                Err(ConfigError::NotFound(_)) => None,
609                Err(error) => return Err(ConfigParseError::new(source, error.into())),
610            };
611        let this_compiled = CompiledByProfile::new(
612            pcx,
613            source,
614            &this_config,
615            ProfileDefaultFilter::new(file_default_filter.as_deref()),
616        )
617        .map_err(|kind| ConfigParseError::new(source, kind))?;
618
619        // Check that all overrides specify known test groups.
620        let mut unknown_group_errors = Vec::new();
621        let mut check_test_group = |profile_name: &str, test_group: Option<&TestGroup>| {
622            if let Some(TestGroup::Custom(group)) = test_group
623                && !known_groups.contains(group)
624            {
625                unknown_group_errors.push(UnknownTestGroupError {
626                    profile_name: profile_name.to_owned(),
627                    name: TestGroup::Custom(group.clone()),
628                });
629            }
630        };
631
632        this_compiled
633            .default
634            .overrides
635            .iter()
636            .for_each(|override_| {
637                check_test_group("default", override_.data.test_group.as_ref());
638            });
639
640        // Check that override test groups are known.
641        this_compiled.other.iter().for_each(|(profile_name, data)| {
642            data.overrides.iter().for_each(|override_| {
643                check_test_group(profile_name, override_.data.test_group.as_ref());
644            });
645        });
646
647        // If there were any unknown groups, error out.
648        if !unknown_group_errors.is_empty() {
649            let known_groups = TestGroup::make_all_groups(known_groups.iter().cloned()).collect();
650            return Err(ConfigParseError::new(
651                source,
652                ConfigParseErrorKind::UnknownTestGroups {
653                    errors: unknown_group_errors,
654                    known_groups,
655                },
656            ));
657        }
658
659        // Check that scripts are known and that there aren't any other errors
660        // with them.
661        let mut profile_script_errors = ProfileScriptErrors::default();
662        let mut check_script_ids = |profile_name: &str,
663                                    script_type: ProfileScriptType,
664                                    expr: Option<&Filterset>,
665                                    scripts: &[ScriptId]| {
666            for script in scripts {
667                if let Some(script_info) = known_scripts.get(script) {
668                    if !script_info.script_type.matches(script_type) {
669                        profile_script_errors.wrong_script_types.push(
670                            ProfileWrongConfigScriptTypeError {
671                                profile_name: profile_name.to_owned(),
672                                name: script.clone(),
673                                attempted: script_type,
674                                actual: script_info.script_type,
675                            },
676                        );
677                    }
678                    if script_type == ProfileScriptType::ListWrapper
679                        && let Some(expr) = expr
680                    {
681                        let runtime_only_leaves = expr.parsed.runtime_only_leaves();
682                        if !runtime_only_leaves.is_empty() {
683                            let filters = runtime_only_leaves
684                                .iter()
685                                .map(|leaf| leaf.to_string())
686                                .collect();
687                            profile_script_errors.list_scripts_using_run_filters.push(
688                                ProfileListScriptUsesRunFiltersError {
689                                    profile_name: profile_name.to_owned(),
690                                    name: script.clone(),
691                                    script_type,
692                                    filters,
693                                },
694                            );
695                        }
696                    }
697                } else {
698                    profile_script_errors
699                        .unknown_scripts
700                        .push(ProfileUnknownScriptError {
701                            profile_name: profile_name.to_owned(),
702                            name: script.clone(),
703                        });
704                }
705            }
706        };
707
708        let mut empty_script_count = 0;
709
710        this_compiled.default.scripts.iter().for_each(|scripts| {
711            if scripts.setup.is_empty()
712                && scripts.list_wrapper.is_none()
713                && scripts.run_wrapper.is_none()
714            {
715                empty_script_count += 1;
716            }
717
718            check_script_ids(
719                "default",
720                ProfileScriptType::Setup,
721                scripts.data.expr(),
722                &scripts.setup,
723            );
724            check_script_ids(
725                "default",
726                ProfileScriptType::ListWrapper,
727                scripts.data.expr(),
728                scripts.list_wrapper.as_slice(),
729            );
730            check_script_ids(
731                "default",
732                ProfileScriptType::RunWrapper,
733                scripts.data.expr(),
734                scripts.run_wrapper.as_slice(),
735            );
736        });
737
738        if empty_script_count > 0 {
739            warnings.empty_script_sections(source, "default", empty_script_count);
740        }
741
742        this_compiled.other.iter().for_each(|(profile_name, data)| {
743            let mut empty_script_count = 0;
744            data.scripts.iter().for_each(|scripts| {
745                if scripts.setup.is_empty()
746                    && scripts.list_wrapper.is_none()
747                    && scripts.run_wrapper.is_none()
748                {
749                    empty_script_count += 1;
750                }
751
752                check_script_ids(
753                    profile_name,
754                    ProfileScriptType::Setup,
755                    scripts.data.expr(),
756                    &scripts.setup,
757                );
758                check_script_ids(
759                    profile_name,
760                    ProfileScriptType::ListWrapper,
761                    scripts.data.expr(),
762                    scripts.list_wrapper.as_slice(),
763                );
764                check_script_ids(
765                    profile_name,
766                    ProfileScriptType::RunWrapper,
767                    scripts.data.expr(),
768                    scripts.run_wrapper.as_slice(),
769                );
770            });
771
772            if empty_script_count > 0 {
773                warnings.empty_script_sections(source, profile_name, empty_script_count);
774            }
775        });
776
777        // If there were any errors parsing profile-specific script data, error
778        // out.
779        if !profile_script_errors.is_empty() {
780            let known_scripts = known_scripts
781                .iter()
782                .map(|script| script.id.clone())
783                .collect();
784            return Err(ConfigParseError::new(
785                source,
786                ConfigParseErrorKind::ProfileScriptErrors {
787                    errors: Box::new(profile_script_errors),
788                    known_scripts,
789                },
790            ));
791        }
792
793        // Grab the compiled data (default-filter, overrides and setup scripts) for this config,
794        // adding them in reversed order (we'll flip it around at the end).
795        compiled_out.default.extend_reverse(this_compiled.default);
796        for (name, mut data) in this_compiled.other {
797            match compiled_out.other.entry(name) {
798                hash_map::Entry::Vacant(entry) => {
799                    // When inserting a new element, reverse the data.
800                    data.reverse();
801                    entry.insert(data);
802                }
803                hash_map::Entry::Occupied(mut entry) => {
804                    // When appending to an existing element, extend the data in reverse.
805                    entry.get_mut().extend_reverse(data);
806                }
807            }
808        }
809
810        Ok(())
811    }
812
813    fn make_default_config() -> ConfigBuilder<DefaultState> {
814        Config::builder().add_source(File::from_str(Self::DEFAULT_CONFIG, FileFormat::Toml))
815    }
816
817    fn make_profile(&self, name: &str) -> Result<EarlyProfile<'_>, ProfileNotFound> {
818        let custom_profile = self.inner.get_profile(name)?;
819
820        // Resolve the inherited profile into a profile chain
821        let inheritance_chain = self.inner.resolve_inheritance_chain(name)?;
822
823        // The profile was found: construct it.
824        let mut store_dir = self.workspace_root.join(&self.inner.store.dir);
825        store_dir.push(name);
826
827        // Grab the compiled data as well, furthest ancestor first so that the
828        // profile itself ends up with the highest priority.
829        let mut compiled_data = self.compiled.default.clone();
830        for profile_name in inheritance_chain
831            .iter()
832            .rev()
833            .map(|(ancestor, _)| *ancestor)
834            .chain(std::iter::once(name))
835        {
836            // It is possible that a profile in the chain doesn't have any
837            // compiled data associated with it. `compiled.other` is built only
838            // from the config files that were read, so a profile that exists
839            // solely in the embedded default config, such as `default-miri`,
840            // doesn't have an entry in `compiled.other`. Ignore this case since
841            // if there's no data, there's certainly no overrides.
842            if let Some(data) = self.compiled.other.get(profile_name) {
843                compiled_data = data.clone().chain(compiled_data);
844            }
845        }
846
847        Ok(EarlyProfile {
848            name: name.to_owned(),
849            store_dir,
850            default_profile: &self.inner.default_profile,
851            custom_profile,
852            inheritance_chain: inheritance_chain
853                .into_iter()
854                .map(|(_, profile)| profile)
855                .collect(),
856            test_groups: &self.inner.test_groups,
857            scripts: &self.inner.scripts,
858            compiled_data,
859        })
860    }
861
862    fn deserialize_config(
863        config: Config,
864        mut ignored: impl FnMut(serde_ignored::Path<'_>),
865    ) -> Result<NextestConfigDeserialize, ConfigParseErrorKind> {
866        let ignored_de = serde_ignored::Deserializer::new(config, &mut ignored);
867        let config: NextestConfigDeserialize = serde_path_to_error::deserialize(ignored_de)
868            .map_err(|error| {
869                // Both serde_path_to_error and the latest versions of the
870                // config crate report the key. We drop the key from the config
871                // error for consistency.
872                let path = error.path().clone();
873                let config_error = error.into_inner();
874                let error = match config_error {
875                    ConfigError::At { error, .. } => *error,
876                    other => other,
877                };
878                ConfigParseErrorKind::DeserializeError(Box::new(serde_path_to_error::Error::new(
879                    path, error,
880                )))
881            })?;
882
883        Ok(config)
884    }
885}
886
887/// The state of nextest profiles before build platforms have been applied.
888#[derive(Clone, Debug, Default)]
889pub(in crate::config) struct PreBuildPlatform {}
890
891/// The state of nextest profiles after build platforms have been applied.
892#[derive(Clone, Debug)]
893pub(crate) struct FinalConfig {
894    // Evaluation result for host_spec on the host platform.
895    pub(in crate::config) host_eval: bool,
896    // Evaluation result for target_spec corresponding to tests that run on the host platform (e.g.
897    // proc-macro tests).
898    pub(in crate::config) host_test_eval: bool,
899    // Evaluation result for target_spec corresponding to tests that run on the target platform
900    // (most regular tests).
901    pub(in crate::config) target_eval: bool,
902}
903
904/// A nextest profile that can be obtained without identifying the host and
905/// target platforms.
906///
907/// Returned by [`NextestConfig::profile`].
908pub struct EarlyProfile<'cfg> {
909    name: String,
910    store_dir: Utf8PathBuf,
911    default_profile: &'cfg DefaultProfileImpl,
912    custom_profile: Option<&'cfg CustomProfileImpl>,
913    inheritance_chain: Vec<&'cfg CustomProfileImpl>,
914    test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
915    // This is ordered because the scripts are used in the order they're defined.
916    scripts: &'cfg ScriptConfig,
917    // Invariant: `compiled_data.default_filter` is always present.
918    pub(in crate::config) compiled_data: CompiledData<PreBuildPlatform>,
919}
920
921/// These macros return a specific config field from a profile, checking in
922/// order: custom profile, inheritance chain, then default profile.
923macro_rules! profile_field {
924    ($eval_prof:ident.$field:ident) => {
925        $eval_prof
926            .custom_profile
927            .iter()
928            .chain($eval_prof.inheritance_chain.iter())
929            .find_map(|p| p.$field)
930            .unwrap_or($eval_prof.default_profile.$field)
931    };
932    ($eval_prof:ident.$nested:ident.$field:ident) => {
933        $eval_prof
934            .custom_profile
935            .iter()
936            .chain($eval_prof.inheritance_chain.iter())
937            .find_map(|p| p.$nested.$field)
938            .unwrap_or($eval_prof.default_profile.$nested.$field)
939    };
940    // Variant for method calls with arguments.
941    ($eval_prof:ident.$method:ident($($arg:expr),*)) => {
942        $eval_prof
943            .custom_profile
944            .iter()
945            .chain($eval_prof.inheritance_chain.iter())
946            .find_map(|p| p.$method($($arg),*))
947            .unwrap_or_else(|| $eval_prof.default_profile.$method($($arg),*))
948    };
949}
950macro_rules! profile_field_from_ref {
951    ($eval_prof:ident.$field:ident.$ref_func:ident()) => {
952        $eval_prof
953            .custom_profile
954            .iter()
955            .chain($eval_prof.inheritance_chain.iter())
956            .find_map(|p| p.$field.$ref_func())
957            .unwrap_or(&$eval_prof.default_profile.$field)
958    };
959    ($eval_prof:ident.$nested:ident.$field:ident.$ref_func:ident()) => {
960        $eval_prof
961            .custom_profile
962            .iter()
963            .chain($eval_prof.inheritance_chain.iter())
964            .find_map(|p| p.$nested.$field.$ref_func())
965            .unwrap_or(&$eval_prof.default_profile.$nested.$field)
966    };
967}
968// Variant for fields where both custom and default are Option.
969macro_rules! profile_field_optional {
970    ($eval_prof:ident.$nested:ident.$field:ident.$ref_func:ident()) => {
971        $eval_prof
972            .custom_profile
973            .iter()
974            .chain($eval_prof.inheritance_chain.iter())
975            .find_map(|p| p.$nested.$field.$ref_func())
976            .or($eval_prof.default_profile.$nested.$field.$ref_func())
977    };
978}
979
980impl<'cfg> EarlyProfile<'cfg> {
981    /// Returns the absolute profile-specific store directory.
982    pub fn store_dir(&self) -> &Utf8Path {
983        &self.store_dir
984    }
985
986    /// Returns true if JUnit XML output is configured for this profile.
987    pub fn has_junit(&self) -> bool {
988        profile_field_optional!(self.junit.path.as_deref()).is_some()
989    }
990
991    /// Returns the global test group configuration.
992    pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
993        self.test_groups
994    }
995
996    /// Returns the known test groups for filterset validation.
997    ///
998    /// Only custom group names are included; `@global` is always
999    /// implicitly valid and handled by `KnownGroups` itself.
1000    pub fn known_groups(&self) -> KnownGroups {
1001        let custom_groups = self
1002            .test_group_config()
1003            .keys()
1004            .map(|g| g.to_string())
1005            .collect();
1006        KnownGroups::Known { custom_groups }
1007    }
1008
1009    /// Applies build platforms to make the profile ready for evaluation.
1010    ///
1011    /// This is a separate step from parsing the config and reading a profile so that cargo-nextest
1012    /// can tell users about configuration parsing errors before building the binary list.
1013    pub fn apply_build_platforms(
1014        self,
1015        build_platforms: &BuildPlatforms,
1016    ) -> EvaluatableProfile<'cfg> {
1017        let compiled_data = self.compiled_data.apply_build_platforms(build_platforms);
1018
1019        let resolved_default_filter = {
1020            // Look for the default filter in the first valid override.
1021            let found_filter = compiled_data
1022                .overrides
1023                .iter()
1024                .find_map(|override_data| override_data.default_filter_if_matches_platform());
1025            found_filter.unwrap_or_else(|| {
1026                // No overrides matching the default filter were found -- use
1027                // the profile's default.
1028                compiled_data
1029                    .profile_default_filter
1030                    .as_ref()
1031                    .expect("compiled data always has default set")
1032            })
1033        }
1034        .clone();
1035
1036        EvaluatableProfile {
1037            name: self.name,
1038            store_dir: self.store_dir,
1039            default_profile: self.default_profile,
1040            custom_profile: self.custom_profile,
1041            inheritance_chain: self.inheritance_chain,
1042            scripts: self.scripts,
1043            test_groups: self.test_groups,
1044            compiled_data,
1045            resolved_default_filter,
1046        }
1047    }
1048}
1049
1050/// A configuration profile for nextest. Contains most configuration used by the nextest runner.
1051///
1052/// Returned by [`EarlyProfile::apply_build_platforms`].
1053#[derive(Clone, Debug)]
1054pub struct EvaluatableProfile<'cfg> {
1055    name: String,
1056    store_dir: Utf8PathBuf,
1057    default_profile: &'cfg DefaultProfileImpl,
1058    custom_profile: Option<&'cfg CustomProfileImpl>,
1059    inheritance_chain: Vec<&'cfg CustomProfileImpl>,
1060    test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
1061    // This is ordered because the scripts are used in the order they're defined.
1062    scripts: &'cfg ScriptConfig,
1063    // Invariant: `compiled_data.default_filter` is always present.
1064    pub(in crate::config) compiled_data: CompiledData<FinalConfig>,
1065    // The default filter that's been resolved after considering overrides (i.e.
1066    // platforms).
1067    resolved_default_filter: CompiledDefaultFilter,
1068}
1069
1070impl<'cfg> EvaluatableProfile<'cfg> {
1071    /// Returns the name of the profile.
1072    pub fn name(&self) -> &str {
1073        &self.name
1074    }
1075
1076    /// Returns the absolute profile-specific store directory.
1077    pub fn store_dir(&self) -> &Utf8Path {
1078        &self.store_dir
1079    }
1080
1081    /// Returns the context in which to evaluate filtersets.
1082    pub fn filterset_ecx(&self) -> EvalContext<'_> {
1083        EvalContext {
1084            default_filter: &self.default_filter().expr,
1085        }
1086    }
1087
1088    /// Precomputes test group memberships for the given tests.
1089    ///
1090    /// Uses [`settings_for`](Self::settings_for) to determine each
1091    /// test's group, keeping the override resolution logic in one
1092    /// place. The result implements [`nextest_filtering::GroupLookup`]
1093    /// and should be passed into an [`EvalContext`] for CLI filterset
1094    /// evaluation.
1095    pub fn precompute_group_memberships<'a>(
1096        &self,
1097        tests: impl Iterator<Item = TestQuery<'a>>,
1098    ) -> PrecomputedGroupMembership {
1099        // test_group is not mode-dependent, so the choice of run mode
1100        // doesn't matter here.
1101        let run_mode = NextestRunMode::Test;
1102
1103        let mut membership = PrecomputedGroupMembership::empty();
1104        for test in tests {
1105            let group = self.settings_for(run_mode, &test).test_group().clone();
1106            if group != TestGroup::Global {
1107                let id = TestInstanceId {
1108                    binary_id: test.binary_query.binary_id,
1109                    test_name: test.test_name,
1110                };
1111                membership.insert(id.to_owned(), group);
1112            }
1113        }
1114        membership
1115    }
1116
1117    /// Returns the default set of tests to run.
1118    pub fn default_filter(&self) -> &CompiledDefaultFilter {
1119        &self.resolved_default_filter
1120    }
1121
1122    /// Returns the global test group configuration.
1123    pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
1124        self.test_groups
1125    }
1126
1127    /// Returns the global script configuration.
1128    pub fn script_config(&self) -> &'cfg ScriptConfig {
1129        self.scripts
1130    }
1131
1132    /// Returns the retry policy for this profile.
1133    pub fn retries(&self) -> RetryPolicy {
1134        profile_field!(self.retries)
1135    }
1136
1137    /// Returns the flaky result behavior for this profile.
1138    pub fn flaky_result(&self) -> FlakyResult {
1139        profile_field!(self.flaky_result)
1140    }
1141
1142    /// Returns the number of threads to run against for this profile.
1143    pub fn test_threads(&self) -> TestThreads {
1144        profile_field!(self.test_threads)
1145    }
1146
1147    /// Returns the number of threads required for each test.
1148    pub fn threads_required(&self) -> ThreadsRequired {
1149        profile_field!(self.threads_required)
1150    }
1151
1152    /// Returns extra arguments to be passed to the test binary at runtime.
1153    pub fn run_extra_args(&self) -> &'cfg [String] {
1154        profile_field_from_ref!(self.run_extra_args.as_deref())
1155    }
1156
1157    /// Returns the time after which tests are treated as slow for this profile.
1158    pub fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1159        profile_field!(self.slow_timeout(run_mode))
1160    }
1161
1162    /// Returns the time after which we should stop running tests.
1163    pub fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1164        profile_field!(self.global_timeout(run_mode))
1165    }
1166
1167    /// Returns the time after which a child process that hasn't closed its handles is marked as
1168    /// leaky.
1169    pub fn leak_timeout(&self) -> LeakTimeout {
1170        profile_field!(self.leak_timeout)
1171    }
1172
1173    /// Returns the test status level.
1174    pub fn status_level(&self) -> StatusLevel {
1175        profile_field!(self.status_level)
1176    }
1177
1178    /// Returns the test status level at the end of the run.
1179    pub fn final_status_level(&self) -> FinalStatusLevel {
1180        profile_field!(self.final_status_level)
1181    }
1182
1183    /// Returns the failure output config for this profile.
1184    pub fn failure_output(&self) -> TestOutputDisplay {
1185        profile_field!(self.failure_output)
1186    }
1187
1188    /// Returns the failure output config for this profile.
1189    pub fn success_output(&self) -> TestOutputDisplay {
1190        profile_field!(self.success_output)
1191    }
1192
1193    /// Returns the max-fail config for this profile.
1194    pub fn max_fail(&self) -> MaxFail {
1195        profile_field!(self.max_fail)
1196    }
1197
1198    /// Returns the archive configuration for this profile.
1199    pub fn archive_config(&self) -> &'cfg ArchiveConfig {
1200        profile_field_from_ref!(self.archive.as_ref())
1201    }
1202
1203    /// Returns the list of setup scripts.
1204    pub fn setup_scripts(&self, test_list: &TestList<'_>) -> SetupScripts<'_> {
1205        SetupScripts::new(self, test_list)
1206    }
1207
1208    /// Returns list-time settings for a test binary.
1209    pub fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1210        ListSettings::new(self, query)
1211    }
1212
1213    /// Returns settings for individual tests.
1214    pub fn settings_for(
1215        &self,
1216        run_mode: NextestRunMode,
1217        query: &TestQuery<'_>,
1218    ) -> TestSettings<'_> {
1219        TestSettings::new(self, run_mode, query)
1220    }
1221
1222    /// Returns override settings for individual tests, with sources attached.
1223    pub(crate) fn settings_with_source_for(
1224        &self,
1225        run_mode: NextestRunMode,
1226        query: &TestQuery<'_>,
1227    ) -> TestSettings<'_, SettingSource<'_>> {
1228        TestSettings::new(self, run_mode, query)
1229    }
1230
1231    /// Returns the JUnit configuration for this profile.
1232    pub fn junit(&self) -> Option<JunitConfig<'cfg>> {
1233        let settings = JunitSettings {
1234            path: profile_field_optional!(self.junit.path.as_deref()),
1235            report_name: profile_field_from_ref!(self.junit.report_name.as_deref()),
1236            store_success_output: profile_field!(self.junit.store_success_output),
1237            store_failure_output: profile_field!(self.junit.store_failure_output),
1238            report_skipped: profile_field!(self.junit.report_skipped),
1239            flaky_fail_status: profile_field!(self.junit.flaky_fail_status),
1240        };
1241        JunitConfig::new(self.store_dir(), settings)
1242    }
1243
1244    /// Returns the profile that this profile inherits from.
1245    pub fn inherits(&self) -> Option<&str> {
1246        if let Some(custom_profile) = self.custom_profile {
1247            return custom_profile.inherits();
1248        }
1249        None
1250    }
1251
1252    #[cfg(test)]
1253    pub(in crate::config) fn custom_profile(&self) -> Option<&'cfg CustomProfileImpl> {
1254        self.custom_profile
1255    }
1256}
1257
1258#[derive(Clone, Debug)]
1259pub(in crate::config) struct NextestConfigImpl {
1260    store: StoreConfigImpl,
1261    test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1262    scripts: ScriptConfig,
1263    default_profile: DefaultProfileImpl,
1264    other_profiles: HashMap<String, CustomProfileImpl>,
1265}
1266
1267impl NextestConfigImpl {
1268    fn get_profile(&self, profile: &str) -> Result<Option<&CustomProfileImpl>, ProfileNotFound> {
1269        let custom_profile = match profile {
1270            NextestConfig::DEFAULT_PROFILE => None,
1271            other => Some(
1272                self.other_profiles
1273                    .get(other)
1274                    .ok_or_else(|| ProfileNotFound::new(profile, self.all_profiles()))?,
1275            ),
1276        };
1277        Ok(custom_profile)
1278    }
1279
1280    fn all_profiles(&self) -> impl Iterator<Item = &str> {
1281        self.other_profiles
1282            .keys()
1283            .map(|key| key.as_str())
1284            .chain(std::iter::once(NextestConfig::DEFAULT_PROFILE))
1285    }
1286
1287    pub(in crate::config) fn default_profile(&self) -> &DefaultProfileImpl {
1288        &self.default_profile
1289    }
1290
1291    pub(in crate::config) fn other_profiles(
1292        &self,
1293    ) -> impl Iterator<Item = (&str, &CustomProfileImpl)> {
1294        self.other_profiles
1295            .iter()
1296            .map(|(key, value)| (key.as_str(), value))
1297    }
1298
1299    /// Resolve a profile's inheritance chain (ancestors only, not including the
1300    /// profile itself).
1301    ///
1302    /// Returns the chain ordered from immediate parent to furthest ancestor.
1303    /// Cycles are assumed to have been checked by `sanitize_profile_inherits()`.
1304    fn resolve_inheritance_chain(
1305        &self,
1306        profile_name: &str,
1307    ) -> Result<Vec<(&str, &CustomProfileImpl)>, ProfileNotFound> {
1308        let mut chain = Vec::new();
1309
1310        // Start from the profile's parent, not the profile itself (the profile
1311        // is already available via custom_profile).
1312        let mut curr = self
1313            .get_profile(profile_name)?
1314            .and_then(|p| p.inherits.as_deref());
1315
1316        while let Some(name) = curr {
1317            let profile = self.get_profile(name)?;
1318            if let Some(profile) = profile {
1319                chain.push((name, profile));
1320                curr = profile.inherits.as_deref();
1321            } else {
1322                // Reached the default profile -- stop.
1323                break;
1324            }
1325        }
1326
1327        Ok(chain)
1328    }
1329
1330    /// Sanitize inherits settings on default and custom profiles.
1331    ///
1332    /// `known_profiles` contains profiles from previously loaded (lower priority) files.
1333    /// A profile can inherit from profiles in the same file or in `known_profiles`.
1334    fn sanitize_profile_inherits(
1335        &self,
1336        known_profiles: &BTreeSet<String>,
1337    ) -> Result<(), ConfigParseErrorKind> {
1338        let mut inherit_err_collector = Vec::new();
1339
1340        self.sanitize_default_profile_inherits(&mut inherit_err_collector);
1341        self.sanitize_custom_profile_inherits(&mut inherit_err_collector, known_profiles);
1342
1343        if !inherit_err_collector.is_empty() {
1344            return Err(ConfigParseErrorKind::InheritanceErrors(
1345                inherit_err_collector,
1346            ));
1347        }
1348
1349        Ok(())
1350    }
1351
1352    /// Check the DefaultProfileImpl and make sure that it doesn't inherit from other
1353    /// profiles
1354    fn sanitize_default_profile_inherits(&self, inherit_err_collector: &mut Vec<InheritsError>) {
1355        if self.default_profile().inherits().is_some() {
1356            inherit_err_collector.push(InheritsError::DefaultProfileInheritance(
1357                NextestConfig::DEFAULT_PROFILE.to_string(),
1358            ));
1359        }
1360    }
1361
1362    /// Iterate through each custom profile inherits and report any inheritance error(s).
1363    fn sanitize_custom_profile_inherits(
1364        &self,
1365        inherit_err_collector: &mut Vec<InheritsError>,
1366        known_profiles: &BTreeSet<String>,
1367    ) {
1368        let mut profile_graph = Graph::<&str, (), Directed>::new();
1369        let mut profile_map = HashMap::new();
1370
1371        // Iterate through all custom profiles within the config file and constructs
1372        // a reduced graph of the inheritance chain(s)
1373        for (name, custom_profile) in self.other_profiles() {
1374            let starts_with_default = self.sanitize_custom_default_profile_inherits(
1375                name,
1376                custom_profile,
1377                inherit_err_collector,
1378            );
1379            if !starts_with_default {
1380                // We don't need to add default- profiles. Since they cannot
1381                // have inherits specified on them (they effectively always
1382                // inherit from default), they cannot participate in inheritance
1383                // cycles.
1384                self.add_profile_to_graph(
1385                    name,
1386                    custom_profile,
1387                    &mut profile_map,
1388                    &mut profile_graph,
1389                    inherit_err_collector,
1390                    known_profiles,
1391                );
1392            }
1393        }
1394
1395        self.check_inheritance_cycles(profile_graph, inherit_err_collector);
1396    }
1397
1398    /// Check any CustomProfileImpl that have a "default-" name and make sure they
1399    /// do not inherit from other profiles.
1400    fn sanitize_custom_default_profile_inherits(
1401        &self,
1402        name: &str,
1403        custom_profile: &CustomProfileImpl,
1404        inherit_err_collector: &mut Vec<InheritsError>,
1405    ) -> bool {
1406        let starts_with_default = name.starts_with("default-");
1407
1408        if starts_with_default && custom_profile.inherits().is_some() {
1409            inherit_err_collector.push(InheritsError::DefaultProfileInheritance(name.to_string()));
1410        }
1411
1412        starts_with_default
1413    }
1414
1415    /// Add the custom profile to the profile graph and collect any inheritance errors like
1416    /// self-referential profiles and nonexisting profiles.
1417    ///
1418    /// `known_profiles` contains profiles from previously loaded (lower priority) files.
1419    fn add_profile_to_graph<'cfg>(
1420        &self,
1421        name: &'cfg str,
1422        custom_profile: &'cfg CustomProfileImpl,
1423        profile_map: &mut HashMap<&'cfg str, NodeIndex>,
1424        profile_graph: &mut Graph<&'cfg str, ()>,
1425        inherit_err_collector: &mut Vec<InheritsError>,
1426        known_profiles: &BTreeSet<String>,
1427    ) {
1428        if let Some(inherits_name) = custom_profile.inherits() {
1429            if inherits_name == name {
1430                inherit_err_collector
1431                    .push(InheritsError::SelfReferentialInheritance(name.to_string()))
1432            } else if self.get_profile(inherits_name).is_ok() {
1433                // Inherited profile exists in this file -- create edge for cycle detection.
1434                let from_node = match profile_map.get(name) {
1435                    None => {
1436                        let profile_node = profile_graph.add_node(name);
1437                        profile_map.insert(name, profile_node);
1438                        profile_node
1439                    }
1440                    Some(node_idx) => *node_idx,
1441                };
1442                let to_node = match profile_map.get(inherits_name) {
1443                    None => {
1444                        let profile_node = profile_graph.add_node(inherits_name);
1445                        profile_map.insert(inherits_name, profile_node);
1446                        profile_node
1447                    }
1448                    Some(node_idx) => *node_idx,
1449                };
1450                profile_graph.add_edge(from_node, to_node, ());
1451            } else if known_profiles.contains(inherits_name) {
1452                // Inherited profile exists in a previously loaded file -- valid, no
1453                // cycle detection needed (cross-file cycles are impossible with
1454                // downward-only inheritance).
1455            } else {
1456                inherit_err_collector.push(InheritsError::UnknownInheritance(
1457                    name.to_string(),
1458                    inherits_name.to_string(),
1459                ))
1460            }
1461        }
1462    }
1463
1464    /// Given a profile graph, reports all SCC cycles within the graph using kosaraju algorithm.
1465    fn check_inheritance_cycles(
1466        &self,
1467        profile_graph: Graph<&str, ()>,
1468        inherit_err_collector: &mut Vec<InheritsError>,
1469    ) {
1470        let profile_sccs: Vec<Vec<NodeIndex>> = kosaraju_scc(&profile_graph);
1471        let profile_sccs: Vec<Vec<NodeIndex>> = profile_sccs
1472            .into_iter()
1473            .filter(|scc| scc.len() >= 2)
1474            .collect();
1475
1476        if !profile_sccs.is_empty() {
1477            inherit_err_collector.push(InheritsError::InheritanceCycle(
1478                profile_sccs
1479                    .iter()
1480                    .map(|node_idxs| {
1481                        let profile_names: Vec<String> = node_idxs
1482                            .iter()
1483                            .map(|node_idx| profile_graph[*node_idx].to_string())
1484                            .collect();
1485                        profile_names
1486                    })
1487                    .collect(),
1488            ));
1489        }
1490    }
1491}
1492
1493// This is the form of `NextestConfig` that gets deserialized.
1494//
1495// NOTE: NextestConfigDeserialize doesn't map directly to nextest.toml,
1496//       as some fields are preprocessed with default values.
1497//       Thus, parts of the JSON Schema require customization.
1498#[derive(Clone, Debug, Deserialize)]
1499#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1500#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1501#[serde(rename_all = "kebab-case")]
1502pub(crate) struct NextestConfigDeserialize {
1503    /// Configuration for the nextest store directory.
1504    #[cfg_attr(
1505        feature = "config-schema",
1506        // NOTE: `store` in the JSON Schema should be optional, given the pre-deserialization logic.
1507        schemars(with = "Option<StoreConfigImpl>")
1508    )]
1509    store: StoreConfigImpl,
1510
1511    /// The minimum required (and optionally recommended) version of nextest
1512    /// for this configuration.
1513    // These are parsed as part of NextestConfigVersionOnly. They're re-parsed
1514    // here to avoid printing an "unknown key" message.
1515    #[expect(unused)]
1516    #[serde(default)]
1517    nextest_version: Option<NextestVersionDeserialize>,
1518
1519    /// Enables experimental, non-stable features.
1520    #[expect(unused)]
1521    #[serde(default)]
1522    experimental: ExperimentalDeserialize,
1523
1524    /// Custom test groups for mutual exclusion and resource management, keyed
1525    /// by group name.
1526    #[serde(default)]
1527    test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1528
1529    /// Deprecated location for setup scripts.
1530    ///
1531    /// New configurations should use `[scripts.setup.<name>]` instead.
1532    // Previous version of setup scripts, stored as "script.<name of script>".
1533    #[serde(default, rename = "script")]
1534    old_setup_scripts: IndexMap<ScriptId, SetupScriptConfig>,
1535
1536    /// Setup and wrapper scripts, keyed by script name.
1537    #[serde(default)]
1538    scripts: ScriptConfig,
1539
1540    /// Test profiles, keyed by profile name.
1541    #[serde(rename = "profile")]
1542    #[cfg_attr(
1543        feature = "config-schema",
1544        // NOTE: `profiles` in the JSON Schema should be optional, given the pre-deserialization logic.
1545        schemars(with = "Option<HashMap<String, CustomProfileImpl>>")
1546    )]
1547    profiles: HashMap<String, CustomProfileImpl>,
1548}
1549
1550impl NextestConfigDeserialize {
1551    fn into_config_impl(mut self) -> NextestConfigImpl {
1552        let p = self
1553            .profiles
1554            .remove("default")
1555            .expect("default profile should exist");
1556        let default_profile = DefaultProfileImpl::new(p);
1557
1558        // XXX: This is not quite right (doesn't obey precedence) but is okay
1559        // because it's unlikely folks are using the combination of setup
1560        // scripts *and* tools *and* relying on this. If it breaks, well, this
1561        // feature isn't stable.
1562        for (script_id, script_config) in self.old_setup_scripts {
1563            if let indexmap::map::Entry::Vacant(entry) = self.scripts.setup.entry(script_id) {
1564                entry.insert(script_config);
1565            }
1566        }
1567
1568        NextestConfigImpl {
1569            store: self.store,
1570            default_profile,
1571            test_groups: self.test_groups,
1572            scripts: self.scripts,
1573            other_profiles: self.profiles,
1574        }
1575    }
1576}
1577
1578/// Returns the JSON schema for `.config/nextest.toml`.
1579///
1580/// The schema is intentionally stricter than nextest's runtime parser. Unknown
1581/// fields are warnings at runtime, since this lets older nextest binaries
1582/// continue to load configs written for newer versions. In the schema, however,
1583/// unknown fields are errors so that editors surface them as likely typos. This
1584/// is the reason behind the various `schemars(deny_unknown_fields)` attributes
1585/// and `additionalProperties: false` clauses in the custom `JsonSchema` impls
1586/// across the config module.
1587#[cfg(feature = "config-schema")]
1588pub fn nextest_config_schema() -> schemars::Schema {
1589    let mut schema = schemars::schema_for!(NextestConfigDeserialize);
1590    // This indicates to Tombi that nextest supports TOML 1.1.0.
1591    schema.insert(
1592        "x-tombi-toml-version".to_owned(),
1593        serde_json::Value::String("v1.1.0".to_owned()),
1594    );
1595    schema
1596}
1597
1598#[derive(Clone, Debug, Deserialize)]
1599#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1600#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1601#[serde(rename_all = "kebab-case")]
1602struct StoreConfigImpl {
1603    /// Directory where nextest stores its data.
1604    #[cfg_attr(
1605        feature = "config-schema",
1606        // NOTE: `dir` in the JSON Schema should be optional, given the pre-deserialization logic.
1607        schemars(with = "Option<String>")
1608    )]
1609    dir: Utf8PathBuf,
1610}
1611
1612#[derive(Clone, Debug)]
1613pub(in crate::config) struct DefaultProfileImpl {
1614    test_threads: TestThreads,
1615    threads_required: ThreadsRequired,
1616    run_extra_args: Vec<String>,
1617    retries: RetryPolicy,
1618    flaky_result: FlakyResult,
1619    status_level: StatusLevel,
1620    final_status_level: FinalStatusLevel,
1621    failure_output: TestOutputDisplay,
1622    success_output: TestOutputDisplay,
1623    max_fail: MaxFail,
1624    slow_timeout: SlowTimeout,
1625    global_timeout: GlobalTimeout,
1626    leak_timeout: LeakTimeout,
1627    overrides: Vec<DeserializedOverride>,
1628    scripts: Vec<DeserializedProfileScriptConfig>,
1629    junit: DefaultJunitImpl,
1630    archive: ArchiveConfig,
1631    bench: DefaultBenchConfig,
1632    inherits: Inherits,
1633}
1634
1635impl DefaultProfileImpl {
1636    fn new(p: CustomProfileImpl) -> Self {
1637        Self {
1638            test_threads: p
1639                .test_threads
1640                .expect("test-threads present in default profile"),
1641            threads_required: p
1642                .threads_required
1643                .expect("threads-required present in default profile"),
1644            run_extra_args: p
1645                .run_extra_args
1646                .expect("run-extra-args present in default profile"),
1647            retries: p.retries.expect("retries present in default profile"),
1648            flaky_result: p
1649                .flaky_result
1650                .expect("flaky-result present in default profile"),
1651            status_level: p
1652                .status_level
1653                .expect("status-level present in default profile"),
1654            final_status_level: p
1655                .final_status_level
1656                .expect("final-status-level present in default profile"),
1657            failure_output: p
1658                .failure_output
1659                .expect("failure-output present in default profile"),
1660            success_output: p
1661                .success_output
1662                .expect("success-output present in default profile"),
1663            max_fail: p.max_fail.expect("fail-fast present in default profile"),
1664            slow_timeout: p
1665                .slow_timeout
1666                .expect("slow-timeout present in default profile"),
1667            global_timeout: p
1668                .global_timeout
1669                .expect("global-timeout present in default profile"),
1670            leak_timeout: p
1671                .leak_timeout
1672                .expect("leak-timeout present in default profile"),
1673            overrides: p.overrides,
1674            scripts: p.scripts,
1675            junit: DefaultJunitImpl::for_default_profile(p.junit),
1676            archive: p.archive.expect("archive present in default profile"),
1677            bench: DefaultBenchConfig::for_default_profile(
1678                p.bench.expect("bench present in default profile"),
1679            ),
1680            inherits: Inherits::new(p.inherits),
1681        }
1682    }
1683
1684    pub(in crate::config) fn inherits(&self) -> Option<&str> {
1685        self.inherits.inherits_from()
1686    }
1687
1688    pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1689        &self.overrides
1690    }
1691
1692    pub(in crate::config) fn setup_scripts(&self) -> &[DeserializedProfileScriptConfig] {
1693        &self.scripts
1694    }
1695
1696    pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1697        match run_mode {
1698            NextestRunMode::Test => self.slow_timeout,
1699            NextestRunMode::Benchmark => self.bench.slow_timeout,
1700        }
1701    }
1702
1703    pub(in crate::config) fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1704        match run_mode {
1705            NextestRunMode::Test => self.global_timeout,
1706            NextestRunMode::Benchmark => self.bench.global_timeout,
1707        }
1708    }
1709}
1710
1711#[derive(Clone, Debug, Deserialize)]
1712#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1713#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1714#[serde(rename_all = "kebab-case")]
1715pub(in crate::config) struct CustomProfileImpl {
1716    /// The default set of tests run by `cargo nextest run`, as a filterset
1717    /// expression.
1718    #[serde(default)]
1719    default_filter: Option<String>,
1720    /// Retry policy for failed tests.
1721    #[serde(default, deserialize_with = "deserialize_retry_policy")]
1722    retries: Option<RetryPolicy>,
1723    /// Whether to treat flaky tests as passing or failing.
1724    #[serde(default)]
1725    flaky_result: Option<FlakyResult>,
1726    /// Number of threads to run tests with.
1727    #[serde(default)]
1728    test_threads: Option<TestThreads>,
1729    /// Number of threads (slots) each test reserves from the pool.
1730    #[serde(default)]
1731    threads_required: Option<ThreadsRequired>,
1732    /// Extra arguments to pass to test binaries.
1733    #[serde(default)]
1734    run_extra_args: Option<Vec<String>>,
1735    /// Level of status information to display during test runs.
1736    #[serde(default)]
1737    status_level: Option<StatusLevel>,
1738    /// Level of status information to display in the final summary.
1739    #[serde(default)]
1740    final_status_level: Option<FinalStatusLevel>,
1741    /// When to display output for failed tests.
1742    #[serde(default)]
1743    failure_output: Option<TestOutputDisplay>,
1744    /// When to display output for successful tests.
1745    #[serde(default)]
1746    success_output: Option<TestOutputDisplay>,
1747    /// Controls when to stop running tests after failures.
1748    #[serde(
1749        default,
1750        rename = "fail-fast",
1751        deserialize_with = "deserialize_fail_fast"
1752    )]
1753    max_fail: Option<MaxFail>,
1754    /// Time after which tests are considered slow, plus optional termination
1755    /// policy.
1756    #[serde(default, deserialize_with = "deserialize_slow_timeout")]
1757    slow_timeout: Option<SlowTimeout>,
1758    /// A global timeout for the entire test run.
1759    #[serde(default)]
1760    global_timeout: Option<GlobalTimeout>,
1761    /// Time to wait for child processes to exit after a test completes.
1762    #[serde(default, deserialize_with = "deserialize_leak_timeout")]
1763    leak_timeout: Option<LeakTimeout>,
1764    /// Per-test setting overrides, evaluated in order.
1765    #[serde(default)]
1766    overrides: Vec<DeserializedOverride>,
1767    /// Profile-specific script bindings (setup and wrapper).
1768    #[serde(default)]
1769    scripts: Vec<DeserializedProfileScriptConfig>,
1770    /// JUnit XML output configuration.
1771    #[serde(default)]
1772    junit: JunitImpl,
1773    /// Archive configuration for this profile.
1774    #[serde(default)]
1775    archive: Option<ArchiveConfig>,
1776    /// Benchmark-specific configuration.
1777    #[serde(default)]
1778    bench: Option<BenchConfig>,
1779    /// The profile to inherit settings from.
1780    #[serde(default)]
1781    inherits: Option<String>,
1782}
1783
1784impl CustomProfileImpl {
1785    #[cfg(test)]
1786    pub(in crate::config) fn test_threads(&self) -> Option<TestThreads> {
1787        self.test_threads
1788    }
1789
1790    pub(in crate::config) fn default_filter(&self) -> Option<&str> {
1791        self.default_filter.as_deref()
1792    }
1793
1794    pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> Option<SlowTimeout> {
1795        match run_mode {
1796            NextestRunMode::Test => self.slow_timeout,
1797            NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.slow_timeout),
1798        }
1799    }
1800
1801    pub(in crate::config) fn global_timeout(
1802        &self,
1803        run_mode: NextestRunMode,
1804    ) -> Option<GlobalTimeout> {
1805        match run_mode {
1806            NextestRunMode::Test => self.global_timeout,
1807            NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.global_timeout),
1808        }
1809    }
1810
1811    pub(in crate::config) fn inherits(&self) -> Option<&str> {
1812        self.inherits.as_deref()
1813    }
1814
1815    pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1816        &self.overrides
1817    }
1818
1819    pub(in crate::config) fn scripts(&self) -> &[DeserializedProfileScriptConfig] {
1820        &self.scripts
1821    }
1822}
1823
1824#[cfg(test)]
1825mod tests {
1826    use super::*;
1827    use crate::config::{
1828        core::{ConfigSourceKind, ToolName},
1829        overrides::CompiledDefaultFilterSection,
1830        utils::test_helpers::*,
1831    };
1832    use camino_tempfile::{Utf8TempDir, tempdir};
1833    use guppy::graph::cargo::BuildPlatform;
1834    use iddqd::{IdHashItem, IdHashMap, id_hash_map, id_upcast};
1835    use nextest_filtering::{CompiledExpr, FiltersetKind};
1836    use nextest_metadata::TestCaseName;
1837    use std::time::Duration;
1838    use test_case::test_case;
1839
1840    fn tool_name(s: &str) -> ToolName {
1841        ToolName::new(s.into()).unwrap()
1842    }
1843
1844    /// Test implementation of ConfigWarnings that collects warnings for testing.
1845    #[derive(Default)]
1846    struct TestConfigWarnings {
1847        unknown_keys: IdHashMap<UnknownKeys>,
1848        reserved_profiles: IdHashMap<ReservedProfiles>,
1849        deprecated_scripts: IdHashMap<DeprecatedScripts>,
1850        empty_script_warnings: IdHashMap<EmptyScriptSections>,
1851    }
1852
1853    impl ConfigWarnings for TestConfigWarnings {
1854        fn unknown_config_keys(&mut self, source: &ConfigSource, unknown: &BTreeSet<String>) {
1855            self.unknown_keys
1856                .insert_unique(UnknownKeys {
1857                    kind: source.kind().clone(),
1858                    config_file: source.path().absolute_path().to_owned(),
1859                    keys: unknown.clone(),
1860                })
1861                .expect("each config source reports unknown keys at most once");
1862        }
1863
1864        fn unknown_reserved_profiles(&mut self, source: &ConfigSource, profiles: &[&str]) {
1865            self.reserved_profiles
1866                .insert_unique(ReservedProfiles {
1867                    kind: source.kind().clone(),
1868                    config_file: source.path().absolute_path().to_owned(),
1869                    profiles: profiles.iter().map(|&s| s.to_owned()).collect(),
1870                })
1871                .expect("each config source reports reserved profiles at most once");
1872        }
1873
1874        fn empty_script_sections(
1875            &mut self,
1876            source: &ConfigSource,
1877            profile_name: &str,
1878            empty_count: usize,
1879        ) {
1880            self.empty_script_warnings
1881                .insert_unique(EmptyScriptSections {
1882                    kind: source.kind().clone(),
1883                    config_file: source.path().absolute_path().to_owned(),
1884                    profile_name: profile_name.to_owned(),
1885                    empty_count,
1886                })
1887                .expect(
1888                    "each config source reports a profile's empty script sections at most once",
1889                );
1890        }
1891
1892        fn deprecated_script_config(&mut self, source: &ConfigSource) {
1893            self.deprecated_scripts
1894                .insert_unique(DeprecatedScripts {
1895                    kind: source.kind().clone(),
1896                    config_file: source.path().absolute_path().to_owned(),
1897                })
1898                .expect("each config source reports deprecated script config at most once");
1899        }
1900    }
1901
1902    #[derive(Clone, Debug, PartialEq, Eq)]
1903    struct UnknownKeys {
1904        kind: ConfigSourceKind,
1905        config_file: Utf8PathBuf,
1906        keys: BTreeSet<String>,
1907    }
1908
1909    impl IdHashItem for UnknownKeys {
1910        type Key<'a> = &'a ConfigSourceKind;
1911        fn key(&self) -> Self::Key<'_> {
1912            &self.kind
1913        }
1914        id_upcast!();
1915    }
1916
1917    #[derive(Clone, Debug, PartialEq, Eq)]
1918    struct ReservedProfiles {
1919        kind: ConfigSourceKind,
1920        config_file: Utf8PathBuf,
1921        profiles: Vec<String>,
1922    }
1923
1924    impl IdHashItem for ReservedProfiles {
1925        type Key<'a> = &'a ConfigSourceKind;
1926        fn key(&self) -> Self::Key<'_> {
1927            &self.kind
1928        }
1929        id_upcast!();
1930    }
1931
1932    #[derive(Clone, Debug, PartialEq, Eq)]
1933    struct DeprecatedScripts {
1934        kind: ConfigSourceKind,
1935        config_file: Utf8PathBuf,
1936    }
1937
1938    impl IdHashItem for DeprecatedScripts {
1939        type Key<'a> = &'a ConfigSourceKind;
1940        fn key(&self) -> Self::Key<'_> {
1941            &self.kind
1942        }
1943        id_upcast!();
1944    }
1945
1946    #[derive(Clone, Debug, PartialEq, Eq)]
1947    struct EmptyScriptSections {
1948        kind: ConfigSourceKind,
1949        config_file: Utf8PathBuf,
1950        profile_name: String,
1951        empty_count: usize,
1952    }
1953
1954    impl IdHashItem for EmptyScriptSections {
1955        type Key<'a> = (&'a ConfigSourceKind, &'a str);
1956        fn key(&self) -> Self::Key<'_> {
1957            (&self.kind, &self.profile_name)
1958        }
1959        id_upcast!();
1960    }
1961
1962    #[test]
1963    fn default_config_is_valid() {
1964        let default_config = NextestConfig::default_config("foo");
1965        default_config
1966            .profile(NextestConfig::DEFAULT_PROFILE)
1967            .expect("default profile should exist");
1968    }
1969
1970    #[test]
1971    fn ignored_keys() {
1972        let config_contents = r#"
1973        ignored1 = "test"
1974
1975        [profile.default]
1976        retries = 3
1977        ignored2 = "hi"
1978
1979        [profile.default-foo]
1980        retries = 5
1981
1982        [[profile.default.overrides]]
1983        filter = 'test(test_foo)'
1984        retries = 20
1985        ignored3 = 42
1986        "#;
1987
1988        let tool_config_contents = r#"
1989        [store]
1990        ignored4 = 20
1991
1992        [profile.default]
1993        retries = 4
1994        ignored5 = false
1995
1996        [profile.default-bar]
1997        retries = 5
1998
1999        [profile.tool]
2000        retries = 12
2001
2002        [[profile.tool.overrides]]
2003        filter = 'test(test_baz)'
2004        retries = 22
2005        ignored6 = 6.5
2006        "#;
2007
2008        let workspace_dir = tempdir().unwrap();
2009
2010        let graph = temp_workspace(&workspace_dir, config_contents);
2011        let workspace_root = graph.workspace().root();
2012        let tool_path = workspace_root.join(".config/tool.toml");
2013        std::fs::write(&tool_path, tool_config_contents).unwrap();
2014
2015        let pcx = ParseContext::new(&graph);
2016
2017        let mut warnings = TestConfigWarnings::default();
2018
2019        let _ = NextestConfig::from_sources_with_warnings(
2020            workspace_root,
2021            &pcx,
2022            None,
2023            &[ToolConfigFile {
2024                tool: tool_name("my-tool"),
2025                config_file: tool_path.clone(),
2026            }][..],
2027            &Default::default(),
2028            &mut warnings,
2029        )
2030        .expect("config is valid");
2031
2032        assert_eq!(
2033            warnings.unknown_keys.len(),
2034            2,
2035            "there are two files with unknown keys"
2036        );
2037
2038        assert_eq!(
2039            warnings.unknown_keys,
2040            id_hash_map! {
2041                UnknownKeys {
2042                    kind: ConfigSourceKind::DiscoveredRepository,
2043                    config_file: workspace_root.join(".config/nextest.toml"),
2044                    keys: maplit::btreeset! {
2045                        "ignored1".to_owned(),
2046                        "profile.default.ignored2".to_owned(),
2047                        "profile.default.overrides.0.ignored3".to_owned(),
2048                    }
2049                },
2050                UnknownKeys {
2051                    kind: ConfigSourceKind::Tool(tool_name("my-tool")),
2052                    config_file: tool_path.clone(),
2053                    keys: maplit::btreeset! {
2054                        "store.ignored4".to_owned(),
2055                        "profile.default.ignored5".to_owned(),
2056                        "profile.tool.overrides.0.ignored6".to_owned(),
2057                    }
2058                }
2059            }
2060        );
2061        assert_eq!(
2062            warnings.reserved_profiles,
2063            id_hash_map! {
2064                ReservedProfiles {
2065                    kind: ConfigSourceKind::DiscoveredRepository,
2066                    config_file: workspace_root.join(".config/nextest.toml"),
2067                    profiles: vec!["default-foo".to_owned()],
2068                },
2069                ReservedProfiles {
2070                    kind: ConfigSourceKind::Tool(tool_name("my-tool")),
2071                    config_file: tool_path,
2072                    profiles: vec!["default-bar".to_owned()],
2073                }
2074            },
2075        )
2076    }
2077
2078    #[test]
2079    fn script_warnings() {
2080        let config_contents = r#"
2081        experimental = ["setup-scripts", "wrapper-scripts"]
2082
2083        [scripts.wrapper.script1]
2084        command = "echo test"
2085
2086        [scripts.wrapper.script2]
2087        command = "echo test2"
2088
2089        [scripts.setup.script3]
2090        command = "echo setup"
2091
2092        [[profile.default.scripts]]
2093        filter = 'all()'
2094        # Empty - no setup or wrapper scripts
2095
2096        [[profile.default.scripts]]
2097        filter = 'test(foo)'
2098        setup = ["script3"]
2099
2100        [profile.custom]
2101        [[profile.custom.scripts]]
2102        filter = 'all()'
2103        # Empty - no setup or wrapper scripts
2104
2105        [[profile.custom.scripts]]
2106        filter = 'test(bar)'
2107        # Another empty section
2108        "#;
2109
2110        let tool_config_contents = r#"
2111        experimental = ["setup-scripts", "wrapper-scripts"]
2112
2113        [scripts.wrapper."@tool:tool:disabled_script"]
2114        command = "echo disabled"
2115
2116        [scripts.setup."@tool:tool:setup_script"]
2117        command = "echo setup"
2118
2119        [profile.tool]
2120        [[profile.tool.scripts]]
2121        filter = 'all()'
2122        # Empty section
2123
2124        [[profile.tool.scripts]]
2125        filter = 'test(foo)'
2126        setup = ["@tool:tool:setup_script"]
2127        "#;
2128
2129        let workspace_dir = tempdir().unwrap();
2130        let graph = temp_workspace(&workspace_dir, config_contents);
2131        let workspace_root = graph.workspace().root();
2132        let tool_path = workspace_root.join(".config/tool.toml");
2133        std::fs::write(&tool_path, tool_config_contents).unwrap();
2134
2135        let pcx = ParseContext::new(&graph);
2136
2137        let mut warnings = TestConfigWarnings::default();
2138
2139        let experimental = maplit::btreeset! {
2140            ConfigExperimental::SetupScripts,
2141            ConfigExperimental::WrapperScripts
2142        };
2143        let _ = NextestConfig::from_sources_with_warnings(
2144            workspace_root,
2145            &pcx,
2146            None,
2147            &[ToolConfigFile {
2148                tool: tool_name("tool"),
2149                config_file: tool_path.clone(),
2150            }][..],
2151            &experimental,
2152            &mut warnings,
2153        )
2154        .expect("config is valid");
2155
2156        assert_eq!(
2157            warnings.empty_script_warnings,
2158            id_hash_map! {
2159                EmptyScriptSections {
2160                    kind: ConfigSourceKind::DiscoveredRepository,
2161                    config_file: workspace_root.join(".config/nextest.toml"),
2162                    profile_name: "default".to_owned(),
2163                    empty_count: 1,
2164                },
2165                EmptyScriptSections {
2166                    kind: ConfigSourceKind::DiscoveredRepository,
2167                    config_file: workspace_root.join(".config/nextest.toml"),
2168                    profile_name: "custom".to_owned(),
2169                    empty_count: 2,
2170                },
2171                EmptyScriptSections {
2172                    kind: ConfigSourceKind::Tool(tool_name("tool")),
2173                    config_file: tool_path,
2174                    profile_name: "tool".to_owned(),
2175                    empty_count: 1,
2176                }
2177            }
2178        );
2179    }
2180
2181    #[test]
2182    fn deprecated_script_config_warning() {
2183        let config_contents = r#"
2184        experimental = ["setup-scripts"]
2185
2186        [script.my-script]
2187        command = "echo hello"
2188"#;
2189
2190        let tool_config_contents = r#"
2191        experimental = ["setup-scripts"]
2192
2193        [script."@tool:my-tool:my-script"]
2194        command = "echo hello"
2195"#;
2196
2197        let temp_dir = tempdir().unwrap();
2198
2199        let graph = temp_workspace(&temp_dir, config_contents);
2200        let workspace_root = graph.workspace().root();
2201        let tool_path = workspace_root.join(".config/my-tool.toml");
2202        std::fs::write(&tool_path, tool_config_contents).unwrap();
2203        let pcx = ParseContext::new(&graph);
2204
2205        let mut warnings = TestConfigWarnings::default();
2206        NextestConfig::from_sources_with_warnings(
2207            graph.workspace().root(),
2208            &pcx,
2209            None,
2210            &[ToolConfigFile {
2211                tool: tool_name("my-tool"),
2212                config_file: tool_path.clone(),
2213            }],
2214            &maplit::btreeset! {ConfigExperimental::SetupScripts},
2215            &mut warnings,
2216        )
2217        .expect("config is valid");
2218
2219        assert_eq!(
2220            warnings.deprecated_scripts,
2221            id_hash_map! {
2222                DeprecatedScripts {
2223                    kind: ConfigSourceKind::DiscoveredRepository,
2224                    config_file: graph.workspace().root().join(".config/nextest.toml"),
2225                },
2226                DeprecatedScripts {
2227                    kind: ConfigSourceKind::Tool(tool_name("my-tool")),
2228                    config_file: tool_path,
2229                }
2230            }
2231        );
2232    }
2233
2234    #[test]
2235    fn inherited_profiles_contribute_compiled_data() {
2236        let dir = tempdir().unwrap();
2237        let graph = temp_workspace(
2238            &dir,
2239            r#"
2240            [scripts.setup.prepare]
2241            command = "echo prepare"
2242
2243            [scripts.wrapper.grandparent-wrapper]
2244            command = "grandparent-wrapper"
2245
2246            [scripts.wrapper.parent-wrapper]
2247            command = "parent-wrapper"
2248
2249            [[profile.default.overrides]]
2250            filter = "all()"
2251            retries = 3
2252            slow-timeout = "10s"
2253
2254            [profile.grandparent]
2255
2256            [[profile.grandparent.overrides]]
2257            filter = "test(parent)"
2258            retries = 13
2259            slow-timeout = "30s"
2260
2261            [[profile.grandparent.scripts]]
2262            filter = "all()"
2263            run-wrapper = "grandparent-wrapper"
2264
2265            [profile.parent]
2266            inherits = "grandparent"
2267            default-filter = "test(parent)"
2268
2269            [[profile.parent.overrides]]
2270            filter = "test(parent)"
2271            retries = 8
2272            slow-timeout = "40s"
2273
2274            [[profile.parent.scripts]]
2275            filter = "all()"
2276            setup = ["prepare"]
2277
2278            [[profile.parent.scripts]]
2279            filter = "test(parent)"
2280            run-wrapper = "parent-wrapper"
2281
2282            [profile.child]
2283            inherits = "parent"
2284
2285            [[profile.child.overrides]]
2286            filter = "test(child)"
2287            retries = 5
2288        "#,
2289        );
2290        let pcx = ParseContext::new(&graph);
2291        let config = NextestConfig::from_sources(
2292            dir.path(),
2293            &pcx,
2294            None,
2295            &[],
2296            &maplit::btreeset! {
2297                ConfigExperimental::SetupScripts,
2298                ConfigExperimental::WrapperScripts,
2299            },
2300        )
2301        .unwrap();
2302        let profile = config
2303            .profile("child")
2304            .unwrap()
2305            .apply_build_platforms(&build_platforms());
2306
2307        let override_profiles: Vec<_> = profile
2308            .compiled_data
2309            .overrides
2310            .iter()
2311            .map(|override_| override_.id().profile_name.as_str())
2312            .collect();
2313        assert_eq!(
2314            override_profiles,
2315            ["child", "parent", "grandparent", "default"],
2316            "overrides are ordered from the profile itself to its furthest ancestor"
2317        );
2318        assert_eq!(
2319            profile.default_filter().profile,
2320            "parent",
2321            "default-filter is inherited from the nearest ancestor that sets it"
2322        );
2323
2324        let package_id = graph.workspace().iter().next().unwrap().id();
2325        let binary = binary_query(
2326            &graph,
2327            package_id,
2328            "lib",
2329            "test-package",
2330            BuildPlatform::Target,
2331        );
2332        for (name, retries, timeout, wrapper) in [
2333            ("child_only", 5, 10, Some("grandparent-wrapper")),
2334            ("parent_only", 8, 40, Some("parent-wrapper")),
2335            ("parent_and_child", 5, 40, Some("parent-wrapper")),
2336            ("other", 3, 10, Some("grandparent-wrapper")),
2337        ] {
2338            let test_name = TestCaseName::new(name);
2339            let query = TestQuery {
2340                binary_query: binary.to_query(),
2341                test_name: &test_name,
2342            };
2343            let settings = profile.settings_for(NextestRunMode::Test, &query);
2344            assert_eq!(settings.retries().count(), retries, "retries for {name}");
2345            assert_eq!(
2346                settings.slow_timeout().period,
2347                Duration::from_secs(timeout),
2348                "slow timeout for {name}"
2349            );
2350            assert_eq!(
2351                settings
2352                    .run_wrapper()
2353                    .map(|wrapper| wrapper.command.program.as_str()),
2354                wrapper,
2355                "run wrapper for {name}"
2356            );
2357        }
2358
2359        let setup_scripts: Vec<_> = profile
2360            .compiled_data
2361            .scripts
2362            .iter()
2363            .flat_map(|scripts| scripts.setup.iter().cloned())
2364            .collect();
2365        assert_eq!(
2366            setup_scripts,
2367            [ScriptId::new("prepare".into()).unwrap()],
2368            "child profile inherits the parent profile's setup script selection"
2369        );
2370    }
2371
2372    #[test]
2373    fn cross_file_inheritance_cycle_is_rejected() {
2374        let workspace_dir = tempdir().unwrap();
2375        let graph = temp_workspace(&workspace_dir, "");
2376        let workspace_root = graph.workspace().root();
2377
2378        // Each file on its own is acyclic -- the cycle only exists once the
2379        // higher-priority file redefines `a`.
2380        let lower_path = workspace_root.join(".config/lower.toml");
2381        std::fs::write(
2382            &lower_path,
2383            "[profile.a]\nretries = 1\n[profile.b]\ninherits = 'a'\n",
2384        )
2385        .unwrap();
2386        let upper_path = workspace_root.join(".config/upper.toml");
2387        std::fs::write(&upper_path, "[profile.a]\ninherits = 'b'\n").unwrap();
2388
2389        // Tool files are processed in reverse array order, so the redefining
2390        // file comes first here to be merged last.
2391        let error = NextestConfig::from_sources(
2392            workspace_root,
2393            &ParseContext::new(&graph),
2394            None,
2395            &[
2396                ToolConfigFile {
2397                    tool: tool_name("upper"),
2398                    config_file: upper_path,
2399                },
2400                ToolConfigFile {
2401                    tool: tool_name("lower"),
2402                    config_file: lower_path,
2403                },
2404            ][..],
2405            &Default::default(),
2406        )
2407        .expect_err("a cycle spanning config files is rejected");
2408
2409        let ConfigParseErrorKind::InheritanceErrors(errors) = error.kind() else {
2410            panic!("expected inheritance errors, got {error:?}");
2411        };
2412        assert!(
2413            errors
2414                .iter()
2415                .any(|error| matches!(error, InheritsError::InheritanceCycle(_))),
2416            "{errors:?}"
2417        );
2418    }
2419
2420    fn tool_config_file(
2421        dir: &Utf8TempDir,
2422        file_name: &str,
2423        tool: &str,
2424        contents: &str,
2425    ) -> ToolConfigFile {
2426        let config_file = dir.path().join(file_name);
2427        std::fs::write(&config_file, contents).expect("wrote the tool config file");
2428        ToolConfigFile {
2429            tool: tool_name(tool),
2430            config_file,
2431        }
2432    }
2433
2434    #[test]
2435    fn unknown_key_warnings_only_include_fields_the_file_defines() {
2436        let dir = tempdir().unwrap();
2437        let graph = temp_workspace(
2438            &dir,
2439            "[profile.ci]\ntest-threads = 2\nrepo-typo = true\nshared-typo = true",
2440        );
2441        let root = graph.workspace().root();
2442        // We must produce a warning for:
2443        //
2444        // * tool file with tool-typo
2445        // * tool file with shared-typo
2446        // * repository file with repo-typo
2447        // * repository file with shared-typo
2448        //
2449        // We must NOT warn about repository file with tool-typo. This is true
2450        // today because when we deserialize a config file, we do so against the
2451        // default config, not in a layered fashion.
2452        let tool = tool_config_file(
2453            &dir,
2454            "tool.toml",
2455            "t",
2456            "[profile.ci]\nretries = 1\ntool-typo = true\nshared-typo = true",
2457        );
2458        let tool_path = tool.config_file.clone();
2459
2460        let mut warnings = TestConfigWarnings::default();
2461        NextestConfig::from_sources_with_warnings(
2462            root,
2463            &ParseContext::new(&graph),
2464            None,
2465            &[tool],
2466            &BTreeSet::new(),
2467            &mut warnings,
2468        )
2469        .expect("config is valid");
2470
2471        assert_eq!(
2472            warnings.unknown_keys,
2473            id_hash_map! {
2474                UnknownKeys {
2475                    kind: ConfigSourceKind::DiscoveredRepository,
2476                    config_file: root.join(NextestConfig::CONFIG_PATH),
2477                    keys: maplit::btreeset! {
2478                        "profile.ci.repo-typo".to_owned(),
2479                        "profile.ci.shared-typo".to_owned(),
2480                    },
2481                },
2482                UnknownKeys {
2483                    kind: ConfigSourceKind::Tool(tool_name("t")),
2484                    config_file: tool_path,
2485                    keys: maplit::btreeset! {
2486                        "profile.ci.tool-typo".to_owned(),
2487                        "profile.ci.shared-typo".to_owned(),
2488                    },
2489                },
2490            },
2491            "each file is warned about exactly the unknown keys it wrote"
2492        );
2493    }
2494
2495    #[test_case("", Some("test(tool)"), "test(tool)"; "repo config omits the default profile")]
2496    #[test_case("[profile.default]\nretries = 1", Some("test(tool)"), "test(tool)"; "repo config sets no default filter")]
2497    #[test_case("[profile.default]\ndefault-filter = 'test(repo)'", Some("test(tool)"), "test(repo)"; "repo config sets a default filter")]
2498    // With no file setting a default filter, the value can only come from
2499    // CompiledByProfile::for_default_config.
2500    #[test_case("[profile.default]\nretries = 1", None, "all()"; "no file sets a default filter")]
2501    fn default_filter_precedence_between_tool_and_repo_configs(
2502        repo_contents: &str,
2503        tool_filter: Option<&str>,
2504        expected_filter: &str,
2505    ) {
2506        let dir = tempdir().unwrap();
2507        let graph = temp_workspace(&dir, repo_contents);
2508        let tool_config_files: Vec<_> = tool_filter
2509            .map(|filter| {
2510                tool_config_file(
2511                    &dir,
2512                    "tool.toml",
2513                    "t",
2514                    &format!("[profile.default]\ndefault-filter = '{filter}'"),
2515                )
2516            })
2517            .into_iter()
2518            .collect();
2519        let pcx = ParseContext::new(&graph);
2520        let config = NextestConfig::from_sources(
2521            graph.workspace().root(),
2522            &pcx,
2523            None,
2524            &tool_config_files,
2525            &BTreeSet::new(),
2526        )
2527        .unwrap();
2528        let profile = config
2529            .profile(NextestConfig::DEFAULT_PROFILE)
2530            .expect("default profile exists")
2531            .apply_build_platforms(&build_platforms());
2532        let expected_expr = Filterset::parse(
2533            expected_filter.to_owned(),
2534            &pcx,
2535            FiltersetKind::DefaultFilter,
2536            &KnownGroups::Unavailable,
2537        )
2538        .expect("expected filter parses")
2539        .compiled;
2540        assert_eq!(profile.default_filter().expr, expected_expr);
2541        assert_eq!(
2542            profile.default_filter().profile,
2543            NextestConfig::DEFAULT_PROFILE
2544        );
2545        assert!(
2546            matches!(
2547                profile.default_filter().section,
2548                CompiledDefaultFilterSection::Profile
2549            ),
2550            "{:?}",
2551            profile.default_filter().section
2552        );
2553    }
2554
2555    #[test]
2556    fn default_config_sets_default_filter_only_on_the_default_profile() {
2557        let default_config = NextestConfig::default_config("foo");
2558
2559        let built_in = NextestConfig::make_default_config()
2560            .build()
2561            .expect("the built-in config builds");
2562        let filter = built_in
2563            .get::<String>(DEFAULT_PROFILE_DEFAULT_FILTER_KEY)
2564            .expect("the built-in default profile sets a default-filter");
2565
2566        // The default filter defined in configuration must agree with
2567        // CompiledDefaultFilter::for_default_config, which hardcodes
2568        // CompiledExpr::ALL. (Why we don't just use the default filter is
2569        // complicated, having to do with not passing around a PackageGraph
2570        // unless necessary.)
2571        let dir = tempdir().unwrap();
2572        let graph = temp_workspace(&dir, "");
2573        let compiled = Filterset::parse(
2574            filter,
2575            &ParseContext::new(&graph),
2576            FiltersetKind::DefaultFilter,
2577            &KnownGroups::Unavailable,
2578        )
2579        .expect("the built-in default-filter parses")
2580        .compiled;
2581        assert_eq!(compiled, CompiledExpr::ALL);
2582
2583        let names: Vec<&str> = default_config
2584            .inner
2585            .other_profiles()
2586            .map(|(name, _)| name)
2587            .collect();
2588        assert_eq!(names, ["default-miri"]);
2589        for (name, profile) in default_config.inner.other_profiles() {
2590            assert_eq!(
2591                profile.default_filter(),
2592                None,
2593                "built-in profile {name} sets no default-filter"
2594            );
2595        }
2596    }
2597}