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 flaky_fail_status: profile_field!(self.junit.flaky_fail_status),
1246 };
1247 JunitConfig::new(self.store_dir(), settings)
1248 }
1249
1250 pub fn inherits(&self) -> Option<&str> {
1252 if let Some(custom_profile) = self.custom_profile {
1253 return custom_profile.inherits();
1254 }
1255 None
1256 }
1257
1258 #[cfg(test)]
1259 pub(in crate::config) fn custom_profile(&self) -> Option<&'cfg CustomProfileImpl> {
1260 self.custom_profile
1261 }
1262}
1263
1264#[derive(Clone, Debug)]
1265pub(in crate::config) struct NextestConfigImpl {
1266 store: StoreConfigImpl,
1267 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1268 scripts: ScriptConfig,
1269 default_profile: DefaultProfileImpl,
1270 other_profiles: HashMap<String, CustomProfileImpl>,
1271}
1272
1273impl NextestConfigImpl {
1274 fn get_profile(&self, profile: &str) -> Result<Option<&CustomProfileImpl>, ProfileNotFound> {
1275 let custom_profile = match profile {
1276 NextestConfig::DEFAULT_PROFILE => None,
1277 other => Some(
1278 self.other_profiles
1279 .get(other)
1280 .ok_or_else(|| ProfileNotFound::new(profile, self.all_profiles()))?,
1281 ),
1282 };
1283 Ok(custom_profile)
1284 }
1285
1286 fn all_profiles(&self) -> impl Iterator<Item = &str> {
1287 self.other_profiles
1288 .keys()
1289 .map(|key| key.as_str())
1290 .chain(std::iter::once(NextestConfig::DEFAULT_PROFILE))
1291 }
1292
1293 pub(in crate::config) fn default_profile(&self) -> &DefaultProfileImpl {
1294 &self.default_profile
1295 }
1296
1297 pub(in crate::config) fn other_profiles(
1298 &self,
1299 ) -> impl Iterator<Item = (&str, &CustomProfileImpl)> {
1300 self.other_profiles
1301 .iter()
1302 .map(|(key, value)| (key.as_str(), value))
1303 }
1304
1305 fn resolve_inheritance_chain(
1311 &self,
1312 profile_name: &str,
1313 ) -> Result<Vec<&CustomProfileImpl>, ProfileNotFound> {
1314 let mut chain = Vec::new();
1315
1316 let mut curr = self
1319 .get_profile(profile_name)?
1320 .and_then(|p| p.inherits.as_deref());
1321
1322 while let Some(name) = curr {
1323 let profile = self.get_profile(name)?;
1324 if let Some(profile) = profile {
1325 chain.push(profile);
1326 curr = profile.inherits.as_deref();
1327 } else {
1328 break;
1330 }
1331 }
1332
1333 Ok(chain)
1334 }
1335
1336 fn sanitize_profile_inherits(
1341 &self,
1342 known_profiles: &BTreeSet<String>,
1343 ) -> Result<(), ConfigParseErrorKind> {
1344 let mut inherit_err_collector = Vec::new();
1345
1346 self.sanitize_default_profile_inherits(&mut inherit_err_collector);
1347 self.sanitize_custom_profile_inherits(&mut inherit_err_collector, known_profiles);
1348
1349 if !inherit_err_collector.is_empty() {
1350 return Err(ConfigParseErrorKind::InheritanceErrors(
1351 inherit_err_collector,
1352 ));
1353 }
1354
1355 Ok(())
1356 }
1357
1358 fn sanitize_default_profile_inherits(&self, inherit_err_collector: &mut Vec<InheritsError>) {
1361 if self.default_profile().inherits().is_some() {
1362 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(
1363 NextestConfig::DEFAULT_PROFILE.to_string(),
1364 ));
1365 }
1366 }
1367
1368 fn sanitize_custom_profile_inherits(
1370 &self,
1371 inherit_err_collector: &mut Vec<InheritsError>,
1372 known_profiles: &BTreeSet<String>,
1373 ) {
1374 let mut profile_graph = Graph::<&str, (), Directed>::new();
1375 let mut profile_map = HashMap::new();
1376
1377 for (name, custom_profile) in self.other_profiles() {
1380 let starts_with_default = self.sanitize_custom_default_profile_inherits(
1381 name,
1382 custom_profile,
1383 inherit_err_collector,
1384 );
1385 if !starts_with_default {
1386 self.add_profile_to_graph(
1391 name,
1392 custom_profile,
1393 &mut profile_map,
1394 &mut profile_graph,
1395 inherit_err_collector,
1396 known_profiles,
1397 );
1398 }
1399 }
1400
1401 self.check_inheritance_cycles(profile_graph, inherit_err_collector);
1402 }
1403
1404 fn sanitize_custom_default_profile_inherits(
1407 &self,
1408 name: &str,
1409 custom_profile: &CustomProfileImpl,
1410 inherit_err_collector: &mut Vec<InheritsError>,
1411 ) -> bool {
1412 let starts_with_default = name.starts_with("default-");
1413
1414 if starts_with_default && custom_profile.inherits().is_some() {
1415 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(name.to_string()));
1416 }
1417
1418 starts_with_default
1419 }
1420
1421 fn add_profile_to_graph<'cfg>(
1426 &self,
1427 name: &'cfg str,
1428 custom_profile: &'cfg CustomProfileImpl,
1429 profile_map: &mut HashMap<&'cfg str, NodeIndex>,
1430 profile_graph: &mut Graph<&'cfg str, ()>,
1431 inherit_err_collector: &mut Vec<InheritsError>,
1432 known_profiles: &BTreeSet<String>,
1433 ) {
1434 if let Some(inherits_name) = custom_profile.inherits() {
1435 if inherits_name == name {
1436 inherit_err_collector
1437 .push(InheritsError::SelfReferentialInheritance(name.to_string()))
1438 } else if self.get_profile(inherits_name).is_ok() {
1439 let from_node = match profile_map.get(name) {
1441 None => {
1442 let profile_node = profile_graph.add_node(name);
1443 profile_map.insert(name, profile_node);
1444 profile_node
1445 }
1446 Some(node_idx) => *node_idx,
1447 };
1448 let to_node = match profile_map.get(inherits_name) {
1449 None => {
1450 let profile_node = profile_graph.add_node(inherits_name);
1451 profile_map.insert(inherits_name, profile_node);
1452 profile_node
1453 }
1454 Some(node_idx) => *node_idx,
1455 };
1456 profile_graph.add_edge(from_node, to_node, ());
1457 } else if known_profiles.contains(inherits_name) {
1458 } else {
1462 inherit_err_collector.push(InheritsError::UnknownInheritance(
1463 name.to_string(),
1464 inherits_name.to_string(),
1465 ))
1466 }
1467 }
1468 }
1469
1470 fn check_inheritance_cycles(
1472 &self,
1473 profile_graph: Graph<&str, ()>,
1474 inherit_err_collector: &mut Vec<InheritsError>,
1475 ) {
1476 let profile_sccs: Vec<Vec<NodeIndex>> = kosaraju_scc(&profile_graph);
1477 let profile_sccs: Vec<Vec<NodeIndex>> = profile_sccs
1478 .into_iter()
1479 .filter(|scc| scc.len() >= 2)
1480 .collect();
1481
1482 if !profile_sccs.is_empty() {
1483 inherit_err_collector.push(InheritsError::InheritanceCycle(
1484 profile_sccs
1485 .iter()
1486 .map(|node_idxs| {
1487 let profile_names: Vec<String> = node_idxs
1488 .iter()
1489 .map(|node_idx| profile_graph[*node_idx].to_string())
1490 .collect();
1491 profile_names
1492 })
1493 .collect(),
1494 ));
1495 }
1496 }
1497}
1498
1499#[derive(Clone, Debug, Deserialize)]
1505#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1506#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1507#[serde(rename_all = "kebab-case")]
1508pub(crate) struct NextestConfigDeserialize {
1509 #[cfg_attr(
1511 feature = "config-schema",
1512 schemars(with = "Option<StoreConfigImpl>")
1514 )]
1515 store: StoreConfigImpl,
1516
1517 #[expect(unused)]
1522 #[serde(default)]
1523 nextest_version: Option<NextestVersionDeserialize>,
1524
1525 #[expect(unused)]
1527 #[serde(default)]
1528 experimental: ExperimentalDeserialize,
1529
1530 #[serde(default)]
1533 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1534
1535 #[serde(default, rename = "script")]
1540 old_setup_scripts: IndexMap<ScriptId, SetupScriptConfig>,
1541
1542 #[serde(default)]
1544 scripts: ScriptConfig,
1545
1546 #[serde(rename = "profile")]
1548 #[cfg_attr(
1549 feature = "config-schema",
1550 schemars(with = "Option<HashMap<String, CustomProfileImpl>>")
1552 )]
1553 profiles: HashMap<String, CustomProfileImpl>,
1554}
1555
1556impl NextestConfigDeserialize {
1557 fn into_config_impl(mut self) -> NextestConfigImpl {
1558 let p = self
1559 .profiles
1560 .remove("default")
1561 .expect("default profile should exist");
1562 let default_profile = DefaultProfileImpl::new(p);
1563
1564 for (script_id, script_config) in self.old_setup_scripts {
1569 if let indexmap::map::Entry::Vacant(entry) = self.scripts.setup.entry(script_id) {
1570 entry.insert(script_config);
1571 }
1572 }
1573
1574 NextestConfigImpl {
1575 store: self.store,
1576 default_profile,
1577 test_groups: self.test_groups,
1578 scripts: self.scripts,
1579 other_profiles: self.profiles,
1580 }
1581 }
1582}
1583
1584#[cfg(feature = "config-schema")]
1594pub fn nextest_config_schema() -> schemars::Schema {
1595 let mut schema = schemars::schema_for!(NextestConfigDeserialize);
1596 schema.insert(
1598 "x-tombi-toml-version".to_owned(),
1599 serde_json::Value::String("v1.1.0".to_owned()),
1600 );
1601 schema
1602}
1603
1604#[derive(Clone, Debug, Deserialize)]
1605#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1606#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1607#[serde(rename_all = "kebab-case")]
1608struct StoreConfigImpl {
1609 #[cfg_attr(
1611 feature = "config-schema",
1612 schemars(with = "Option<String>")
1614 )]
1615 dir: Utf8PathBuf,
1616}
1617
1618#[derive(Clone, Debug)]
1619pub(in crate::config) struct DefaultProfileImpl {
1620 default_filter: String,
1621 test_threads: TestThreads,
1622 threads_required: ThreadsRequired,
1623 run_extra_args: Vec<String>,
1624 retries: RetryPolicy,
1625 flaky_result: FlakyResult,
1626 status_level: StatusLevel,
1627 final_status_level: FinalStatusLevel,
1628 failure_output: TestOutputDisplay,
1629 success_output: TestOutputDisplay,
1630 max_fail: MaxFail,
1631 slow_timeout: SlowTimeout,
1632 global_timeout: GlobalTimeout,
1633 leak_timeout: LeakTimeout,
1634 overrides: Vec<DeserializedOverride>,
1635 scripts: Vec<DeserializedProfileScriptConfig>,
1636 junit: DefaultJunitImpl,
1637 archive: ArchiveConfig,
1638 bench: DefaultBenchConfig,
1639 inherits: Inherits,
1640}
1641
1642impl DefaultProfileImpl {
1643 fn new(p: CustomProfileImpl) -> Self {
1644 Self {
1645 default_filter: p
1646 .default_filter
1647 .expect("default-filter present in default profile"),
1648 test_threads: p
1649 .test_threads
1650 .expect("test-threads present in default profile"),
1651 threads_required: p
1652 .threads_required
1653 .expect("threads-required present in default profile"),
1654 run_extra_args: p
1655 .run_extra_args
1656 .expect("run-extra-args present in default profile"),
1657 retries: p.retries.expect("retries present in default profile"),
1658 flaky_result: p
1659 .flaky_result
1660 .expect("flaky-result present in default profile"),
1661 status_level: p
1662 .status_level
1663 .expect("status-level present in default profile"),
1664 final_status_level: p
1665 .final_status_level
1666 .expect("final-status-level present in default profile"),
1667 failure_output: p
1668 .failure_output
1669 .expect("failure-output present in default profile"),
1670 success_output: p
1671 .success_output
1672 .expect("success-output present in default profile"),
1673 max_fail: p.max_fail.expect("fail-fast present in default profile"),
1674 slow_timeout: p
1675 .slow_timeout
1676 .expect("slow-timeout present in default profile"),
1677 global_timeout: p
1678 .global_timeout
1679 .expect("global-timeout present in default profile"),
1680 leak_timeout: p
1681 .leak_timeout
1682 .expect("leak-timeout present in default profile"),
1683 overrides: p.overrides,
1684 scripts: p.scripts,
1685 junit: DefaultJunitImpl::for_default_profile(p.junit),
1686 archive: p.archive.expect("archive present in default profile"),
1687 bench: DefaultBenchConfig::for_default_profile(
1688 p.bench.expect("bench present in default profile"),
1689 ),
1690 inherits: Inherits::new(p.inherits),
1691 }
1692 }
1693
1694 pub(in crate::config) fn default_filter(&self) -> &str {
1695 &self.default_filter
1696 }
1697
1698 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1699 self.inherits.inherits_from()
1700 }
1701
1702 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1703 &self.overrides
1704 }
1705
1706 pub(in crate::config) fn setup_scripts(&self) -> &[DeserializedProfileScriptConfig] {
1707 &self.scripts
1708 }
1709
1710 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1711 match run_mode {
1712 NextestRunMode::Test => self.slow_timeout,
1713 NextestRunMode::Benchmark => self.bench.slow_timeout,
1714 }
1715 }
1716
1717 pub(in crate::config) fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1718 match run_mode {
1719 NextestRunMode::Test => self.global_timeout,
1720 NextestRunMode::Benchmark => self.bench.global_timeout,
1721 }
1722 }
1723}
1724
1725#[derive(Clone, Debug, Deserialize)]
1726#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1727#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1728#[serde(rename_all = "kebab-case")]
1729pub(in crate::config) struct CustomProfileImpl {
1730 #[serde(default)]
1733 default_filter: Option<String>,
1734 #[serde(default, deserialize_with = "deserialize_retry_policy")]
1736 retries: Option<RetryPolicy>,
1737 #[serde(default)]
1739 flaky_result: Option<FlakyResult>,
1740 #[serde(default)]
1742 test_threads: Option<TestThreads>,
1743 #[serde(default)]
1745 threads_required: Option<ThreadsRequired>,
1746 #[serde(default)]
1748 run_extra_args: Option<Vec<String>>,
1749 #[serde(default)]
1751 status_level: Option<StatusLevel>,
1752 #[serde(default)]
1754 final_status_level: Option<FinalStatusLevel>,
1755 #[serde(default)]
1757 failure_output: Option<TestOutputDisplay>,
1758 #[serde(default)]
1760 success_output: Option<TestOutputDisplay>,
1761 #[serde(
1763 default,
1764 rename = "fail-fast",
1765 deserialize_with = "deserialize_fail_fast"
1766 )]
1767 max_fail: Option<MaxFail>,
1768 #[serde(default, deserialize_with = "deserialize_slow_timeout")]
1771 slow_timeout: Option<SlowTimeout>,
1772 #[serde(default)]
1774 global_timeout: Option<GlobalTimeout>,
1775 #[serde(default, deserialize_with = "deserialize_leak_timeout")]
1777 leak_timeout: Option<LeakTimeout>,
1778 #[serde(default)]
1780 overrides: Vec<DeserializedOverride>,
1781 #[serde(default)]
1783 scripts: Vec<DeserializedProfileScriptConfig>,
1784 #[serde(default)]
1786 junit: JunitImpl,
1787 #[serde(default)]
1789 archive: Option<ArchiveConfig>,
1790 #[serde(default)]
1792 bench: Option<BenchConfig>,
1793 #[serde(default)]
1795 inherits: Option<String>,
1796}
1797
1798impl CustomProfileImpl {
1799 #[cfg(test)]
1800 pub(in crate::config) fn test_threads(&self) -> Option<TestThreads> {
1801 self.test_threads
1802 }
1803
1804 pub(in crate::config) fn default_filter(&self) -> Option<&str> {
1805 self.default_filter.as_deref()
1806 }
1807
1808 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> Option<SlowTimeout> {
1809 match run_mode {
1810 NextestRunMode::Test => self.slow_timeout,
1811 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.slow_timeout),
1812 }
1813 }
1814
1815 pub(in crate::config) fn global_timeout(
1816 &self,
1817 run_mode: NextestRunMode,
1818 ) -> Option<GlobalTimeout> {
1819 match run_mode {
1820 NextestRunMode::Test => self.global_timeout,
1821 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.global_timeout),
1822 }
1823 }
1824
1825 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1826 self.inherits.as_deref()
1827 }
1828
1829 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1830 &self.overrides
1831 }
1832
1833 pub(in crate::config) fn scripts(&self) -> &[DeserializedProfileScriptConfig] {
1834 &self.scripts
1835 }
1836}
1837
1838#[cfg(test)]
1839mod tests {
1840 use super::*;
1841 use crate::config::utils::test_helpers::*;
1842 use camino_tempfile::tempdir;
1843 use iddqd::{IdHashItem, IdHashMap, id_hash_map, id_upcast};
1844
1845 fn tool_name(s: &str) -> ToolName {
1846 ToolName::new(s.into()).unwrap()
1847 }
1848
1849 #[derive(Default)]
1851 struct TestConfigWarnings {
1852 unknown_keys: IdHashMap<UnknownKeys>,
1853 reserved_profiles: IdHashMap<ReservedProfiles>,
1854 deprecated_scripts: IdHashMap<DeprecatedScripts>,
1855 empty_script_warnings: IdHashMap<EmptyScriptSections>,
1856 }
1857
1858 impl ConfigWarnings for TestConfigWarnings {
1859 fn unknown_config_keys(
1860 &mut self,
1861 config_file: &Utf8Path,
1862 _workspace_root: &Utf8Path,
1863 tool: Option<&ToolName>,
1864 unknown: &BTreeSet<String>,
1865 ) {
1866 self.unknown_keys
1867 .insert_unique(UnknownKeys {
1868 tool: tool.cloned(),
1869 config_file: config_file.to_owned(),
1870 keys: unknown.clone(),
1871 })
1872 .unwrap();
1873 }
1874
1875 fn unknown_reserved_profiles(
1876 &mut self,
1877 config_file: &Utf8Path,
1878 _workspace_root: &Utf8Path,
1879 tool: Option<&ToolName>,
1880 profiles: &[&str],
1881 ) {
1882 self.reserved_profiles
1883 .insert_unique(ReservedProfiles {
1884 tool: tool.cloned(),
1885 config_file: config_file.to_owned(),
1886 profiles: profiles.iter().map(|&s| s.to_owned()).collect(),
1887 })
1888 .unwrap();
1889 }
1890
1891 fn empty_script_sections(
1892 &mut self,
1893 config_file: &Utf8Path,
1894 _workspace_root: &Utf8Path,
1895 tool: Option<&ToolName>,
1896 profile_name: &str,
1897 empty_count: usize,
1898 ) {
1899 self.empty_script_warnings
1900 .insert_unique(EmptyScriptSections {
1901 tool: tool.cloned(),
1902 config_file: config_file.to_owned(),
1903 profile_name: profile_name.to_owned(),
1904 empty_count,
1905 })
1906 .unwrap();
1907 }
1908
1909 fn deprecated_script_config(
1910 &mut self,
1911 config_file: &Utf8Path,
1912 _workspace_root: &Utf8Path,
1913 tool: Option<&ToolName>,
1914 ) {
1915 self.deprecated_scripts
1916 .insert_unique(DeprecatedScripts {
1917 tool: tool.cloned(),
1918 config_file: config_file.to_owned(),
1919 })
1920 .unwrap();
1921 }
1922 }
1923
1924 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1925 struct UnknownKeys {
1926 tool: Option<ToolName>,
1927 config_file: Utf8PathBuf,
1928 keys: BTreeSet<String>,
1929 }
1930
1931 impl IdHashItem for UnknownKeys {
1932 type Key<'a> = Option<&'a ToolName>;
1933 fn key(&self) -> Self::Key<'_> {
1934 self.tool.as_ref()
1935 }
1936 id_upcast!();
1937 }
1938
1939 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1940 struct ReservedProfiles {
1941 tool: Option<ToolName>,
1942 config_file: Utf8PathBuf,
1943 profiles: Vec<String>,
1944 }
1945
1946 impl IdHashItem for ReservedProfiles {
1947 type Key<'a> = Option<&'a ToolName>;
1948 fn key(&self) -> Self::Key<'_> {
1949 self.tool.as_ref()
1950 }
1951 id_upcast!();
1952 }
1953
1954 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1955 struct DeprecatedScripts {
1956 tool: Option<ToolName>,
1957 config_file: Utf8PathBuf,
1958 }
1959
1960 impl IdHashItem for DeprecatedScripts {
1961 type Key<'a> = Option<&'a ToolName>;
1962 fn key(&self) -> Self::Key<'_> {
1963 self.tool.as_ref()
1964 }
1965 id_upcast!();
1966 }
1967
1968 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1969 struct EmptyScriptSections {
1970 tool: Option<ToolName>,
1971 config_file: Utf8PathBuf,
1972 profile_name: String,
1973 empty_count: usize,
1974 }
1975
1976 impl IdHashItem for EmptyScriptSections {
1977 type Key<'a> = (&'a Option<ToolName>, &'a str);
1978 fn key(&self) -> Self::Key<'_> {
1979 (&self.tool, &self.profile_name)
1980 }
1981 id_upcast!();
1982 }
1983
1984 #[test]
1985 fn default_config_is_valid() {
1986 let default_config = NextestConfig::default_config("foo");
1987 default_config
1988 .profile(NextestConfig::DEFAULT_PROFILE)
1989 .expect("default profile should exist");
1990 }
1991
1992 #[test]
1993 fn ignored_keys() {
1994 let config_contents = r#"
1995 ignored1 = "test"
1996
1997 [profile.default]
1998 retries = 3
1999 ignored2 = "hi"
2000
2001 [profile.default-foo]
2002 retries = 5
2003
2004 [[profile.default.overrides]]
2005 filter = 'test(test_foo)'
2006 retries = 20
2007 ignored3 = 42
2008 "#;
2009
2010 let tool_config_contents = r#"
2011 [store]
2012 ignored4 = 20
2013
2014 [profile.default]
2015 retries = 4
2016 ignored5 = false
2017
2018 [profile.default-bar]
2019 retries = 5
2020
2021 [profile.tool]
2022 retries = 12
2023
2024 [[profile.tool.overrides]]
2025 filter = 'test(test_baz)'
2026 retries = 22
2027 ignored6 = 6.5
2028 "#;
2029
2030 let workspace_dir = tempdir().unwrap();
2031
2032 let graph = temp_workspace(&workspace_dir, config_contents);
2033 let workspace_root = graph.workspace().root();
2034 let tool_path = workspace_root.join(".config/tool.toml");
2035 std::fs::write(&tool_path, tool_config_contents).unwrap();
2036
2037 let pcx = ParseContext::new(&graph);
2038
2039 let mut warnings = TestConfigWarnings::default();
2040
2041 let _ = NextestConfig::from_sources_with_warnings(
2042 workspace_root,
2043 &pcx,
2044 None,
2045 &[ToolConfigFile {
2046 tool: tool_name("my-tool"),
2047 config_file: tool_path.clone(),
2048 }][..],
2049 &Default::default(),
2050 &mut warnings,
2051 )
2052 .expect("config is valid");
2053
2054 assert_eq!(
2055 warnings.unknown_keys.len(),
2056 2,
2057 "there are two files with unknown keys"
2058 );
2059
2060 assert_eq!(
2061 warnings.unknown_keys,
2062 id_hash_map! {
2063 UnknownKeys {
2064 tool: None,
2065 config_file: workspace_root.join(".config/nextest.toml"),
2066 keys: maplit::btreeset! {
2067 "ignored1".to_owned(),
2068 "profile.default.ignored2".to_owned(),
2069 "profile.default.overrides.0.ignored3".to_owned(),
2070 }
2071 },
2072 UnknownKeys {
2073 tool: Some(tool_name("my-tool")),
2074 config_file: tool_path.clone(),
2075 keys: maplit::btreeset! {
2076 "store.ignored4".to_owned(),
2077 "profile.default.ignored5".to_owned(),
2078 "profile.tool.overrides.0.ignored6".to_owned(),
2079 }
2080 }
2081 }
2082 );
2083 assert_eq!(
2084 warnings.reserved_profiles,
2085 id_hash_map! {
2086 ReservedProfiles {
2087 tool: None,
2088 config_file: workspace_root.join(".config/nextest.toml"),
2089 profiles: vec!["default-foo".to_owned()],
2090 },
2091 ReservedProfiles {
2092 tool: Some(tool_name("my-tool")),
2093 config_file: tool_path,
2094 profiles: vec!["default-bar".to_owned()],
2095 }
2096 },
2097 )
2098 }
2099
2100 #[test]
2101 fn script_warnings() {
2102 let config_contents = r#"
2103 experimental = ["setup-scripts", "wrapper-scripts"]
2104
2105 [scripts.wrapper.script1]
2106 command = "echo test"
2107
2108 [scripts.wrapper.script2]
2109 command = "echo test2"
2110
2111 [scripts.setup.script3]
2112 command = "echo setup"
2113
2114 [[profile.default.scripts]]
2115 filter = 'all()'
2116 # Empty - no setup or wrapper scripts
2117
2118 [[profile.default.scripts]]
2119 filter = 'test(foo)'
2120 setup = ["script3"]
2121
2122 [profile.custom]
2123 [[profile.custom.scripts]]
2124 filter = 'all()'
2125 # Empty - no setup or wrapper scripts
2126
2127 [[profile.custom.scripts]]
2128 filter = 'test(bar)'
2129 # Another empty section
2130 "#;
2131
2132 let tool_config_contents = r#"
2133 experimental = ["setup-scripts", "wrapper-scripts"]
2134
2135 [scripts.wrapper."@tool:tool:disabled_script"]
2136 command = "echo disabled"
2137
2138 [scripts.setup."@tool:tool:setup_script"]
2139 command = "echo setup"
2140
2141 [profile.tool]
2142 [[profile.tool.scripts]]
2143 filter = 'all()'
2144 # Empty section
2145
2146 [[profile.tool.scripts]]
2147 filter = 'test(foo)'
2148 setup = ["@tool:tool:setup_script"]
2149 "#;
2150
2151 let workspace_dir = tempdir().unwrap();
2152 let graph = temp_workspace(&workspace_dir, config_contents);
2153 let workspace_root = graph.workspace().root();
2154 let tool_path = workspace_root.join(".config/tool.toml");
2155 std::fs::write(&tool_path, tool_config_contents).unwrap();
2156
2157 let pcx = ParseContext::new(&graph);
2158
2159 let mut warnings = TestConfigWarnings::default();
2160
2161 let experimental = maplit::btreeset! {
2162 ConfigExperimental::SetupScripts,
2163 ConfigExperimental::WrapperScripts
2164 };
2165 let _ = NextestConfig::from_sources_with_warnings(
2166 workspace_root,
2167 &pcx,
2168 None,
2169 &[ToolConfigFile {
2170 tool: tool_name("tool"),
2171 config_file: tool_path.clone(),
2172 }][..],
2173 &experimental,
2174 &mut warnings,
2175 )
2176 .expect("config is valid");
2177
2178 assert_eq!(
2179 warnings.empty_script_warnings,
2180 id_hash_map! {
2181 EmptyScriptSections {
2182 tool: None,
2183 config_file: workspace_root.join(".config/nextest.toml"),
2184 profile_name: "default".to_owned(),
2185 empty_count: 1,
2186 },
2187 EmptyScriptSections {
2188 tool: None,
2189 config_file: workspace_root.join(".config/nextest.toml"),
2190 profile_name: "custom".to_owned(),
2191 empty_count: 2,
2192 },
2193 EmptyScriptSections {
2194 tool: Some(tool_name("tool")),
2195 config_file: tool_path,
2196 profile_name: "tool".to_owned(),
2197 empty_count: 1,
2198 }
2199 }
2200 );
2201 }
2202
2203 #[test]
2204 fn deprecated_script_config_warning() {
2205 let config_contents = r#"
2206 experimental = ["setup-scripts"]
2207
2208 [script.my-script]
2209 command = "echo hello"
2210"#;
2211
2212 let tool_config_contents = r#"
2213 experimental = ["setup-scripts"]
2214
2215 [script."@tool:my-tool:my-script"]
2216 command = "echo hello"
2217"#;
2218
2219 let temp_dir = tempdir().unwrap();
2220
2221 let graph = temp_workspace(&temp_dir, config_contents);
2222 let workspace_root = graph.workspace().root();
2223 let tool_path = workspace_root.join(".config/my-tool.toml");
2224 std::fs::write(&tool_path, tool_config_contents).unwrap();
2225 let pcx = ParseContext::new(&graph);
2226
2227 let mut warnings = TestConfigWarnings::default();
2228 NextestConfig::from_sources_with_warnings(
2229 graph.workspace().root(),
2230 &pcx,
2231 None,
2232 &[ToolConfigFile {
2233 tool: tool_name("my-tool"),
2234 config_file: tool_path.clone(),
2235 }],
2236 &maplit::btreeset! {ConfigExperimental::SetupScripts},
2237 &mut warnings,
2238 )
2239 .expect("config is valid");
2240
2241 assert_eq!(
2242 warnings.deprecated_scripts,
2243 id_hash_map! {
2244 DeprecatedScripts {
2245 tool: None,
2246 config_file: graph.workspace().root().join(".config/nextest.toml"),
2247 },
2248 DeprecatedScripts {
2249 tool: Some(tool_name("my-tool")),
2250 config_file: tool_path,
2251 }
2252 }
2253 );
2254 }
2255}