1use 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
56pub trait ConfigWarnings {
61 fn unknown_config_keys(&mut self, source: &ConfigSource, unknown: &BTreeSet<String>);
63
64 fn unknown_reserved_profiles(&mut self, source: &ConfigSource, profiles: &[&str]);
66
67 fn deprecated_script_config(&mut self, source: &ConfigSource);
69
70 fn empty_script_sections(
73 &mut self,
74 source: &ConfigSource,
75 profile_name: &str,
76 empty_count: usize,
77 );
78}
79
80#[derive(Clone, Debug, Default)]
83pub struct DefaultConfigWarnings {
84 styles: ConfigStyles,
85}
86
87impl DefaultConfigWarnings {
88 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 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#[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#[derive(Clone, Debug)]
181pub struct NextestConfig {
182 workspace_root: Utf8PathBuf,
183 inner: NextestConfigImpl,
184 compiled: CompiledByProfile,
185}
186
187const DEFAULT_PROFILE_DEFAULT_FILTER_KEY: &str = "profile.default.default-filter";
189
190impl NextestConfig {
191 pub const CONFIG_PATH: &'static str = ".config/nextest.toml";
194
195 pub const DEFAULT_CONFIG: &'static str = include_str!("../../../default-config.toml");
199
200 pub const REFERENCE_MD: &'static str = include_str!("../../../repo-config-reference.md");
202
203 pub const SCHEMA: &'static str = include_str!("../../../jsonschemas/repo-config.json");
210
211 pub const ENVIRONMENT_PREFIX: &'static str = "NEXTEST";
213
214 pub const DEFAULT_PROFILE: &'static str = "default";
216
217 pub const DEFAULT_MIRI_PROFILE: &'static str = "default-miri";
219
220 pub const DEFAULT_PROFILES: &'static [&'static str] =
222 &[Self::DEFAULT_PROFILE, Self::DEFAULT_MIRI_PROFILE];
223
224 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 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 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 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 #[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 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 compiled: CompiledByProfile::for_default_config(),
362 }
363 }
364
365 pub fn profile(&self, name: impl AsRef<str>) -> Result<EarlyProfile<'_>, ProfileNotFound> {
368 self.make_profile(name.as_ref())
369 }
370
371 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 let mut composite_builder = Self::make_default_config();
385
386 let mut compiled = CompiledByProfile::for_default_config();
389
390 let mut known_groups = BTreeSet::new();
391 let mut known_scripts = IdOrdMap::new();
392 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 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 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 config
435 .sanitize_profile_inherits(&BTreeSet::new())
436 .map_err(|kind| ConfigParseError::from_path(&config_file, kind))?;
437
438 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 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 let (valid_groups, invalid_groups): (BTreeSet<_>, _) =
479 this_config.test_groups.keys().cloned().partition(|group| {
480 if let Some(tool) = tool {
481 group
483 .as_identifier()
484 .tool_components()
485 .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
486 } else {
487 !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 !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 !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 {
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 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 script
552 .as_identifier()
553 .tool_components()
554 .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
555 } else {
556 !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 this_config
590 .sanitize_profile_inherits(known_profiles)
591 .map_err(|kind| ConfigParseError::new(source, kind))?;
592
593 known_profiles.extend(
595 this_config
596 .other_profiles()
597 .map(|(name, _)| name.to_owned()),
598 );
599
600 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 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 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 !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 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 !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 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 data.reverse();
801 entry.insert(data);
802 }
803 hash_map::Entry::Occupied(mut entry) => {
804 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 let inheritance_chain = self.inner.resolve_inheritance_chain(name)?;
822
823 let mut store_dir = self.workspace_root.join(&self.inner.store.dir);
825 store_dir.push(name);
826
827 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 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 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#[derive(Clone, Debug, Default)]
889pub(in crate::config) struct PreBuildPlatform {}
890
891#[derive(Clone, Debug)]
893pub(crate) struct FinalConfig {
894 pub(in crate::config) host_eval: bool,
896 pub(in crate::config) host_test_eval: bool,
899 pub(in crate::config) target_eval: bool,
902}
903
904pub 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 scripts: &'cfg ScriptConfig,
917 pub(in crate::config) compiled_data: CompiledData<PreBuildPlatform>,
919}
920
921macro_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 ($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}
968macro_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 pub fn store_dir(&self) -> &Utf8Path {
983 &self.store_dir
984 }
985
986 pub fn has_junit(&self) -> bool {
988 profile_field_optional!(self.junit.path.as_deref()).is_some()
989 }
990
991 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
993 self.test_groups
994 }
995
996 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 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 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 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#[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 scripts: &'cfg ScriptConfig,
1063 pub(in crate::config) compiled_data: CompiledData<FinalConfig>,
1065 resolved_default_filter: CompiledDefaultFilter,
1068}
1069
1070impl<'cfg> EvaluatableProfile<'cfg> {
1071 pub fn name(&self) -> &str {
1073 &self.name
1074 }
1075
1076 pub fn store_dir(&self) -> &Utf8Path {
1078 &self.store_dir
1079 }
1080
1081 pub fn filterset_ecx(&self) -> EvalContext<'_> {
1083 EvalContext {
1084 default_filter: &self.default_filter().expr,
1085 }
1086 }
1087
1088 pub fn precompute_group_memberships<'a>(
1096 &self,
1097 tests: impl Iterator<Item = TestQuery<'a>>,
1098 ) -> PrecomputedGroupMembership {
1099 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 pub fn default_filter(&self) -> &CompiledDefaultFilter {
1119 &self.resolved_default_filter
1120 }
1121
1122 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
1124 self.test_groups
1125 }
1126
1127 pub fn script_config(&self) -> &'cfg ScriptConfig {
1129 self.scripts
1130 }
1131
1132 pub fn retries(&self) -> RetryPolicy {
1134 profile_field!(self.retries)
1135 }
1136
1137 pub fn flaky_result(&self) -> FlakyResult {
1139 profile_field!(self.flaky_result)
1140 }
1141
1142 pub fn test_threads(&self) -> TestThreads {
1144 profile_field!(self.test_threads)
1145 }
1146
1147 pub fn threads_required(&self) -> ThreadsRequired {
1149 profile_field!(self.threads_required)
1150 }
1151
1152 pub fn run_extra_args(&self) -> &'cfg [String] {
1154 profile_field_from_ref!(self.run_extra_args.as_deref())
1155 }
1156
1157 pub fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1159 profile_field!(self.slow_timeout(run_mode))
1160 }
1161
1162 pub fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1164 profile_field!(self.global_timeout(run_mode))
1165 }
1166
1167 pub fn leak_timeout(&self) -> LeakTimeout {
1170 profile_field!(self.leak_timeout)
1171 }
1172
1173 pub fn status_level(&self) -> StatusLevel {
1175 profile_field!(self.status_level)
1176 }
1177
1178 pub fn final_status_level(&self) -> FinalStatusLevel {
1180 profile_field!(self.final_status_level)
1181 }
1182
1183 pub fn failure_output(&self) -> TestOutputDisplay {
1185 profile_field!(self.failure_output)
1186 }
1187
1188 pub fn success_output(&self) -> TestOutputDisplay {
1190 profile_field!(self.success_output)
1191 }
1192
1193 pub fn max_fail(&self) -> MaxFail {
1195 profile_field!(self.max_fail)
1196 }
1197
1198 pub fn archive_config(&self) -> &'cfg ArchiveConfig {
1200 profile_field_from_ref!(self.archive.as_ref())
1201 }
1202
1203 pub fn setup_scripts(&self, test_list: &TestList<'_>) -> SetupScripts<'_> {
1205 SetupScripts::new(self, test_list)
1206 }
1207
1208 pub fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1210 ListSettings::new(self, query)
1211 }
1212
1213 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 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 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 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 fn resolve_inheritance_chain(
1305 &self,
1306 profile_name: &str,
1307 ) -> Result<Vec<(&str, &CustomProfileImpl)>, ProfileNotFound> {
1308 let mut chain = Vec::new();
1309
1310 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 break;
1324 }
1325 }
1326
1327 Ok(chain)
1328 }
1329
1330 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 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 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 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 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 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 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 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 } else {
1456 inherit_err_collector.push(InheritsError::UnknownInheritance(
1457 name.to_string(),
1458 inherits_name.to_string(),
1459 ))
1460 }
1461 }
1462 }
1463
1464 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#[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 #[cfg_attr(
1505 feature = "config-schema",
1506 schemars(with = "Option<StoreConfigImpl>")
1508 )]
1509 store: StoreConfigImpl,
1510
1511 #[expect(unused)]
1516 #[serde(default)]
1517 nextest_version: Option<NextestVersionDeserialize>,
1518
1519 #[expect(unused)]
1521 #[serde(default)]
1522 experimental: ExperimentalDeserialize,
1523
1524 #[serde(default)]
1527 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1528
1529 #[serde(default, rename = "script")]
1534 old_setup_scripts: IndexMap<ScriptId, SetupScriptConfig>,
1535
1536 #[serde(default)]
1538 scripts: ScriptConfig,
1539
1540 #[serde(rename = "profile")]
1542 #[cfg_attr(
1543 feature = "config-schema",
1544 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 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#[cfg(feature = "config-schema")]
1588pub fn nextest_config_schema() -> schemars::Schema {
1589 let mut schema = schemars::schema_for!(NextestConfigDeserialize);
1590 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 #[cfg_attr(
1605 feature = "config-schema",
1606 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 #[serde(default)]
1719 default_filter: Option<String>,
1720 #[serde(default, deserialize_with = "deserialize_retry_policy")]
1722 retries: Option<RetryPolicy>,
1723 #[serde(default)]
1725 flaky_result: Option<FlakyResult>,
1726 #[serde(default)]
1728 test_threads: Option<TestThreads>,
1729 #[serde(default)]
1731 threads_required: Option<ThreadsRequired>,
1732 #[serde(default)]
1734 run_extra_args: Option<Vec<String>>,
1735 #[serde(default)]
1737 status_level: Option<StatusLevel>,
1738 #[serde(default)]
1740 final_status_level: Option<FinalStatusLevel>,
1741 #[serde(default)]
1743 failure_output: Option<TestOutputDisplay>,
1744 #[serde(default)]
1746 success_output: Option<TestOutputDisplay>,
1747 #[serde(
1749 default,
1750 rename = "fail-fast",
1751 deserialize_with = "deserialize_fail_fast"
1752 )]
1753 max_fail: Option<MaxFail>,
1754 #[serde(default, deserialize_with = "deserialize_slow_timeout")]
1757 slow_timeout: Option<SlowTimeout>,
1758 #[serde(default)]
1760 global_timeout: Option<GlobalTimeout>,
1761 #[serde(default, deserialize_with = "deserialize_leak_timeout")]
1763 leak_timeout: Option<LeakTimeout>,
1764 #[serde(default)]
1766 overrides: Vec<DeserializedOverride>,
1767 #[serde(default)]
1769 scripts: Vec<DeserializedProfileScriptConfig>,
1770 #[serde(default)]
1772 junit: JunitImpl,
1773 #[serde(default)]
1775 archive: Option<ArchiveConfig>,
1776 #[serde(default)]
1778 bench: Option<BenchConfig>,
1779 #[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 #[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 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 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 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 #[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 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}