1use super::{ExperimentalDeserialize, NextestVersionDeserialize, ToolConfigFile, ToolName};
5use crate::{
6 config::{
7 core::ConfigExperimental,
8 elements::{
9 ArchiveConfig, BenchConfig, CustomTestGroup, DefaultBenchConfig, DefaultJunitImpl,
10 FlakyResult, GlobalTimeout, Inherits, JunitConfig, JunitImpl, JunitSettings,
11 LeakTimeout, MaxFail, RetryPolicy, SlowTimeout, TestGroup, TestGroupConfig,
12 TestThreads, ThreadsRequired, deserialize_fail_fast, deserialize_leak_timeout,
13 deserialize_retry_policy, deserialize_slow_timeout,
14 },
15 overrides::{
16 CompiledByProfile, CompiledData, CompiledDefaultFilter, DeserializedOverride,
17 ListSettings, SettingSource, TestSettings,
18 group_membership::PrecomputedGroupMembership,
19 },
20 scripts::{
21 DeserializedProfileScriptConfig, ProfileScriptType, ScriptConfig, ScriptId, ScriptInfo,
22 SetupScriptConfig, SetupScripts,
23 },
24 },
25 errors::{
26 ConfigParseError, ConfigParseErrorKind, InheritsError,
27 ProfileListScriptUsesRunFiltersError, ProfileNotFound, ProfileScriptErrors,
28 ProfileUnknownScriptError, ProfileWrongConfigScriptTypeError, UnknownTestGroupError,
29 provided_by_tool,
30 },
31 helpers::plural,
32 list::{TestInstanceId, TestList},
33 platform::BuildPlatforms,
34 reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplay},
35 run_mode::NextestRunMode,
36};
37use camino::{Utf8Path, Utf8PathBuf};
38use config::{
39 Config, ConfigBuilder, ConfigError, File, FileFormat, FileSourceFile, builder::DefaultState,
40};
41use iddqd::IdOrdMap;
42use indexmap::IndexMap;
43use nextest_filtering::{
44 BinaryQuery, EvalContext, Filterset, KnownGroups, ParseContext, TestQuery,
45};
46use petgraph::{Directed, Graph, algo::scc::kosaraju_scc, graph::NodeIndex};
47use serde::Deserialize;
48use std::{
49 collections::{BTreeMap, BTreeSet, HashMap, hash_map},
50 sync::LazyLock,
51};
52use tracing::warn;
53
54pub trait ConfigWarnings {
59 fn unknown_config_keys(
61 &mut self,
62 config_file: &Utf8Path,
63 workspace_root: &Utf8Path,
64 tool: Option<&ToolName>,
65 unknown: &BTreeSet<String>,
66 );
67
68 fn unknown_reserved_profiles(
70 &mut self,
71 config_file: &Utf8Path,
72 workspace_root: &Utf8Path,
73 tool: Option<&ToolName>,
74 profiles: &[&str],
75 );
76
77 fn deprecated_script_config(
79 &mut self,
80 config_file: &Utf8Path,
81 workspace_root: &Utf8Path,
82 tool: Option<&ToolName>,
83 );
84
85 fn empty_script_sections(
88 &mut self,
89 config_file: &Utf8Path,
90 workspace_root: &Utf8Path,
91 tool: Option<&ToolName>,
92 profile_name: &str,
93 empty_count: usize,
94 );
95}
96
97pub struct DefaultConfigWarnings;
99
100impl ConfigWarnings for DefaultConfigWarnings {
101 fn unknown_config_keys(
102 &mut self,
103 config_file: &Utf8Path,
104 workspace_root: &Utf8Path,
105 tool: Option<&ToolName>,
106 unknown: &BTreeSet<String>,
107 ) {
108 let mut unknown_str = String::new();
109 if unknown.len() == 1 {
110 unknown_str.push_str("key: ");
112 unknown_str.push_str(unknown.iter().next().unwrap());
113 } else {
114 unknown_str.push_str("keys:\n");
115 for ignored_key in unknown {
116 unknown_str.push('\n');
117 unknown_str.push_str(" - ");
118 unknown_str.push_str(ignored_key);
119 }
120 }
121
122 warn!(
123 "in config file {}{}, ignoring unknown configuration {unknown_str}",
124 config_file
125 .strip_prefix(workspace_root)
126 .unwrap_or(config_file),
127 provided_by_tool(tool),
128 )
129 }
130
131 fn unknown_reserved_profiles(
132 &mut self,
133 config_file: &Utf8Path,
134 workspace_root: &Utf8Path,
135 tool: Option<&ToolName>,
136 profiles: &[&str],
137 ) {
138 warn!(
139 "in config file {}{}, ignoring unknown profiles in the reserved `default-` namespace:",
140 config_file
141 .strip_prefix(workspace_root)
142 .unwrap_or(config_file),
143 provided_by_tool(tool),
144 );
145
146 for profile in profiles {
147 warn!(" {profile}");
148 }
149 }
150
151 fn deprecated_script_config(
152 &mut self,
153 config_file: &Utf8Path,
154 workspace_root: &Utf8Path,
155 tool: Option<&ToolName>,
156 ) {
157 warn!(
158 "in config file {}{}, [script.*] is deprecated and will be removed in a \
159 future version of nextest; use the `scripts.setup` table instead",
160 config_file
161 .strip_prefix(workspace_root)
162 .unwrap_or(config_file),
163 provided_by_tool(tool),
164 );
165 }
166
167 fn empty_script_sections(
168 &mut self,
169 config_file: &Utf8Path,
170 workspace_root: &Utf8Path,
171 tool: Option<&ToolName>,
172 profile_name: &str,
173 empty_count: usize,
174 ) {
175 warn!(
176 "in config file {}{}, [[profile.{}.scripts]] has {} {} \
177 with neither setup nor wrapper scripts",
178 config_file
179 .strip_prefix(workspace_root)
180 .unwrap_or(config_file),
181 provided_by_tool(tool),
182 profile_name,
183 empty_count,
184 plural::sections_str(empty_count),
185 );
186 }
187}
188
189#[inline]
191pub fn get_num_cpus() -> usize {
192 static NUM_CPUS: LazyLock<usize> =
193 LazyLock::new(|| match std::thread::available_parallelism() {
194 Ok(count) => count.into(),
195 Err(err) => {
196 warn!("unable to determine num-cpus ({err}), assuming 1 logical CPU");
197 1
198 }
199 });
200
201 *NUM_CPUS
202}
203
204#[derive(Clone, Debug)]
213pub struct NextestConfig {
214 workspace_root: Utf8PathBuf,
215 inner: NextestConfigImpl,
216 compiled: CompiledByProfile,
217}
218
219impl NextestConfig {
220 pub const CONFIG_PATH: &'static str = ".config/nextest.toml";
223
224 pub const DEFAULT_CONFIG: &'static str = include_str!("../../../default-config.toml");
228
229 pub const REFERENCE_MD: &'static str = include_str!("../../../repo-config-reference.md");
231
232 pub const SCHEMA: &'static str = include_str!("../../../jsonschemas/repo-config.json");
239
240 pub const ENVIRONMENT_PREFIX: &'static str = "NEXTEST";
242
243 pub const DEFAULT_PROFILE: &'static str = "default";
245
246 pub const DEFAULT_MIRI_PROFILE: &'static str = "default-miri";
248
249 pub const DEFAULT_PROFILES: &'static [&'static str] =
251 &[Self::DEFAULT_PROFILE, Self::DEFAULT_MIRI_PROFILE];
252
253 pub fn from_sources<'a, I>(
263 workspace_root: impl Into<Utf8PathBuf>,
264 pcx: &ParseContext<'_>,
265 config_file: Option<&Utf8Path>,
266 tool_config_files: impl IntoIterator<IntoIter = I>,
267 experimental: &BTreeSet<ConfigExperimental>,
268 ) -> Result<Self, ConfigParseError>
269 where
270 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
271 {
272 Self::from_sources_with_warnings(
273 workspace_root,
274 pcx,
275 config_file,
276 tool_config_files,
277 experimental,
278 &mut DefaultConfigWarnings,
279 )
280 }
281
282 pub fn from_sources_with_warnings<'a, I>(
284 workspace_root: impl Into<Utf8PathBuf>,
285 pcx: &ParseContext<'_>,
286 config_file: Option<&Utf8Path>,
287 tool_config_files: impl IntoIterator<IntoIter = I>,
288 experimental: &BTreeSet<ConfigExperimental>,
289 warnings: &mut impl ConfigWarnings,
290 ) -> Result<Self, ConfigParseError>
291 where
292 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
293 {
294 Self::from_sources_impl(
295 workspace_root,
296 pcx,
297 config_file,
298 tool_config_files,
299 experimental,
300 warnings,
301 )
302 }
303
304 fn from_sources_impl<'a, I>(
306 workspace_root: impl Into<Utf8PathBuf>,
307 pcx: &ParseContext<'_>,
308 config_file: Option<&Utf8Path>,
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 = workspace_root.into();
317 let tool_config_files_rev = tool_config_files.into_iter().rev();
318 let (inner, compiled) = Self::read_from_sources(
319 pcx,
320 &workspace_root,
321 config_file,
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: NextestConfigDeserialize =
344 serde_ignored::deserialize(config, |path: serde_ignored::Path| {
345 unknown.insert(path.to_string());
346 })
347 .expect("default config is always valid");
348
349 if !unknown.is_empty() {
352 panic!(
353 "found unknown keys in default config: {}",
354 unknown.iter().join(", ")
355 );
356 }
357
358 Self {
359 workspace_root: workspace_root.into(),
360 inner: deserialized.into_config_impl(),
361 compiled: CompiledByProfile::for_default_config(),
363 }
364 }
365
366 pub fn profile(&self, name: impl AsRef<str>) -> Result<EarlyProfile<'_>, ProfileNotFound> {
369 self.make_profile(name.as_ref())
370 }
371
372 fn read_from_sources<'a>(
377 pcx: &ParseContext<'_>,
378 workspace_root: &Utf8Path,
379 file: Option<&Utf8Path>,
380 tool_config_files_rev: impl Iterator<Item = &'a ToolConfigFile>,
381 experimental: &BTreeSet<ConfigExperimental>,
382 warnings: &mut impl ConfigWarnings,
383 ) -> Result<(NextestConfigImpl, CompiledByProfile), ConfigParseError> {
384 let mut composite_builder = Self::make_default_config();
386
387 let mut compiled = CompiledByProfile::for_default_config();
390
391 let mut known_groups = BTreeSet::new();
392 let mut known_scripts = IdOrdMap::new();
393 let mut known_profiles = BTreeSet::new();
396
397 for ToolConfigFile { config_file, tool } in tool_config_files_rev {
399 let source = File::new(config_file.as_str(), FileFormat::Toml);
400 Self::deserialize_individual_config(
401 pcx,
402 workspace_root,
403 config_file,
404 Some(tool),
405 source.clone(),
406 &mut compiled,
407 experimental,
408 warnings,
409 &mut known_groups,
410 &mut known_scripts,
411 &mut known_profiles,
412 )?;
413
414 composite_builder = composite_builder.add_source(source);
416 }
417
418 let (config_file, source) = match file {
420 Some(file) => (file.to_owned(), File::new(file.as_str(), FileFormat::Toml)),
421 None => {
422 let config_file = workspace_root.join(Self::CONFIG_PATH);
423 let source = File::new(config_file.as_str(), FileFormat::Toml).required(false);
424 (config_file, source)
425 }
426 };
427
428 Self::deserialize_individual_config(
429 pcx,
430 workspace_root,
431 &config_file,
432 None,
433 source.clone(),
434 &mut compiled,
435 experimental,
436 warnings,
437 &mut known_groups,
438 &mut known_scripts,
439 &mut known_profiles,
440 )?;
441
442 composite_builder = composite_builder.add_source(source);
443
444 let (config, _unknown) = Self::build_and_deserialize_config(&composite_builder)
447 .map_err(|kind| ConfigParseError::new(&config_file, None, kind))?;
448
449 compiled.default.reverse();
451 for data in compiled.other.values_mut() {
452 data.reverse();
453 }
454
455 Ok((config.into_config_impl(), compiled))
456 }
457
458 #[expect(clippy::too_many_arguments)]
459 fn deserialize_individual_config(
460 pcx: &ParseContext<'_>,
461 workspace_root: &Utf8Path,
462 config_file: &Utf8Path,
463 tool: Option<&ToolName>,
464 source: File<FileSourceFile, FileFormat>,
465 compiled_out: &mut CompiledByProfile,
466 experimental: &BTreeSet<ConfigExperimental>,
467 warnings: &mut impl ConfigWarnings,
468 known_groups: &mut BTreeSet<CustomTestGroup>,
469 known_scripts: &mut IdOrdMap<ScriptInfo>,
470 known_profiles: &mut BTreeSet<String>,
471 ) -> Result<(), ConfigParseError> {
472 let default_builder = Self::make_default_config();
475 let this_builder = default_builder.add_source(source);
476 let (mut this_config, unknown) = Self::build_and_deserialize_config(&this_builder)
477 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
478
479 if !unknown.is_empty() {
480 warnings.unknown_config_keys(config_file, workspace_root, tool, &unknown);
481 }
482
483 let (valid_groups, invalid_groups): (BTreeSet<_>, _) =
485 this_config.test_groups.keys().cloned().partition(|group| {
486 if let Some(tool) = tool {
487 group
489 .as_identifier()
490 .tool_components()
491 .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
492 } else {
493 !group.as_identifier().is_tool_identifier()
495 }
496 });
497
498 if !invalid_groups.is_empty() {
499 let kind = if tool.is_some() {
500 ConfigParseErrorKind::InvalidTestGroupsDefinedByTool(invalid_groups)
501 } else {
502 ConfigParseErrorKind::InvalidTestGroupsDefined(invalid_groups)
503 };
504 return Err(ConfigParseError::new(config_file, tool, kind));
505 }
506
507 known_groups.extend(valid_groups);
508
509 if !this_config.scripts.is_empty() && !this_config.old_setup_scripts.is_empty() {
511 return Err(ConfigParseError::new(
512 config_file,
513 tool,
514 ConfigParseErrorKind::BothScriptAndScriptsDefined,
515 ));
516 }
517
518 if !this_config.old_setup_scripts.is_empty() {
520 warnings.deprecated_script_config(config_file, workspace_root, tool);
521 this_config.scripts.setup = this_config.old_setup_scripts.clone();
522 }
523
524 {
526 let mut missing_features = BTreeSet::new();
527 if !this_config.scripts.setup.is_empty()
528 && !experimental.contains(&ConfigExperimental::SetupScripts)
529 {
530 missing_features.insert(ConfigExperimental::SetupScripts);
531 }
532 if !this_config.scripts.wrapper.is_empty()
533 && !experimental.contains(&ConfigExperimental::WrapperScripts)
534 {
535 missing_features.insert(ConfigExperimental::WrapperScripts);
536 }
537 if !missing_features.is_empty() {
538 return Err(ConfigParseError::new(
539 config_file,
540 tool,
541 ConfigParseErrorKind::ExperimentalFeaturesNotEnabled { missing_features },
542 ));
543 }
544 }
545
546 let duplicate_ids: BTreeSet<_> = this_config.scripts.duplicate_ids().cloned().collect();
547 if !duplicate_ids.is_empty() {
548 return Err(ConfigParseError::new(
549 config_file,
550 tool,
551 ConfigParseErrorKind::DuplicateConfigScriptNames(duplicate_ids),
552 ));
553 }
554
555 let (valid_scripts, invalid_scripts): (BTreeSet<_>, _) = this_config
557 .scripts
558 .all_script_ids()
559 .cloned()
560 .partition(|script| {
561 if let Some(tool) = tool {
562 script
564 .as_identifier()
565 .tool_components()
566 .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
567 } else {
568 !script.as_identifier().is_tool_identifier()
570 }
571 });
572
573 if !invalid_scripts.is_empty() {
574 let kind = if tool.is_some() {
575 ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool(invalid_scripts)
576 } else {
577 ConfigParseErrorKind::InvalidConfigScriptsDefined(invalid_scripts)
578 };
579 return Err(ConfigParseError::new(config_file, tool, kind));
580 }
581
582 known_scripts.extend(
583 valid_scripts
584 .into_iter()
585 .map(|id| this_config.scripts.script_info(id)),
586 );
587
588 let this_config = this_config.into_config_impl();
589
590 let unknown_default_profiles: Vec<_> = this_config
591 .all_profiles()
592 .filter(|p| p.starts_with("default-") && !NextestConfig::DEFAULT_PROFILES.contains(p))
593 .collect();
594 if !unknown_default_profiles.is_empty() {
595 warnings.unknown_reserved_profiles(
596 config_file,
597 workspace_root,
598 tool,
599 &unknown_default_profiles,
600 );
601 }
602
603 this_config
607 .sanitize_profile_inherits(known_profiles)
608 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
609
610 known_profiles.extend(
612 this_config
613 .other_profiles()
614 .map(|(name, _)| name.to_owned()),
615 );
616
617 let this_compiled = CompiledByProfile::new(pcx, &this_config)
619 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
620
621 let mut unknown_group_errors = Vec::new();
623 let mut check_test_group = |profile_name: &str, test_group: Option<&TestGroup>| {
624 if let Some(TestGroup::Custom(group)) = test_group
625 && !known_groups.contains(group)
626 {
627 unknown_group_errors.push(UnknownTestGroupError {
628 profile_name: profile_name.to_owned(),
629 name: TestGroup::Custom(group.clone()),
630 });
631 }
632 };
633
634 this_compiled
635 .default
636 .overrides
637 .iter()
638 .for_each(|override_| {
639 check_test_group("default", override_.data.test_group.as_ref());
640 });
641
642 this_compiled.other.iter().for_each(|(profile_name, data)| {
644 data.overrides.iter().for_each(|override_| {
645 check_test_group(profile_name, override_.data.test_group.as_ref());
646 });
647 });
648
649 if !unknown_group_errors.is_empty() {
651 let known_groups = TestGroup::make_all_groups(known_groups.iter().cloned()).collect();
652 return Err(ConfigParseError::new(
653 config_file,
654 tool,
655 ConfigParseErrorKind::UnknownTestGroups {
656 errors: unknown_group_errors,
657 known_groups,
658 },
659 ));
660 }
661
662 let mut profile_script_errors = ProfileScriptErrors::default();
665 let mut check_script_ids = |profile_name: &str,
666 script_type: ProfileScriptType,
667 expr: Option<&Filterset>,
668 scripts: &[ScriptId]| {
669 for script in scripts {
670 if let Some(script_info) = known_scripts.get(script) {
671 if !script_info.script_type.matches(script_type) {
672 profile_script_errors.wrong_script_types.push(
673 ProfileWrongConfigScriptTypeError {
674 profile_name: profile_name.to_owned(),
675 name: script.clone(),
676 attempted: script_type,
677 actual: script_info.script_type,
678 },
679 );
680 }
681 if script_type == ProfileScriptType::ListWrapper
682 && let Some(expr) = expr
683 {
684 let runtime_only_leaves = expr.parsed.runtime_only_leaves();
685 if !runtime_only_leaves.is_empty() {
686 let filters = runtime_only_leaves
687 .iter()
688 .map(|leaf| leaf.to_string())
689 .collect();
690 profile_script_errors.list_scripts_using_run_filters.push(
691 ProfileListScriptUsesRunFiltersError {
692 profile_name: profile_name.to_owned(),
693 name: script.clone(),
694 script_type,
695 filters,
696 },
697 );
698 }
699 }
700 } else {
701 profile_script_errors
702 .unknown_scripts
703 .push(ProfileUnknownScriptError {
704 profile_name: profile_name.to_owned(),
705 name: script.clone(),
706 });
707 }
708 }
709 };
710
711 let mut empty_script_count = 0;
712
713 this_compiled.default.scripts.iter().for_each(|scripts| {
714 if scripts.setup.is_empty()
715 && scripts.list_wrapper.is_none()
716 && scripts.run_wrapper.is_none()
717 {
718 empty_script_count += 1;
719 }
720
721 check_script_ids(
722 "default",
723 ProfileScriptType::Setup,
724 scripts.data.expr(),
725 &scripts.setup,
726 );
727 check_script_ids(
728 "default",
729 ProfileScriptType::ListWrapper,
730 scripts.data.expr(),
731 scripts.list_wrapper.as_slice(),
732 );
733 check_script_ids(
734 "default",
735 ProfileScriptType::RunWrapper,
736 scripts.data.expr(),
737 scripts.run_wrapper.as_slice(),
738 );
739 });
740
741 if empty_script_count > 0 {
742 warnings.empty_script_sections(
743 config_file,
744 workspace_root,
745 tool,
746 "default",
747 empty_script_count,
748 );
749 }
750
751 this_compiled.other.iter().for_each(|(profile_name, data)| {
752 let mut empty_script_count = 0;
753 data.scripts.iter().for_each(|scripts| {
754 if scripts.setup.is_empty()
755 && scripts.list_wrapper.is_none()
756 && scripts.run_wrapper.is_none()
757 {
758 empty_script_count += 1;
759 }
760
761 check_script_ids(
762 profile_name,
763 ProfileScriptType::Setup,
764 scripts.data.expr(),
765 &scripts.setup,
766 );
767 check_script_ids(
768 profile_name,
769 ProfileScriptType::ListWrapper,
770 scripts.data.expr(),
771 scripts.list_wrapper.as_slice(),
772 );
773 check_script_ids(
774 profile_name,
775 ProfileScriptType::RunWrapper,
776 scripts.data.expr(),
777 scripts.run_wrapper.as_slice(),
778 );
779 });
780
781 if empty_script_count > 0 {
782 warnings.empty_script_sections(
783 config_file,
784 workspace_root,
785 tool,
786 profile_name,
787 empty_script_count,
788 );
789 }
790 });
791
792 if !profile_script_errors.is_empty() {
795 let known_scripts = known_scripts
796 .iter()
797 .map(|script| script.id.clone())
798 .collect();
799 return Err(ConfigParseError::new(
800 config_file,
801 tool,
802 ConfigParseErrorKind::ProfileScriptErrors {
803 errors: Box::new(profile_script_errors),
804 known_scripts,
805 },
806 ));
807 }
808
809 compiled_out.default.extend_reverse(this_compiled.default);
812 for (name, mut data) in this_compiled.other {
813 match compiled_out.other.entry(name) {
814 hash_map::Entry::Vacant(entry) => {
815 data.reverse();
817 entry.insert(data);
818 }
819 hash_map::Entry::Occupied(mut entry) => {
820 entry.get_mut().extend_reverse(data);
822 }
823 }
824 }
825
826 Ok(())
827 }
828
829 fn make_default_config() -> ConfigBuilder<DefaultState> {
830 Config::builder().add_source(File::from_str(Self::DEFAULT_CONFIG, FileFormat::Toml))
831 }
832
833 fn make_profile(&self, name: &str) -> Result<EarlyProfile<'_>, ProfileNotFound> {
834 let custom_profile = self.inner.get_profile(name)?;
835
836 let inheritance_chain = self.inner.resolve_inheritance_chain(name)?;
838
839 let mut store_dir = self.workspace_root.join(&self.inner.store.dir);
841 store_dir.push(name);
842
843 let compiled_data = match self.compiled.other.get(name) {
845 Some(data) => data.clone().chain(self.compiled.default.clone()),
846 None => self.compiled.default.clone(),
847 };
848
849 Ok(EarlyProfile {
850 name: name.to_owned(),
851 store_dir,
852 default_profile: &self.inner.default_profile,
853 custom_profile,
854 inheritance_chain,
855 test_groups: &self.inner.test_groups,
856 scripts: &self.inner.scripts,
857 compiled_data,
858 })
859 }
860
861 fn build_and_deserialize_config(
863 builder: &ConfigBuilder<DefaultState>,
864 ) -> Result<(NextestConfigDeserialize, BTreeSet<String>), ConfigParseErrorKind> {
865 let config = builder
866 .build_cloned()
867 .map_err(|error| ConfigParseErrorKind::BuildError(Box::new(error)))?;
868
869 let mut ignored = BTreeSet::new();
870 let mut cb = |path: serde_ignored::Path| {
871 ignored.insert(path.to_string());
872 };
873 let ignored_de = serde_ignored::Deserializer::new(config, &mut cb);
874 let config: NextestConfigDeserialize = serde_path_to_error::deserialize(ignored_de)
875 .map_err(|error| {
876 let path = error.path().clone();
880 let config_error = error.into_inner();
881 let error = match config_error {
882 ConfigError::At { error, .. } => *error,
883 other => other,
884 };
885 ConfigParseErrorKind::DeserializeError(Box::new(serde_path_to_error::Error::new(
886 path, error,
887 )))
888 })?;
889
890 Ok((config, ignored))
891 }
892}
893
894#[derive(Clone, Debug, Default)]
896pub(in crate::config) struct PreBuildPlatform {}
897
898#[derive(Clone, Debug)]
900pub(crate) struct FinalConfig {
901 pub(in crate::config) host_eval: bool,
903 pub(in crate::config) host_test_eval: bool,
906 pub(in crate::config) target_eval: bool,
909}
910
911pub struct EarlyProfile<'cfg> {
916 name: String,
917 store_dir: Utf8PathBuf,
918 default_profile: &'cfg DefaultProfileImpl,
919 custom_profile: Option<&'cfg CustomProfileImpl>,
920 inheritance_chain: Vec<&'cfg CustomProfileImpl>,
921 test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
922 scripts: &'cfg ScriptConfig,
924 pub(in crate::config) compiled_data: CompiledData<PreBuildPlatform>,
926}
927
928macro_rules! profile_field {
931 ($eval_prof:ident.$field:ident) => {
932 $eval_prof
933 .custom_profile
934 .iter()
935 .chain($eval_prof.inheritance_chain.iter())
936 .find_map(|p| p.$field)
937 .unwrap_or($eval_prof.default_profile.$field)
938 };
939 ($eval_prof:ident.$nested:ident.$field:ident) => {
940 $eval_prof
941 .custom_profile
942 .iter()
943 .chain($eval_prof.inheritance_chain.iter())
944 .find_map(|p| p.$nested.$field)
945 .unwrap_or($eval_prof.default_profile.$nested.$field)
946 };
947 ($eval_prof:ident.$method:ident($($arg:expr),*)) => {
949 $eval_prof
950 .custom_profile
951 .iter()
952 .chain($eval_prof.inheritance_chain.iter())
953 .find_map(|p| p.$method($($arg),*))
954 .unwrap_or_else(|| $eval_prof.default_profile.$method($($arg),*))
955 };
956}
957macro_rules! profile_field_from_ref {
958 ($eval_prof:ident.$field:ident.$ref_func:ident()) => {
959 $eval_prof
960 .custom_profile
961 .iter()
962 .chain($eval_prof.inheritance_chain.iter())
963 .find_map(|p| p.$field.$ref_func())
964 .unwrap_or(&$eval_prof.default_profile.$field)
965 };
966 ($eval_prof:ident.$nested:ident.$field:ident.$ref_func:ident()) => {
967 $eval_prof
968 .custom_profile
969 .iter()
970 .chain($eval_prof.inheritance_chain.iter())
971 .find_map(|p| p.$nested.$field.$ref_func())
972 .unwrap_or(&$eval_prof.default_profile.$nested.$field)
973 };
974}
975macro_rules! profile_field_optional {
977 ($eval_prof:ident.$nested:ident.$field:ident.$ref_func:ident()) => {
978 $eval_prof
979 .custom_profile
980 .iter()
981 .chain($eval_prof.inheritance_chain.iter())
982 .find_map(|p| p.$nested.$field.$ref_func())
983 .or($eval_prof.default_profile.$nested.$field.$ref_func())
984 };
985}
986
987impl<'cfg> EarlyProfile<'cfg> {
988 pub fn store_dir(&self) -> &Utf8Path {
990 &self.store_dir
991 }
992
993 pub fn has_junit(&self) -> bool {
995 profile_field_optional!(self.junit.path.as_deref()).is_some()
996 }
997
998 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
1000 self.test_groups
1001 }
1002
1003 pub fn known_groups(&self) -> KnownGroups {
1008 let custom_groups = self
1009 .test_group_config()
1010 .keys()
1011 .map(|g| g.to_string())
1012 .collect();
1013 KnownGroups::Known { custom_groups }
1014 }
1015
1016 pub fn apply_build_platforms(
1021 self,
1022 build_platforms: &BuildPlatforms,
1023 ) -> EvaluatableProfile<'cfg> {
1024 let compiled_data = self.compiled_data.apply_build_platforms(build_platforms);
1025
1026 let resolved_default_filter = {
1027 let found_filter = compiled_data
1029 .overrides
1030 .iter()
1031 .find_map(|override_data| override_data.default_filter_if_matches_platform());
1032 found_filter.unwrap_or_else(|| {
1033 compiled_data
1036 .profile_default_filter
1037 .as_ref()
1038 .expect("compiled data always has default set")
1039 })
1040 }
1041 .clone();
1042
1043 EvaluatableProfile {
1044 name: self.name,
1045 store_dir: self.store_dir,
1046 default_profile: self.default_profile,
1047 custom_profile: self.custom_profile,
1048 inheritance_chain: self.inheritance_chain,
1049 scripts: self.scripts,
1050 test_groups: self.test_groups,
1051 compiled_data,
1052 resolved_default_filter,
1053 }
1054 }
1055}
1056
1057#[derive(Clone, Debug)]
1061pub struct EvaluatableProfile<'cfg> {
1062 name: String,
1063 store_dir: Utf8PathBuf,
1064 default_profile: &'cfg DefaultProfileImpl,
1065 custom_profile: Option<&'cfg CustomProfileImpl>,
1066 inheritance_chain: Vec<&'cfg CustomProfileImpl>,
1067 test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
1068 scripts: &'cfg ScriptConfig,
1070 pub(in crate::config) compiled_data: CompiledData<FinalConfig>,
1072 resolved_default_filter: CompiledDefaultFilter,
1075}
1076
1077impl<'cfg> EvaluatableProfile<'cfg> {
1078 pub fn name(&self) -> &str {
1080 &self.name
1081 }
1082
1083 pub fn store_dir(&self) -> &Utf8Path {
1085 &self.store_dir
1086 }
1087
1088 pub fn filterset_ecx(&self) -> EvalContext<'_> {
1090 EvalContext {
1091 default_filter: &self.default_filter().expr,
1092 }
1093 }
1094
1095 pub fn precompute_group_memberships<'a>(
1103 &self,
1104 tests: impl Iterator<Item = TestQuery<'a>>,
1105 ) -> PrecomputedGroupMembership {
1106 let run_mode = NextestRunMode::Test;
1109
1110 let mut membership = PrecomputedGroupMembership::empty();
1111 for test in tests {
1112 let group = self.settings_for(run_mode, &test).test_group().clone();
1113 if group != TestGroup::Global {
1114 let id = TestInstanceId {
1115 binary_id: test.binary_query.binary_id,
1116 test_name: test.test_name,
1117 };
1118 membership.insert(id.to_owned(), group);
1119 }
1120 }
1121 membership
1122 }
1123
1124 pub fn default_filter(&self) -> &CompiledDefaultFilter {
1126 &self.resolved_default_filter
1127 }
1128
1129 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
1131 self.test_groups
1132 }
1133
1134 pub fn script_config(&self) -> &'cfg ScriptConfig {
1136 self.scripts
1137 }
1138
1139 pub fn retries(&self) -> RetryPolicy {
1141 profile_field!(self.retries)
1142 }
1143
1144 pub fn flaky_result(&self) -> FlakyResult {
1146 profile_field!(self.flaky_result)
1147 }
1148
1149 pub fn test_threads(&self) -> TestThreads {
1151 profile_field!(self.test_threads)
1152 }
1153
1154 pub fn threads_required(&self) -> ThreadsRequired {
1156 profile_field!(self.threads_required)
1157 }
1158
1159 pub fn run_extra_args(&self) -> &'cfg [String] {
1161 profile_field_from_ref!(self.run_extra_args.as_deref())
1162 }
1163
1164 pub fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1166 profile_field!(self.slow_timeout(run_mode))
1167 }
1168
1169 pub fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1171 profile_field!(self.global_timeout(run_mode))
1172 }
1173
1174 pub fn leak_timeout(&self) -> LeakTimeout {
1177 profile_field!(self.leak_timeout)
1178 }
1179
1180 pub fn status_level(&self) -> StatusLevel {
1182 profile_field!(self.status_level)
1183 }
1184
1185 pub fn final_status_level(&self) -> FinalStatusLevel {
1187 profile_field!(self.final_status_level)
1188 }
1189
1190 pub fn failure_output(&self) -> TestOutputDisplay {
1192 profile_field!(self.failure_output)
1193 }
1194
1195 pub fn success_output(&self) -> TestOutputDisplay {
1197 profile_field!(self.success_output)
1198 }
1199
1200 pub fn max_fail(&self) -> MaxFail {
1202 profile_field!(self.max_fail)
1203 }
1204
1205 pub fn archive_config(&self) -> &'cfg ArchiveConfig {
1207 profile_field_from_ref!(self.archive.as_ref())
1208 }
1209
1210 pub fn setup_scripts(&self, test_list: &TestList<'_>) -> SetupScripts<'_> {
1212 SetupScripts::new(self, test_list)
1213 }
1214
1215 pub fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1217 ListSettings::new(self, query)
1218 }
1219
1220 pub fn settings_for(
1222 &self,
1223 run_mode: NextestRunMode,
1224 query: &TestQuery<'_>,
1225 ) -> TestSettings<'_> {
1226 TestSettings::new(self, run_mode, query)
1227 }
1228
1229 pub(crate) fn settings_with_source_for(
1231 &self,
1232 run_mode: NextestRunMode,
1233 query: &TestQuery<'_>,
1234 ) -> TestSettings<'_, SettingSource<'_>> {
1235 TestSettings::new(self, run_mode, query)
1236 }
1237
1238 pub fn junit(&self) -> Option<JunitConfig<'cfg>> {
1240 let settings = JunitSettings {
1241 path: profile_field_optional!(self.junit.path.as_deref()),
1242 report_name: profile_field_from_ref!(self.junit.report_name.as_deref()),
1243 store_success_output: profile_field!(self.junit.store_success_output),
1244 store_failure_output: profile_field!(self.junit.store_failure_output),
1245 report_skipped: profile_field!(self.junit.report_skipped),
1246 flaky_fail_status: profile_field!(self.junit.flaky_fail_status),
1247 };
1248 JunitConfig::new(self.store_dir(), settings)
1249 }
1250
1251 pub fn inherits(&self) -> Option<&str> {
1253 if let Some(custom_profile) = self.custom_profile {
1254 return custom_profile.inherits();
1255 }
1256 None
1257 }
1258
1259 #[cfg(test)]
1260 pub(in crate::config) fn custom_profile(&self) -> Option<&'cfg CustomProfileImpl> {
1261 self.custom_profile
1262 }
1263}
1264
1265#[derive(Clone, Debug)]
1266pub(in crate::config) struct NextestConfigImpl {
1267 store: StoreConfigImpl,
1268 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1269 scripts: ScriptConfig,
1270 default_profile: DefaultProfileImpl,
1271 other_profiles: HashMap<String, CustomProfileImpl>,
1272}
1273
1274impl NextestConfigImpl {
1275 fn get_profile(&self, profile: &str) -> Result<Option<&CustomProfileImpl>, ProfileNotFound> {
1276 let custom_profile = match profile {
1277 NextestConfig::DEFAULT_PROFILE => None,
1278 other => Some(
1279 self.other_profiles
1280 .get(other)
1281 .ok_or_else(|| ProfileNotFound::new(profile, self.all_profiles()))?,
1282 ),
1283 };
1284 Ok(custom_profile)
1285 }
1286
1287 fn all_profiles(&self) -> impl Iterator<Item = &str> {
1288 self.other_profiles
1289 .keys()
1290 .map(|key| key.as_str())
1291 .chain(std::iter::once(NextestConfig::DEFAULT_PROFILE))
1292 }
1293
1294 pub(in crate::config) fn default_profile(&self) -> &DefaultProfileImpl {
1295 &self.default_profile
1296 }
1297
1298 pub(in crate::config) fn other_profiles(
1299 &self,
1300 ) -> impl Iterator<Item = (&str, &CustomProfileImpl)> {
1301 self.other_profiles
1302 .iter()
1303 .map(|(key, value)| (key.as_str(), value))
1304 }
1305
1306 fn resolve_inheritance_chain(
1312 &self,
1313 profile_name: &str,
1314 ) -> Result<Vec<&CustomProfileImpl>, ProfileNotFound> {
1315 let mut chain = Vec::new();
1316
1317 let mut curr = self
1320 .get_profile(profile_name)?
1321 .and_then(|p| p.inherits.as_deref());
1322
1323 while let Some(name) = curr {
1324 let profile = self.get_profile(name)?;
1325 if let Some(profile) = profile {
1326 chain.push(profile);
1327 curr = profile.inherits.as_deref();
1328 } else {
1329 break;
1331 }
1332 }
1333
1334 Ok(chain)
1335 }
1336
1337 fn sanitize_profile_inherits(
1342 &self,
1343 known_profiles: &BTreeSet<String>,
1344 ) -> Result<(), ConfigParseErrorKind> {
1345 let mut inherit_err_collector = Vec::new();
1346
1347 self.sanitize_default_profile_inherits(&mut inherit_err_collector);
1348 self.sanitize_custom_profile_inherits(&mut inherit_err_collector, known_profiles);
1349
1350 if !inherit_err_collector.is_empty() {
1351 return Err(ConfigParseErrorKind::InheritanceErrors(
1352 inherit_err_collector,
1353 ));
1354 }
1355
1356 Ok(())
1357 }
1358
1359 fn sanitize_default_profile_inherits(&self, inherit_err_collector: &mut Vec<InheritsError>) {
1362 if self.default_profile().inherits().is_some() {
1363 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(
1364 NextestConfig::DEFAULT_PROFILE.to_string(),
1365 ));
1366 }
1367 }
1368
1369 fn sanitize_custom_profile_inherits(
1371 &self,
1372 inherit_err_collector: &mut Vec<InheritsError>,
1373 known_profiles: &BTreeSet<String>,
1374 ) {
1375 let mut profile_graph = Graph::<&str, (), Directed>::new();
1376 let mut profile_map = HashMap::new();
1377
1378 for (name, custom_profile) in self.other_profiles() {
1381 let starts_with_default = self.sanitize_custom_default_profile_inherits(
1382 name,
1383 custom_profile,
1384 inherit_err_collector,
1385 );
1386 if !starts_with_default {
1387 self.add_profile_to_graph(
1392 name,
1393 custom_profile,
1394 &mut profile_map,
1395 &mut profile_graph,
1396 inherit_err_collector,
1397 known_profiles,
1398 );
1399 }
1400 }
1401
1402 self.check_inheritance_cycles(profile_graph, inherit_err_collector);
1403 }
1404
1405 fn sanitize_custom_default_profile_inherits(
1408 &self,
1409 name: &str,
1410 custom_profile: &CustomProfileImpl,
1411 inherit_err_collector: &mut Vec<InheritsError>,
1412 ) -> bool {
1413 let starts_with_default = name.starts_with("default-");
1414
1415 if starts_with_default && custom_profile.inherits().is_some() {
1416 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(name.to_string()));
1417 }
1418
1419 starts_with_default
1420 }
1421
1422 fn add_profile_to_graph<'cfg>(
1427 &self,
1428 name: &'cfg str,
1429 custom_profile: &'cfg CustomProfileImpl,
1430 profile_map: &mut HashMap<&'cfg str, NodeIndex>,
1431 profile_graph: &mut Graph<&'cfg str, ()>,
1432 inherit_err_collector: &mut Vec<InheritsError>,
1433 known_profiles: &BTreeSet<String>,
1434 ) {
1435 if let Some(inherits_name) = custom_profile.inherits() {
1436 if inherits_name == name {
1437 inherit_err_collector
1438 .push(InheritsError::SelfReferentialInheritance(name.to_string()))
1439 } else if self.get_profile(inherits_name).is_ok() {
1440 let from_node = match profile_map.get(name) {
1442 None => {
1443 let profile_node = profile_graph.add_node(name);
1444 profile_map.insert(name, profile_node);
1445 profile_node
1446 }
1447 Some(node_idx) => *node_idx,
1448 };
1449 let to_node = match profile_map.get(inherits_name) {
1450 None => {
1451 let profile_node = profile_graph.add_node(inherits_name);
1452 profile_map.insert(inherits_name, profile_node);
1453 profile_node
1454 }
1455 Some(node_idx) => *node_idx,
1456 };
1457 profile_graph.add_edge(from_node, to_node, ());
1458 } else if known_profiles.contains(inherits_name) {
1459 } else {
1463 inherit_err_collector.push(InheritsError::UnknownInheritance(
1464 name.to_string(),
1465 inherits_name.to_string(),
1466 ))
1467 }
1468 }
1469 }
1470
1471 fn check_inheritance_cycles(
1473 &self,
1474 profile_graph: Graph<&str, ()>,
1475 inherit_err_collector: &mut Vec<InheritsError>,
1476 ) {
1477 let profile_sccs: Vec<Vec<NodeIndex>> = kosaraju_scc(&profile_graph);
1478 let profile_sccs: Vec<Vec<NodeIndex>> = profile_sccs
1479 .into_iter()
1480 .filter(|scc| scc.len() >= 2)
1481 .collect();
1482
1483 if !profile_sccs.is_empty() {
1484 inherit_err_collector.push(InheritsError::InheritanceCycle(
1485 profile_sccs
1486 .iter()
1487 .map(|node_idxs| {
1488 let profile_names: Vec<String> = node_idxs
1489 .iter()
1490 .map(|node_idx| profile_graph[*node_idx].to_string())
1491 .collect();
1492 profile_names
1493 })
1494 .collect(),
1495 ));
1496 }
1497 }
1498}
1499
1500#[derive(Clone, Debug, Deserialize)]
1506#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1507#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1508#[serde(rename_all = "kebab-case")]
1509pub(crate) struct NextestConfigDeserialize {
1510 #[cfg_attr(
1512 feature = "config-schema",
1513 schemars(with = "Option<StoreConfigImpl>")
1515 )]
1516 store: StoreConfigImpl,
1517
1518 #[expect(unused)]
1523 #[serde(default)]
1524 nextest_version: Option<NextestVersionDeserialize>,
1525
1526 #[expect(unused)]
1528 #[serde(default)]
1529 experimental: ExperimentalDeserialize,
1530
1531 #[serde(default)]
1534 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1535
1536 #[serde(default, rename = "script")]
1541 old_setup_scripts: IndexMap<ScriptId, SetupScriptConfig>,
1542
1543 #[serde(default)]
1545 scripts: ScriptConfig,
1546
1547 #[serde(rename = "profile")]
1549 #[cfg_attr(
1550 feature = "config-schema",
1551 schemars(with = "Option<HashMap<String, CustomProfileImpl>>")
1553 )]
1554 profiles: HashMap<String, CustomProfileImpl>,
1555}
1556
1557impl NextestConfigDeserialize {
1558 fn into_config_impl(mut self) -> NextestConfigImpl {
1559 let p = self
1560 .profiles
1561 .remove("default")
1562 .expect("default profile should exist");
1563 let default_profile = DefaultProfileImpl::new(p);
1564
1565 for (script_id, script_config) in self.old_setup_scripts {
1570 if let indexmap::map::Entry::Vacant(entry) = self.scripts.setup.entry(script_id) {
1571 entry.insert(script_config);
1572 }
1573 }
1574
1575 NextestConfigImpl {
1576 store: self.store,
1577 default_profile,
1578 test_groups: self.test_groups,
1579 scripts: self.scripts,
1580 other_profiles: self.profiles,
1581 }
1582 }
1583}
1584
1585#[cfg(feature = "config-schema")]
1595pub fn nextest_config_schema() -> schemars::Schema {
1596 let mut schema = schemars::schema_for!(NextestConfigDeserialize);
1597 schema.insert(
1599 "x-tombi-toml-version".to_owned(),
1600 serde_json::Value::String("v1.1.0".to_owned()),
1601 );
1602 schema
1603}
1604
1605#[derive(Clone, Debug, Deserialize)]
1606#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1607#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1608#[serde(rename_all = "kebab-case")]
1609struct StoreConfigImpl {
1610 #[cfg_attr(
1612 feature = "config-schema",
1613 schemars(with = "Option<String>")
1615 )]
1616 dir: Utf8PathBuf,
1617}
1618
1619#[derive(Clone, Debug)]
1620pub(in crate::config) struct DefaultProfileImpl {
1621 default_filter: String,
1622 test_threads: TestThreads,
1623 threads_required: ThreadsRequired,
1624 run_extra_args: Vec<String>,
1625 retries: RetryPolicy,
1626 flaky_result: FlakyResult,
1627 status_level: StatusLevel,
1628 final_status_level: FinalStatusLevel,
1629 failure_output: TestOutputDisplay,
1630 success_output: TestOutputDisplay,
1631 max_fail: MaxFail,
1632 slow_timeout: SlowTimeout,
1633 global_timeout: GlobalTimeout,
1634 leak_timeout: LeakTimeout,
1635 overrides: Vec<DeserializedOverride>,
1636 scripts: Vec<DeserializedProfileScriptConfig>,
1637 junit: DefaultJunitImpl,
1638 archive: ArchiveConfig,
1639 bench: DefaultBenchConfig,
1640 inherits: Inherits,
1641}
1642
1643impl DefaultProfileImpl {
1644 fn new(p: CustomProfileImpl) -> Self {
1645 Self {
1646 default_filter: p
1647 .default_filter
1648 .expect("default-filter present in default profile"),
1649 test_threads: p
1650 .test_threads
1651 .expect("test-threads present in default profile"),
1652 threads_required: p
1653 .threads_required
1654 .expect("threads-required present in default profile"),
1655 run_extra_args: p
1656 .run_extra_args
1657 .expect("run-extra-args present in default profile"),
1658 retries: p.retries.expect("retries present in default profile"),
1659 flaky_result: p
1660 .flaky_result
1661 .expect("flaky-result present in default profile"),
1662 status_level: p
1663 .status_level
1664 .expect("status-level present in default profile"),
1665 final_status_level: p
1666 .final_status_level
1667 .expect("final-status-level present in default profile"),
1668 failure_output: p
1669 .failure_output
1670 .expect("failure-output present in default profile"),
1671 success_output: p
1672 .success_output
1673 .expect("success-output present in default profile"),
1674 max_fail: p.max_fail.expect("fail-fast present in default profile"),
1675 slow_timeout: p
1676 .slow_timeout
1677 .expect("slow-timeout present in default profile"),
1678 global_timeout: p
1679 .global_timeout
1680 .expect("global-timeout present in default profile"),
1681 leak_timeout: p
1682 .leak_timeout
1683 .expect("leak-timeout present in default profile"),
1684 overrides: p.overrides,
1685 scripts: p.scripts,
1686 junit: DefaultJunitImpl::for_default_profile(p.junit),
1687 archive: p.archive.expect("archive present in default profile"),
1688 bench: DefaultBenchConfig::for_default_profile(
1689 p.bench.expect("bench present in default profile"),
1690 ),
1691 inherits: Inherits::new(p.inherits),
1692 }
1693 }
1694
1695 pub(in crate::config) fn default_filter(&self) -> &str {
1696 &self.default_filter
1697 }
1698
1699 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1700 self.inherits.inherits_from()
1701 }
1702
1703 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1704 &self.overrides
1705 }
1706
1707 pub(in crate::config) fn setup_scripts(&self) -> &[DeserializedProfileScriptConfig] {
1708 &self.scripts
1709 }
1710
1711 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1712 match run_mode {
1713 NextestRunMode::Test => self.slow_timeout,
1714 NextestRunMode::Benchmark => self.bench.slow_timeout,
1715 }
1716 }
1717
1718 pub(in crate::config) fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1719 match run_mode {
1720 NextestRunMode::Test => self.global_timeout,
1721 NextestRunMode::Benchmark => self.bench.global_timeout,
1722 }
1723 }
1724}
1725
1726#[derive(Clone, Debug, Deserialize)]
1727#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1728#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1729#[serde(rename_all = "kebab-case")]
1730pub(in crate::config) struct CustomProfileImpl {
1731 #[serde(default)]
1734 default_filter: Option<String>,
1735 #[serde(default, deserialize_with = "deserialize_retry_policy")]
1737 retries: Option<RetryPolicy>,
1738 #[serde(default)]
1740 flaky_result: Option<FlakyResult>,
1741 #[serde(default)]
1743 test_threads: Option<TestThreads>,
1744 #[serde(default)]
1746 threads_required: Option<ThreadsRequired>,
1747 #[serde(default)]
1749 run_extra_args: Option<Vec<String>>,
1750 #[serde(default)]
1752 status_level: Option<StatusLevel>,
1753 #[serde(default)]
1755 final_status_level: Option<FinalStatusLevel>,
1756 #[serde(default)]
1758 failure_output: Option<TestOutputDisplay>,
1759 #[serde(default)]
1761 success_output: Option<TestOutputDisplay>,
1762 #[serde(
1764 default,
1765 rename = "fail-fast",
1766 deserialize_with = "deserialize_fail_fast"
1767 )]
1768 max_fail: Option<MaxFail>,
1769 #[serde(default, deserialize_with = "deserialize_slow_timeout")]
1772 slow_timeout: Option<SlowTimeout>,
1773 #[serde(default)]
1775 global_timeout: Option<GlobalTimeout>,
1776 #[serde(default, deserialize_with = "deserialize_leak_timeout")]
1778 leak_timeout: Option<LeakTimeout>,
1779 #[serde(default)]
1781 overrides: Vec<DeserializedOverride>,
1782 #[serde(default)]
1784 scripts: Vec<DeserializedProfileScriptConfig>,
1785 #[serde(default)]
1787 junit: JunitImpl,
1788 #[serde(default)]
1790 archive: Option<ArchiveConfig>,
1791 #[serde(default)]
1793 bench: Option<BenchConfig>,
1794 #[serde(default)]
1796 inherits: Option<String>,
1797}
1798
1799impl CustomProfileImpl {
1800 #[cfg(test)]
1801 pub(in crate::config) fn test_threads(&self) -> Option<TestThreads> {
1802 self.test_threads
1803 }
1804
1805 pub(in crate::config) fn default_filter(&self) -> Option<&str> {
1806 self.default_filter.as_deref()
1807 }
1808
1809 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> Option<SlowTimeout> {
1810 match run_mode {
1811 NextestRunMode::Test => self.slow_timeout,
1812 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.slow_timeout),
1813 }
1814 }
1815
1816 pub(in crate::config) fn global_timeout(
1817 &self,
1818 run_mode: NextestRunMode,
1819 ) -> Option<GlobalTimeout> {
1820 match run_mode {
1821 NextestRunMode::Test => self.global_timeout,
1822 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.global_timeout),
1823 }
1824 }
1825
1826 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1827 self.inherits.as_deref()
1828 }
1829
1830 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1831 &self.overrides
1832 }
1833
1834 pub(in crate::config) fn scripts(&self) -> &[DeserializedProfileScriptConfig] {
1835 &self.scripts
1836 }
1837}
1838
1839#[cfg(test)]
1840mod tests {
1841 use super::*;
1842 use crate::config::utils::test_helpers::*;
1843 use camino_tempfile::tempdir;
1844 use iddqd::{IdHashItem, IdHashMap, id_hash_map, id_upcast};
1845
1846 fn tool_name(s: &str) -> ToolName {
1847 ToolName::new(s.into()).unwrap()
1848 }
1849
1850 #[derive(Default)]
1852 struct TestConfigWarnings {
1853 unknown_keys: IdHashMap<UnknownKeys>,
1854 reserved_profiles: IdHashMap<ReservedProfiles>,
1855 deprecated_scripts: IdHashMap<DeprecatedScripts>,
1856 empty_script_warnings: IdHashMap<EmptyScriptSections>,
1857 }
1858
1859 impl ConfigWarnings for TestConfigWarnings {
1860 fn unknown_config_keys(
1861 &mut self,
1862 config_file: &Utf8Path,
1863 _workspace_root: &Utf8Path,
1864 tool: Option<&ToolName>,
1865 unknown: &BTreeSet<String>,
1866 ) {
1867 self.unknown_keys
1868 .insert_unique(UnknownKeys {
1869 tool: tool.cloned(),
1870 config_file: config_file.to_owned(),
1871 keys: unknown.clone(),
1872 })
1873 .unwrap();
1874 }
1875
1876 fn unknown_reserved_profiles(
1877 &mut self,
1878 config_file: &Utf8Path,
1879 _workspace_root: &Utf8Path,
1880 tool: Option<&ToolName>,
1881 profiles: &[&str],
1882 ) {
1883 self.reserved_profiles
1884 .insert_unique(ReservedProfiles {
1885 tool: tool.cloned(),
1886 config_file: config_file.to_owned(),
1887 profiles: profiles.iter().map(|&s| s.to_owned()).collect(),
1888 })
1889 .unwrap();
1890 }
1891
1892 fn empty_script_sections(
1893 &mut self,
1894 config_file: &Utf8Path,
1895 _workspace_root: &Utf8Path,
1896 tool: Option<&ToolName>,
1897 profile_name: &str,
1898 empty_count: usize,
1899 ) {
1900 self.empty_script_warnings
1901 .insert_unique(EmptyScriptSections {
1902 tool: tool.cloned(),
1903 config_file: config_file.to_owned(),
1904 profile_name: profile_name.to_owned(),
1905 empty_count,
1906 })
1907 .unwrap();
1908 }
1909
1910 fn deprecated_script_config(
1911 &mut self,
1912 config_file: &Utf8Path,
1913 _workspace_root: &Utf8Path,
1914 tool: Option<&ToolName>,
1915 ) {
1916 self.deprecated_scripts
1917 .insert_unique(DeprecatedScripts {
1918 tool: tool.cloned(),
1919 config_file: config_file.to_owned(),
1920 })
1921 .unwrap();
1922 }
1923 }
1924
1925 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1926 struct UnknownKeys {
1927 tool: Option<ToolName>,
1928 config_file: Utf8PathBuf,
1929 keys: BTreeSet<String>,
1930 }
1931
1932 impl IdHashItem for UnknownKeys {
1933 type Key<'a> = Option<&'a ToolName>;
1934 fn key(&self) -> Self::Key<'_> {
1935 self.tool.as_ref()
1936 }
1937 id_upcast!();
1938 }
1939
1940 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1941 struct ReservedProfiles {
1942 tool: Option<ToolName>,
1943 config_file: Utf8PathBuf,
1944 profiles: Vec<String>,
1945 }
1946
1947 impl IdHashItem for ReservedProfiles {
1948 type Key<'a> = Option<&'a ToolName>;
1949 fn key(&self) -> Self::Key<'_> {
1950 self.tool.as_ref()
1951 }
1952 id_upcast!();
1953 }
1954
1955 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1956 struct DeprecatedScripts {
1957 tool: Option<ToolName>,
1958 config_file: Utf8PathBuf,
1959 }
1960
1961 impl IdHashItem for DeprecatedScripts {
1962 type Key<'a> = Option<&'a ToolName>;
1963 fn key(&self) -> Self::Key<'_> {
1964 self.tool.as_ref()
1965 }
1966 id_upcast!();
1967 }
1968
1969 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1970 struct EmptyScriptSections {
1971 tool: Option<ToolName>,
1972 config_file: Utf8PathBuf,
1973 profile_name: String,
1974 empty_count: usize,
1975 }
1976
1977 impl IdHashItem for EmptyScriptSections {
1978 type Key<'a> = (&'a Option<ToolName>, &'a str);
1979 fn key(&self) -> Self::Key<'_> {
1980 (&self.tool, &self.profile_name)
1981 }
1982 id_upcast!();
1983 }
1984
1985 #[test]
1986 fn default_config_is_valid() {
1987 let default_config = NextestConfig::default_config("foo");
1988 default_config
1989 .profile(NextestConfig::DEFAULT_PROFILE)
1990 .expect("default profile should exist");
1991 }
1992
1993 #[test]
1994 fn ignored_keys() {
1995 let config_contents = r#"
1996 ignored1 = "test"
1997
1998 [profile.default]
1999 retries = 3
2000 ignored2 = "hi"
2001
2002 [profile.default-foo]
2003 retries = 5
2004
2005 [[profile.default.overrides]]
2006 filter = 'test(test_foo)'
2007 retries = 20
2008 ignored3 = 42
2009 "#;
2010
2011 let tool_config_contents = r#"
2012 [store]
2013 ignored4 = 20
2014
2015 [profile.default]
2016 retries = 4
2017 ignored5 = false
2018
2019 [profile.default-bar]
2020 retries = 5
2021
2022 [profile.tool]
2023 retries = 12
2024
2025 [[profile.tool.overrides]]
2026 filter = 'test(test_baz)'
2027 retries = 22
2028 ignored6 = 6.5
2029 "#;
2030
2031 let workspace_dir = tempdir().unwrap();
2032
2033 let graph = temp_workspace(&workspace_dir, config_contents);
2034 let workspace_root = graph.workspace().root();
2035 let tool_path = workspace_root.join(".config/tool.toml");
2036 std::fs::write(&tool_path, tool_config_contents).unwrap();
2037
2038 let pcx = ParseContext::new(&graph);
2039
2040 let mut warnings = TestConfigWarnings::default();
2041
2042 let _ = NextestConfig::from_sources_with_warnings(
2043 workspace_root,
2044 &pcx,
2045 None,
2046 &[ToolConfigFile {
2047 tool: tool_name("my-tool"),
2048 config_file: tool_path.clone(),
2049 }][..],
2050 &Default::default(),
2051 &mut warnings,
2052 )
2053 .expect("config is valid");
2054
2055 assert_eq!(
2056 warnings.unknown_keys.len(),
2057 2,
2058 "there are two files with unknown keys"
2059 );
2060
2061 assert_eq!(
2062 warnings.unknown_keys,
2063 id_hash_map! {
2064 UnknownKeys {
2065 tool: None,
2066 config_file: workspace_root.join(".config/nextest.toml"),
2067 keys: maplit::btreeset! {
2068 "ignored1".to_owned(),
2069 "profile.default.ignored2".to_owned(),
2070 "profile.default.overrides.0.ignored3".to_owned(),
2071 }
2072 },
2073 UnknownKeys {
2074 tool: Some(tool_name("my-tool")),
2075 config_file: tool_path.clone(),
2076 keys: maplit::btreeset! {
2077 "store.ignored4".to_owned(),
2078 "profile.default.ignored5".to_owned(),
2079 "profile.tool.overrides.0.ignored6".to_owned(),
2080 }
2081 }
2082 }
2083 );
2084 assert_eq!(
2085 warnings.reserved_profiles,
2086 id_hash_map! {
2087 ReservedProfiles {
2088 tool: None,
2089 config_file: workspace_root.join(".config/nextest.toml"),
2090 profiles: vec!["default-foo".to_owned()],
2091 },
2092 ReservedProfiles {
2093 tool: Some(tool_name("my-tool")),
2094 config_file: tool_path,
2095 profiles: vec!["default-bar".to_owned()],
2096 }
2097 },
2098 )
2099 }
2100
2101 #[test]
2102 fn script_warnings() {
2103 let config_contents = r#"
2104 experimental = ["setup-scripts", "wrapper-scripts"]
2105
2106 [scripts.wrapper.script1]
2107 command = "echo test"
2108
2109 [scripts.wrapper.script2]
2110 command = "echo test2"
2111
2112 [scripts.setup.script3]
2113 command = "echo setup"
2114
2115 [[profile.default.scripts]]
2116 filter = 'all()'
2117 # Empty - no setup or wrapper scripts
2118
2119 [[profile.default.scripts]]
2120 filter = 'test(foo)'
2121 setup = ["script3"]
2122
2123 [profile.custom]
2124 [[profile.custom.scripts]]
2125 filter = 'all()'
2126 # Empty - no setup or wrapper scripts
2127
2128 [[profile.custom.scripts]]
2129 filter = 'test(bar)'
2130 # Another empty section
2131 "#;
2132
2133 let tool_config_contents = r#"
2134 experimental = ["setup-scripts", "wrapper-scripts"]
2135
2136 [scripts.wrapper."@tool:tool:disabled_script"]
2137 command = "echo disabled"
2138
2139 [scripts.setup."@tool:tool:setup_script"]
2140 command = "echo setup"
2141
2142 [profile.tool]
2143 [[profile.tool.scripts]]
2144 filter = 'all()'
2145 # Empty section
2146
2147 [[profile.tool.scripts]]
2148 filter = 'test(foo)'
2149 setup = ["@tool:tool:setup_script"]
2150 "#;
2151
2152 let workspace_dir = tempdir().unwrap();
2153 let graph = temp_workspace(&workspace_dir, config_contents);
2154 let workspace_root = graph.workspace().root();
2155 let tool_path = workspace_root.join(".config/tool.toml");
2156 std::fs::write(&tool_path, tool_config_contents).unwrap();
2157
2158 let pcx = ParseContext::new(&graph);
2159
2160 let mut warnings = TestConfigWarnings::default();
2161
2162 let experimental = maplit::btreeset! {
2163 ConfigExperimental::SetupScripts,
2164 ConfigExperimental::WrapperScripts
2165 };
2166 let _ = NextestConfig::from_sources_with_warnings(
2167 workspace_root,
2168 &pcx,
2169 None,
2170 &[ToolConfigFile {
2171 tool: tool_name("tool"),
2172 config_file: tool_path.clone(),
2173 }][..],
2174 &experimental,
2175 &mut warnings,
2176 )
2177 .expect("config is valid");
2178
2179 assert_eq!(
2180 warnings.empty_script_warnings,
2181 id_hash_map! {
2182 EmptyScriptSections {
2183 tool: None,
2184 config_file: workspace_root.join(".config/nextest.toml"),
2185 profile_name: "default".to_owned(),
2186 empty_count: 1,
2187 },
2188 EmptyScriptSections {
2189 tool: None,
2190 config_file: workspace_root.join(".config/nextest.toml"),
2191 profile_name: "custom".to_owned(),
2192 empty_count: 2,
2193 },
2194 EmptyScriptSections {
2195 tool: Some(tool_name("tool")),
2196 config_file: tool_path,
2197 profile_name: "tool".to_owned(),
2198 empty_count: 1,
2199 }
2200 }
2201 );
2202 }
2203
2204 #[test]
2205 fn deprecated_script_config_warning() {
2206 let config_contents = r#"
2207 experimental = ["setup-scripts"]
2208
2209 [script.my-script]
2210 command = "echo hello"
2211"#;
2212
2213 let tool_config_contents = r#"
2214 experimental = ["setup-scripts"]
2215
2216 [script."@tool:my-tool:my-script"]
2217 command = "echo hello"
2218"#;
2219
2220 let temp_dir = tempdir().unwrap();
2221
2222 let graph = temp_workspace(&temp_dir, config_contents);
2223 let workspace_root = graph.workspace().root();
2224 let tool_path = workspace_root.join(".config/my-tool.toml");
2225 std::fs::write(&tool_path, tool_config_contents).unwrap();
2226 let pcx = ParseContext::new(&graph);
2227
2228 let mut warnings = TestConfigWarnings::default();
2229 NextestConfig::from_sources_with_warnings(
2230 graph.workspace().root(),
2231 &pcx,
2232 None,
2233 &[ToolConfigFile {
2234 tool: tool_name("my-tool"),
2235 config_file: tool_path.clone(),
2236 }],
2237 &maplit::btreeset! {ConfigExperimental::SetupScripts},
2238 &mut warnings,
2239 )
2240 .expect("config is valid");
2241
2242 assert_eq!(
2243 warnings.deprecated_scripts,
2244 id_hash_map! {
2245 DeprecatedScripts {
2246 tool: None,
2247 config_file: graph.workspace().root().join(".config/nextest.toml"),
2248 },
2249 DeprecatedScripts {
2250 tool: Some(tool_name("my-tool")),
2251 config_file: tool_path,
2252 }
2253 }
2254 );
2255 }
2256}