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 SCHEMA: &'static str = include_str!("../../../jsonschemas/repo-config.json");
236
237 pub const ENVIRONMENT_PREFIX: &'static str = "NEXTEST";
239
240 pub const DEFAULT_PROFILE: &'static str = "default";
242
243 pub const DEFAULT_MIRI_PROFILE: &'static str = "default-miri";
245
246 pub const DEFAULT_PROFILES: &'static [&'static str] =
248 &[Self::DEFAULT_PROFILE, Self::DEFAULT_MIRI_PROFILE];
249
250 pub fn from_sources<'a, I>(
260 workspace_root: impl Into<Utf8PathBuf>,
261 pcx: &ParseContext<'_>,
262 config_file: Option<&Utf8Path>,
263 tool_config_files: impl IntoIterator<IntoIter = I>,
264 experimental: &BTreeSet<ConfigExperimental>,
265 ) -> Result<Self, ConfigParseError>
266 where
267 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
268 {
269 Self::from_sources_with_warnings(
270 workspace_root,
271 pcx,
272 config_file,
273 tool_config_files,
274 experimental,
275 &mut DefaultConfigWarnings,
276 )
277 }
278
279 pub fn from_sources_with_warnings<'a, I>(
281 workspace_root: impl Into<Utf8PathBuf>,
282 pcx: &ParseContext<'_>,
283 config_file: Option<&Utf8Path>,
284 tool_config_files: impl IntoIterator<IntoIter = I>,
285 experimental: &BTreeSet<ConfigExperimental>,
286 warnings: &mut impl ConfigWarnings,
287 ) -> Result<Self, ConfigParseError>
288 where
289 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
290 {
291 Self::from_sources_impl(
292 workspace_root,
293 pcx,
294 config_file,
295 tool_config_files,
296 experimental,
297 warnings,
298 )
299 }
300
301 fn from_sources_impl<'a, I>(
303 workspace_root: impl Into<Utf8PathBuf>,
304 pcx: &ParseContext<'_>,
305 config_file: Option<&Utf8Path>,
306 tool_config_files: impl IntoIterator<IntoIter = I>,
307 experimental: &BTreeSet<ConfigExperimental>,
308 warnings: &mut impl ConfigWarnings,
309 ) -> Result<Self, ConfigParseError>
310 where
311 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
312 {
313 let workspace_root = workspace_root.into();
314 let tool_config_files_rev = tool_config_files.into_iter().rev();
315 let (inner, compiled) = Self::read_from_sources(
316 pcx,
317 &workspace_root,
318 config_file,
319 tool_config_files_rev,
320 experimental,
321 warnings,
322 )?;
323 Ok(Self {
324 workspace_root,
325 inner,
326 compiled,
327 })
328 }
329
330 #[cfg(test)]
332 pub(crate) fn default_config(workspace_root: impl Into<Utf8PathBuf>) -> Self {
333 use itertools::Itertools;
334
335 let config = Self::make_default_config()
336 .build()
337 .expect("default config is always valid");
338
339 let mut unknown = BTreeSet::new();
340 let deserialized: NextestConfigDeserialize =
341 serde_ignored::deserialize(config, |path: serde_ignored::Path| {
342 unknown.insert(path.to_string());
343 })
344 .expect("default config is always valid");
345
346 if !unknown.is_empty() {
349 panic!(
350 "found unknown keys in default config: {}",
351 unknown.iter().join(", ")
352 );
353 }
354
355 Self {
356 workspace_root: workspace_root.into(),
357 inner: deserialized.into_config_impl(),
358 compiled: CompiledByProfile::for_default_config(),
360 }
361 }
362
363 pub fn profile(&self, name: impl AsRef<str>) -> Result<EarlyProfile<'_>, ProfileNotFound> {
366 self.make_profile(name.as_ref())
367 }
368
369 fn read_from_sources<'a>(
374 pcx: &ParseContext<'_>,
375 workspace_root: &Utf8Path,
376 file: Option<&Utf8Path>,
377 tool_config_files_rev: impl Iterator<Item = &'a ToolConfigFile>,
378 experimental: &BTreeSet<ConfigExperimental>,
379 warnings: &mut impl ConfigWarnings,
380 ) -> Result<(NextestConfigImpl, CompiledByProfile), ConfigParseError> {
381 let mut composite_builder = Self::make_default_config();
383
384 let mut compiled = CompiledByProfile::for_default_config();
387
388 let mut known_groups = BTreeSet::new();
389 let mut known_scripts = IdOrdMap::new();
390 let mut known_profiles = BTreeSet::new();
393
394 for ToolConfigFile { config_file, tool } in tool_config_files_rev {
396 let source = File::new(config_file.as_str(), FileFormat::Toml);
397 Self::deserialize_individual_config(
398 pcx,
399 workspace_root,
400 config_file,
401 Some(tool),
402 source.clone(),
403 &mut compiled,
404 experimental,
405 warnings,
406 &mut known_groups,
407 &mut known_scripts,
408 &mut known_profiles,
409 )?;
410
411 composite_builder = composite_builder.add_source(source);
413 }
414
415 let (config_file, source) = match file {
417 Some(file) => (file.to_owned(), File::new(file.as_str(), FileFormat::Toml)),
418 None => {
419 let config_file = workspace_root.join(Self::CONFIG_PATH);
420 let source = File::new(config_file.as_str(), FileFormat::Toml).required(false);
421 (config_file, source)
422 }
423 };
424
425 Self::deserialize_individual_config(
426 pcx,
427 workspace_root,
428 &config_file,
429 None,
430 source.clone(),
431 &mut compiled,
432 experimental,
433 warnings,
434 &mut known_groups,
435 &mut known_scripts,
436 &mut known_profiles,
437 )?;
438
439 composite_builder = composite_builder.add_source(source);
440
441 let (config, _unknown) = Self::build_and_deserialize_config(&composite_builder)
444 .map_err(|kind| ConfigParseError::new(&config_file, None, kind))?;
445
446 compiled.default.reverse();
448 for data in compiled.other.values_mut() {
449 data.reverse();
450 }
451
452 Ok((config.into_config_impl(), compiled))
453 }
454
455 #[expect(clippy::too_many_arguments)]
456 fn deserialize_individual_config(
457 pcx: &ParseContext<'_>,
458 workspace_root: &Utf8Path,
459 config_file: &Utf8Path,
460 tool: Option<&ToolName>,
461 source: File<FileSourceFile, FileFormat>,
462 compiled_out: &mut CompiledByProfile,
463 experimental: &BTreeSet<ConfigExperimental>,
464 warnings: &mut impl ConfigWarnings,
465 known_groups: &mut BTreeSet<CustomTestGroup>,
466 known_scripts: &mut IdOrdMap<ScriptInfo>,
467 known_profiles: &mut BTreeSet<String>,
468 ) -> Result<(), ConfigParseError> {
469 let default_builder = Self::make_default_config();
472 let this_builder = default_builder.add_source(source);
473 let (mut this_config, unknown) = Self::build_and_deserialize_config(&this_builder)
474 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
475
476 if !unknown.is_empty() {
477 warnings.unknown_config_keys(config_file, workspace_root, tool, &unknown);
478 }
479
480 let (valid_groups, invalid_groups): (BTreeSet<_>, _) =
482 this_config.test_groups.keys().cloned().partition(|group| {
483 if let Some(tool) = tool {
484 group
486 .as_identifier()
487 .tool_components()
488 .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
489 } else {
490 !group.as_identifier().is_tool_identifier()
492 }
493 });
494
495 if !invalid_groups.is_empty() {
496 let kind = if tool.is_some() {
497 ConfigParseErrorKind::InvalidTestGroupsDefinedByTool(invalid_groups)
498 } else {
499 ConfigParseErrorKind::InvalidTestGroupsDefined(invalid_groups)
500 };
501 return Err(ConfigParseError::new(config_file, tool, kind));
502 }
503
504 known_groups.extend(valid_groups);
505
506 if !this_config.scripts.is_empty() && !this_config.old_setup_scripts.is_empty() {
508 return Err(ConfigParseError::new(
509 config_file,
510 tool,
511 ConfigParseErrorKind::BothScriptAndScriptsDefined,
512 ));
513 }
514
515 if !this_config.old_setup_scripts.is_empty() {
517 warnings.deprecated_script_config(config_file, workspace_root, tool);
518 this_config.scripts.setup = this_config.old_setup_scripts.clone();
519 }
520
521 {
523 let mut missing_features = BTreeSet::new();
524 if !this_config.scripts.setup.is_empty()
525 && !experimental.contains(&ConfigExperimental::SetupScripts)
526 {
527 missing_features.insert(ConfigExperimental::SetupScripts);
528 }
529 if !this_config.scripts.wrapper.is_empty()
530 && !experimental.contains(&ConfigExperimental::WrapperScripts)
531 {
532 missing_features.insert(ConfigExperimental::WrapperScripts);
533 }
534 if !missing_features.is_empty() {
535 return Err(ConfigParseError::new(
536 config_file,
537 tool,
538 ConfigParseErrorKind::ExperimentalFeaturesNotEnabled { missing_features },
539 ));
540 }
541 }
542
543 let duplicate_ids: BTreeSet<_> = this_config.scripts.duplicate_ids().cloned().collect();
544 if !duplicate_ids.is_empty() {
545 return Err(ConfigParseError::new(
546 config_file,
547 tool,
548 ConfigParseErrorKind::DuplicateConfigScriptNames(duplicate_ids),
549 ));
550 }
551
552 let (valid_scripts, invalid_scripts): (BTreeSet<_>, _) = this_config
554 .scripts
555 .all_script_ids()
556 .cloned()
557 .partition(|script| {
558 if let Some(tool) = tool {
559 script
561 .as_identifier()
562 .tool_components()
563 .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
564 } else {
565 !script.as_identifier().is_tool_identifier()
567 }
568 });
569
570 if !invalid_scripts.is_empty() {
571 let kind = if tool.is_some() {
572 ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool(invalid_scripts)
573 } else {
574 ConfigParseErrorKind::InvalidConfigScriptsDefined(invalid_scripts)
575 };
576 return Err(ConfigParseError::new(config_file, tool, kind));
577 }
578
579 known_scripts.extend(
580 valid_scripts
581 .into_iter()
582 .map(|id| this_config.scripts.script_info(id)),
583 );
584
585 let this_config = this_config.into_config_impl();
586
587 let unknown_default_profiles: Vec<_> = this_config
588 .all_profiles()
589 .filter(|p| p.starts_with("default-") && !NextestConfig::DEFAULT_PROFILES.contains(p))
590 .collect();
591 if !unknown_default_profiles.is_empty() {
592 warnings.unknown_reserved_profiles(
593 config_file,
594 workspace_root,
595 tool,
596 &unknown_default_profiles,
597 );
598 }
599
600 this_config
604 .sanitize_profile_inherits(known_profiles)
605 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
606
607 known_profiles.extend(
609 this_config
610 .other_profiles()
611 .map(|(name, _)| name.to_owned()),
612 );
613
614 let this_compiled = CompiledByProfile::new(pcx, &this_config)
616 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
617
618 let mut unknown_group_errors = Vec::new();
620 let mut check_test_group = |profile_name: &str, test_group: Option<&TestGroup>| {
621 if let Some(TestGroup::Custom(group)) = test_group
622 && !known_groups.contains(group)
623 {
624 unknown_group_errors.push(UnknownTestGroupError {
625 profile_name: profile_name.to_owned(),
626 name: TestGroup::Custom(group.clone()),
627 });
628 }
629 };
630
631 this_compiled
632 .default
633 .overrides
634 .iter()
635 .for_each(|override_| {
636 check_test_group("default", override_.data.test_group.as_ref());
637 });
638
639 this_compiled.other.iter().for_each(|(profile_name, data)| {
641 data.overrides.iter().for_each(|override_| {
642 check_test_group(profile_name, override_.data.test_group.as_ref());
643 });
644 });
645
646 if !unknown_group_errors.is_empty() {
648 let known_groups = TestGroup::make_all_groups(known_groups.iter().cloned()).collect();
649 return Err(ConfigParseError::new(
650 config_file,
651 tool,
652 ConfigParseErrorKind::UnknownTestGroups {
653 errors: unknown_group_errors,
654 known_groups,
655 },
656 ));
657 }
658
659 let mut profile_script_errors = ProfileScriptErrors::default();
662 let mut check_script_ids = |profile_name: &str,
663 script_type: ProfileScriptType,
664 expr: Option<&Filterset>,
665 scripts: &[ScriptId]| {
666 for script in scripts {
667 if let Some(script_info) = known_scripts.get(script) {
668 if !script_info.script_type.matches(script_type) {
669 profile_script_errors.wrong_script_types.push(
670 ProfileWrongConfigScriptTypeError {
671 profile_name: profile_name.to_owned(),
672 name: script.clone(),
673 attempted: script_type,
674 actual: script_info.script_type,
675 },
676 );
677 }
678 if script_type == ProfileScriptType::ListWrapper
679 && let Some(expr) = expr
680 {
681 let runtime_only_leaves = expr.parsed.runtime_only_leaves();
682 if !runtime_only_leaves.is_empty() {
683 let filters = runtime_only_leaves
684 .iter()
685 .map(|leaf| leaf.to_string())
686 .collect();
687 profile_script_errors.list_scripts_using_run_filters.push(
688 ProfileListScriptUsesRunFiltersError {
689 profile_name: profile_name.to_owned(),
690 name: script.clone(),
691 script_type,
692 filters,
693 },
694 );
695 }
696 }
697 } else {
698 profile_script_errors
699 .unknown_scripts
700 .push(ProfileUnknownScriptError {
701 profile_name: profile_name.to_owned(),
702 name: script.clone(),
703 });
704 }
705 }
706 };
707
708 let mut empty_script_count = 0;
709
710 this_compiled.default.scripts.iter().for_each(|scripts| {
711 if scripts.setup.is_empty()
712 && scripts.list_wrapper.is_none()
713 && scripts.run_wrapper.is_none()
714 {
715 empty_script_count += 1;
716 }
717
718 check_script_ids(
719 "default",
720 ProfileScriptType::Setup,
721 scripts.data.expr(),
722 &scripts.setup,
723 );
724 check_script_ids(
725 "default",
726 ProfileScriptType::ListWrapper,
727 scripts.data.expr(),
728 scripts.list_wrapper.as_slice(),
729 );
730 check_script_ids(
731 "default",
732 ProfileScriptType::RunWrapper,
733 scripts.data.expr(),
734 scripts.run_wrapper.as_slice(),
735 );
736 });
737
738 if empty_script_count > 0 {
739 warnings.empty_script_sections(
740 config_file,
741 workspace_root,
742 tool,
743 "default",
744 empty_script_count,
745 );
746 }
747
748 this_compiled.other.iter().for_each(|(profile_name, data)| {
749 let mut empty_script_count = 0;
750 data.scripts.iter().for_each(|scripts| {
751 if scripts.setup.is_empty()
752 && scripts.list_wrapper.is_none()
753 && scripts.run_wrapper.is_none()
754 {
755 empty_script_count += 1;
756 }
757
758 check_script_ids(
759 profile_name,
760 ProfileScriptType::Setup,
761 scripts.data.expr(),
762 &scripts.setup,
763 );
764 check_script_ids(
765 profile_name,
766 ProfileScriptType::ListWrapper,
767 scripts.data.expr(),
768 scripts.list_wrapper.as_slice(),
769 );
770 check_script_ids(
771 profile_name,
772 ProfileScriptType::RunWrapper,
773 scripts.data.expr(),
774 scripts.run_wrapper.as_slice(),
775 );
776 });
777
778 if empty_script_count > 0 {
779 warnings.empty_script_sections(
780 config_file,
781 workspace_root,
782 tool,
783 profile_name,
784 empty_script_count,
785 );
786 }
787 });
788
789 if !profile_script_errors.is_empty() {
792 let known_scripts = known_scripts
793 .iter()
794 .map(|script| script.id.clone())
795 .collect();
796 return Err(ConfigParseError::new(
797 config_file,
798 tool,
799 ConfigParseErrorKind::ProfileScriptErrors {
800 errors: Box::new(profile_script_errors),
801 known_scripts,
802 },
803 ));
804 }
805
806 compiled_out.default.extend_reverse(this_compiled.default);
809 for (name, mut data) in this_compiled.other {
810 match compiled_out.other.entry(name) {
811 hash_map::Entry::Vacant(entry) => {
812 data.reverse();
814 entry.insert(data);
815 }
816 hash_map::Entry::Occupied(mut entry) => {
817 entry.get_mut().extend_reverse(data);
819 }
820 }
821 }
822
823 Ok(())
824 }
825
826 fn make_default_config() -> ConfigBuilder<DefaultState> {
827 Config::builder().add_source(File::from_str(Self::DEFAULT_CONFIG, FileFormat::Toml))
828 }
829
830 fn make_profile(&self, name: &str) -> Result<EarlyProfile<'_>, ProfileNotFound> {
831 let custom_profile = self.inner.get_profile(name)?;
832
833 let inheritance_chain = self.inner.resolve_inheritance_chain(name)?;
835
836 let mut store_dir = self.workspace_root.join(&self.inner.store.dir);
838 store_dir.push(name);
839
840 let compiled_data = match self.compiled.other.get(name) {
842 Some(data) => data.clone().chain(self.compiled.default.clone()),
843 None => self.compiled.default.clone(),
844 };
845
846 Ok(EarlyProfile {
847 name: name.to_owned(),
848 store_dir,
849 default_profile: &self.inner.default_profile,
850 custom_profile,
851 inheritance_chain,
852 test_groups: &self.inner.test_groups,
853 scripts: &self.inner.scripts,
854 compiled_data,
855 })
856 }
857
858 fn build_and_deserialize_config(
860 builder: &ConfigBuilder<DefaultState>,
861 ) -> Result<(NextestConfigDeserialize, BTreeSet<String>), ConfigParseErrorKind> {
862 let config = builder
863 .build_cloned()
864 .map_err(|error| ConfigParseErrorKind::BuildError(Box::new(error)))?;
865
866 let mut ignored = BTreeSet::new();
867 let mut cb = |path: serde_ignored::Path| {
868 ignored.insert(path.to_string());
869 };
870 let ignored_de = serde_ignored::Deserializer::new(config, &mut cb);
871 let config: NextestConfigDeserialize = serde_path_to_error::deserialize(ignored_de)
872 .map_err(|error| {
873 let path = error.path().clone();
877 let config_error = error.into_inner();
878 let error = match config_error {
879 ConfigError::At { error, .. } => *error,
880 other => other,
881 };
882 ConfigParseErrorKind::DeserializeError(Box::new(serde_path_to_error::Error::new(
883 path, error,
884 )))
885 })?;
886
887 Ok((config, ignored))
888 }
889}
890
891#[derive(Clone, Debug, Default)]
893pub(in crate::config) struct PreBuildPlatform {}
894
895#[derive(Clone, Debug)]
897pub(crate) struct FinalConfig {
898 pub(in crate::config) host_eval: bool,
900 pub(in crate::config) host_test_eval: bool,
903 pub(in crate::config) target_eval: bool,
906}
907
908pub struct EarlyProfile<'cfg> {
913 name: String,
914 store_dir: Utf8PathBuf,
915 default_profile: &'cfg DefaultProfileImpl,
916 custom_profile: Option<&'cfg CustomProfileImpl>,
917 inheritance_chain: Vec<&'cfg CustomProfileImpl>,
918 test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
919 scripts: &'cfg ScriptConfig,
921 pub(in crate::config) compiled_data: CompiledData<PreBuildPlatform>,
923}
924
925macro_rules! profile_field {
928 ($eval_prof:ident.$field:ident) => {
929 $eval_prof
930 .custom_profile
931 .iter()
932 .chain($eval_prof.inheritance_chain.iter())
933 .find_map(|p| p.$field)
934 .unwrap_or($eval_prof.default_profile.$field)
935 };
936 ($eval_prof:ident.$nested:ident.$field:ident) => {
937 $eval_prof
938 .custom_profile
939 .iter()
940 .chain($eval_prof.inheritance_chain.iter())
941 .find_map(|p| p.$nested.$field)
942 .unwrap_or($eval_prof.default_profile.$nested.$field)
943 };
944 ($eval_prof:ident.$method:ident($($arg:expr),*)) => {
946 $eval_prof
947 .custom_profile
948 .iter()
949 .chain($eval_prof.inheritance_chain.iter())
950 .find_map(|p| p.$method($($arg),*))
951 .unwrap_or_else(|| $eval_prof.default_profile.$method($($arg),*))
952 };
953}
954macro_rules! profile_field_from_ref {
955 ($eval_prof:ident.$field:ident.$ref_func:ident()) => {
956 $eval_prof
957 .custom_profile
958 .iter()
959 .chain($eval_prof.inheritance_chain.iter())
960 .find_map(|p| p.$field.$ref_func())
961 .unwrap_or(&$eval_prof.default_profile.$field)
962 };
963 ($eval_prof:ident.$nested:ident.$field:ident.$ref_func:ident()) => {
964 $eval_prof
965 .custom_profile
966 .iter()
967 .chain($eval_prof.inheritance_chain.iter())
968 .find_map(|p| p.$nested.$field.$ref_func())
969 .unwrap_or(&$eval_prof.default_profile.$nested.$field)
970 };
971}
972macro_rules! profile_field_optional {
974 ($eval_prof:ident.$nested:ident.$field:ident.$ref_func:ident()) => {
975 $eval_prof
976 .custom_profile
977 .iter()
978 .chain($eval_prof.inheritance_chain.iter())
979 .find_map(|p| p.$nested.$field.$ref_func())
980 .or($eval_prof.default_profile.$nested.$field.$ref_func())
981 };
982}
983
984impl<'cfg> EarlyProfile<'cfg> {
985 pub fn store_dir(&self) -> &Utf8Path {
987 &self.store_dir
988 }
989
990 pub fn has_junit(&self) -> bool {
992 profile_field_optional!(self.junit.path.as_deref()).is_some()
993 }
994
995 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
997 self.test_groups
998 }
999
1000 pub fn known_groups(&self) -> KnownGroups {
1005 let custom_groups = self
1006 .test_group_config()
1007 .keys()
1008 .map(|g| g.to_string())
1009 .collect();
1010 KnownGroups::Known { custom_groups }
1011 }
1012
1013 pub fn apply_build_platforms(
1018 self,
1019 build_platforms: &BuildPlatforms,
1020 ) -> EvaluatableProfile<'cfg> {
1021 let compiled_data = self.compiled_data.apply_build_platforms(build_platforms);
1022
1023 let resolved_default_filter = {
1024 let found_filter = compiled_data
1026 .overrides
1027 .iter()
1028 .find_map(|override_data| override_data.default_filter_if_matches_platform());
1029 found_filter.unwrap_or_else(|| {
1030 compiled_data
1033 .profile_default_filter
1034 .as_ref()
1035 .expect("compiled data always has default set")
1036 })
1037 }
1038 .clone();
1039
1040 EvaluatableProfile {
1041 name: self.name,
1042 store_dir: self.store_dir,
1043 default_profile: self.default_profile,
1044 custom_profile: self.custom_profile,
1045 inheritance_chain: self.inheritance_chain,
1046 scripts: self.scripts,
1047 test_groups: self.test_groups,
1048 compiled_data,
1049 resolved_default_filter,
1050 }
1051 }
1052}
1053
1054#[derive(Clone, Debug)]
1058pub struct EvaluatableProfile<'cfg> {
1059 name: String,
1060 store_dir: Utf8PathBuf,
1061 default_profile: &'cfg DefaultProfileImpl,
1062 custom_profile: Option<&'cfg CustomProfileImpl>,
1063 inheritance_chain: Vec<&'cfg CustomProfileImpl>,
1064 test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
1065 scripts: &'cfg ScriptConfig,
1067 pub(in crate::config) compiled_data: CompiledData<FinalConfig>,
1069 resolved_default_filter: CompiledDefaultFilter,
1072}
1073
1074impl<'cfg> EvaluatableProfile<'cfg> {
1075 pub fn name(&self) -> &str {
1077 &self.name
1078 }
1079
1080 pub fn store_dir(&self) -> &Utf8Path {
1082 &self.store_dir
1083 }
1084
1085 pub fn filterset_ecx(&self) -> EvalContext<'_> {
1087 EvalContext {
1088 default_filter: &self.default_filter().expr,
1089 }
1090 }
1091
1092 pub fn precompute_group_memberships<'a>(
1100 &self,
1101 tests: impl Iterator<Item = TestQuery<'a>>,
1102 ) -> PrecomputedGroupMembership {
1103 let run_mode = NextestRunMode::Test;
1106
1107 let mut membership = PrecomputedGroupMembership::empty();
1108 for test in tests {
1109 let group = self.settings_for(run_mode, &test).test_group().clone();
1110 if group != TestGroup::Global {
1111 let id = TestInstanceId {
1112 binary_id: test.binary_query.binary_id,
1113 test_name: test.test_name,
1114 };
1115 membership.insert(id.to_owned(), group);
1116 }
1117 }
1118 membership
1119 }
1120
1121 pub fn default_filter(&self) -> &CompiledDefaultFilter {
1123 &self.resolved_default_filter
1124 }
1125
1126 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
1128 self.test_groups
1129 }
1130
1131 pub fn script_config(&self) -> &'cfg ScriptConfig {
1133 self.scripts
1134 }
1135
1136 pub fn retries(&self) -> RetryPolicy {
1138 profile_field!(self.retries)
1139 }
1140
1141 pub fn flaky_result(&self) -> FlakyResult {
1143 profile_field!(self.flaky_result)
1144 }
1145
1146 pub fn test_threads(&self) -> TestThreads {
1148 profile_field!(self.test_threads)
1149 }
1150
1151 pub fn threads_required(&self) -> ThreadsRequired {
1153 profile_field!(self.threads_required)
1154 }
1155
1156 pub fn run_extra_args(&self) -> &'cfg [String] {
1158 profile_field_from_ref!(self.run_extra_args.as_deref())
1159 }
1160
1161 pub fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1163 profile_field!(self.slow_timeout(run_mode))
1164 }
1165
1166 pub fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1168 profile_field!(self.global_timeout(run_mode))
1169 }
1170
1171 pub fn leak_timeout(&self) -> LeakTimeout {
1174 profile_field!(self.leak_timeout)
1175 }
1176
1177 pub fn status_level(&self) -> StatusLevel {
1179 profile_field!(self.status_level)
1180 }
1181
1182 pub fn final_status_level(&self) -> FinalStatusLevel {
1184 profile_field!(self.final_status_level)
1185 }
1186
1187 pub fn failure_output(&self) -> TestOutputDisplay {
1189 profile_field!(self.failure_output)
1190 }
1191
1192 pub fn success_output(&self) -> TestOutputDisplay {
1194 profile_field!(self.success_output)
1195 }
1196
1197 pub fn max_fail(&self) -> MaxFail {
1199 profile_field!(self.max_fail)
1200 }
1201
1202 pub fn archive_config(&self) -> &'cfg ArchiveConfig {
1204 profile_field_from_ref!(self.archive.as_ref())
1205 }
1206
1207 pub fn setup_scripts(&self, test_list: &TestList<'_>) -> SetupScripts<'_> {
1209 SetupScripts::new(self, test_list)
1210 }
1211
1212 pub fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1214 ListSettings::new(self, query)
1215 }
1216
1217 pub fn settings_for(
1219 &self,
1220 run_mode: NextestRunMode,
1221 query: &TestQuery<'_>,
1222 ) -> TestSettings<'_> {
1223 TestSettings::new(self, run_mode, query)
1224 }
1225
1226 pub(crate) fn settings_with_source_for(
1228 &self,
1229 run_mode: NextestRunMode,
1230 query: &TestQuery<'_>,
1231 ) -> TestSettings<'_, SettingSource<'_>> {
1232 TestSettings::new(self, run_mode, query)
1233 }
1234
1235 pub fn junit(&self) -> Option<JunitConfig<'cfg>> {
1237 let settings = JunitSettings {
1238 path: profile_field_optional!(self.junit.path.as_deref()),
1239 report_name: profile_field_from_ref!(self.junit.report_name.as_deref()),
1240 store_success_output: profile_field!(self.junit.store_success_output),
1241 store_failure_output: profile_field!(self.junit.store_failure_output),
1242 flaky_fail_status: profile_field!(self.junit.flaky_fail_status),
1243 };
1244 JunitConfig::new(self.store_dir(), settings)
1245 }
1246
1247 pub fn inherits(&self) -> Option<&str> {
1249 if let Some(custom_profile) = self.custom_profile {
1250 return custom_profile.inherits();
1251 }
1252 None
1253 }
1254
1255 #[cfg(test)]
1256 pub(in crate::config) fn custom_profile(&self) -> Option<&'cfg CustomProfileImpl> {
1257 self.custom_profile
1258 }
1259}
1260
1261#[derive(Clone, Debug)]
1262pub(in crate::config) struct NextestConfigImpl {
1263 store: StoreConfigImpl,
1264 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1265 scripts: ScriptConfig,
1266 default_profile: DefaultProfileImpl,
1267 other_profiles: HashMap<String, CustomProfileImpl>,
1268}
1269
1270impl NextestConfigImpl {
1271 fn get_profile(&self, profile: &str) -> Result<Option<&CustomProfileImpl>, ProfileNotFound> {
1272 let custom_profile = match profile {
1273 NextestConfig::DEFAULT_PROFILE => None,
1274 other => Some(
1275 self.other_profiles
1276 .get(other)
1277 .ok_or_else(|| ProfileNotFound::new(profile, self.all_profiles()))?,
1278 ),
1279 };
1280 Ok(custom_profile)
1281 }
1282
1283 fn all_profiles(&self) -> impl Iterator<Item = &str> {
1284 self.other_profiles
1285 .keys()
1286 .map(|key| key.as_str())
1287 .chain(std::iter::once(NextestConfig::DEFAULT_PROFILE))
1288 }
1289
1290 pub(in crate::config) fn default_profile(&self) -> &DefaultProfileImpl {
1291 &self.default_profile
1292 }
1293
1294 pub(in crate::config) fn other_profiles(
1295 &self,
1296 ) -> impl Iterator<Item = (&str, &CustomProfileImpl)> {
1297 self.other_profiles
1298 .iter()
1299 .map(|(key, value)| (key.as_str(), value))
1300 }
1301
1302 fn resolve_inheritance_chain(
1308 &self,
1309 profile_name: &str,
1310 ) -> Result<Vec<&CustomProfileImpl>, ProfileNotFound> {
1311 let mut chain = Vec::new();
1312
1313 let mut curr = self
1316 .get_profile(profile_name)?
1317 .and_then(|p| p.inherits.as_deref());
1318
1319 while let Some(name) = curr {
1320 let profile = self.get_profile(name)?;
1321 if let Some(profile) = profile {
1322 chain.push(profile);
1323 curr = profile.inherits.as_deref();
1324 } else {
1325 break;
1327 }
1328 }
1329
1330 Ok(chain)
1331 }
1332
1333 fn sanitize_profile_inherits(
1338 &self,
1339 known_profiles: &BTreeSet<String>,
1340 ) -> Result<(), ConfigParseErrorKind> {
1341 let mut inherit_err_collector = Vec::new();
1342
1343 self.sanitize_default_profile_inherits(&mut inherit_err_collector);
1344 self.sanitize_custom_profile_inherits(&mut inherit_err_collector, known_profiles);
1345
1346 if !inherit_err_collector.is_empty() {
1347 return Err(ConfigParseErrorKind::InheritanceErrors(
1348 inherit_err_collector,
1349 ));
1350 }
1351
1352 Ok(())
1353 }
1354
1355 fn sanitize_default_profile_inherits(&self, inherit_err_collector: &mut Vec<InheritsError>) {
1358 if self.default_profile().inherits().is_some() {
1359 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(
1360 NextestConfig::DEFAULT_PROFILE.to_string(),
1361 ));
1362 }
1363 }
1364
1365 fn sanitize_custom_profile_inherits(
1367 &self,
1368 inherit_err_collector: &mut Vec<InheritsError>,
1369 known_profiles: &BTreeSet<String>,
1370 ) {
1371 let mut profile_graph = Graph::<&str, (), Directed>::new();
1372 let mut profile_map = HashMap::new();
1373
1374 for (name, custom_profile) in self.other_profiles() {
1377 let starts_with_default = self.sanitize_custom_default_profile_inherits(
1378 name,
1379 custom_profile,
1380 inherit_err_collector,
1381 );
1382 if !starts_with_default {
1383 self.add_profile_to_graph(
1388 name,
1389 custom_profile,
1390 &mut profile_map,
1391 &mut profile_graph,
1392 inherit_err_collector,
1393 known_profiles,
1394 );
1395 }
1396 }
1397
1398 self.check_inheritance_cycles(profile_graph, inherit_err_collector);
1399 }
1400
1401 fn sanitize_custom_default_profile_inherits(
1404 &self,
1405 name: &str,
1406 custom_profile: &CustomProfileImpl,
1407 inherit_err_collector: &mut Vec<InheritsError>,
1408 ) -> bool {
1409 let starts_with_default = name.starts_with("default-");
1410
1411 if starts_with_default && custom_profile.inherits().is_some() {
1412 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(name.to_string()));
1413 }
1414
1415 starts_with_default
1416 }
1417
1418 fn add_profile_to_graph<'cfg>(
1423 &self,
1424 name: &'cfg str,
1425 custom_profile: &'cfg CustomProfileImpl,
1426 profile_map: &mut HashMap<&'cfg str, NodeIndex>,
1427 profile_graph: &mut Graph<&'cfg str, ()>,
1428 inherit_err_collector: &mut Vec<InheritsError>,
1429 known_profiles: &BTreeSet<String>,
1430 ) {
1431 if let Some(inherits_name) = custom_profile.inherits() {
1432 if inherits_name == name {
1433 inherit_err_collector
1434 .push(InheritsError::SelfReferentialInheritance(name.to_string()))
1435 } else if self.get_profile(inherits_name).is_ok() {
1436 let from_node = match profile_map.get(name) {
1438 None => {
1439 let profile_node = profile_graph.add_node(name);
1440 profile_map.insert(name, profile_node);
1441 profile_node
1442 }
1443 Some(node_idx) => *node_idx,
1444 };
1445 let to_node = match profile_map.get(inherits_name) {
1446 None => {
1447 let profile_node = profile_graph.add_node(inherits_name);
1448 profile_map.insert(inherits_name, profile_node);
1449 profile_node
1450 }
1451 Some(node_idx) => *node_idx,
1452 };
1453 profile_graph.add_edge(from_node, to_node, ());
1454 } else if known_profiles.contains(inherits_name) {
1455 } else {
1459 inherit_err_collector.push(InheritsError::UnknownInheritance(
1460 name.to_string(),
1461 inherits_name.to_string(),
1462 ))
1463 }
1464 }
1465 }
1466
1467 fn check_inheritance_cycles(
1469 &self,
1470 profile_graph: Graph<&str, ()>,
1471 inherit_err_collector: &mut Vec<InheritsError>,
1472 ) {
1473 let profile_sccs: Vec<Vec<NodeIndex>> = kosaraju_scc(&profile_graph);
1474 let profile_sccs: Vec<Vec<NodeIndex>> = profile_sccs
1475 .into_iter()
1476 .filter(|scc| scc.len() >= 2)
1477 .collect();
1478
1479 if !profile_sccs.is_empty() {
1480 inherit_err_collector.push(InheritsError::InheritanceCycle(
1481 profile_sccs
1482 .iter()
1483 .map(|node_idxs| {
1484 let profile_names: Vec<String> = node_idxs
1485 .iter()
1486 .map(|node_idx| profile_graph[*node_idx].to_string())
1487 .collect();
1488 profile_names
1489 })
1490 .collect(),
1491 ));
1492 }
1493 }
1494}
1495
1496#[derive(Clone, Debug, Deserialize)]
1502#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1503#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1504#[serde(rename_all = "kebab-case")]
1505pub(crate) struct NextestConfigDeserialize {
1506 #[cfg_attr(
1508 feature = "config-schema",
1509 schemars(with = "Option<StoreConfigImpl>")
1511 )]
1512 store: StoreConfigImpl,
1513
1514 #[expect(unused)]
1519 #[serde(default)]
1520 nextest_version: Option<NextestVersionDeserialize>,
1521
1522 #[expect(unused)]
1524 #[serde(default)]
1525 experimental: ExperimentalDeserialize,
1526
1527 #[serde(default)]
1530 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1531
1532 #[serde(default, rename = "script")]
1537 old_setup_scripts: IndexMap<ScriptId, SetupScriptConfig>,
1538
1539 #[serde(default)]
1541 scripts: ScriptConfig,
1542
1543 #[serde(rename = "profile")]
1545 #[cfg_attr(
1546 feature = "config-schema",
1547 schemars(with = "Option<HashMap<String, CustomProfileImpl>>")
1549 )]
1550 profiles: HashMap<String, CustomProfileImpl>,
1551}
1552
1553impl NextestConfigDeserialize {
1554 fn into_config_impl(mut self) -> NextestConfigImpl {
1555 let p = self
1556 .profiles
1557 .remove("default")
1558 .expect("default profile should exist");
1559 let default_profile = DefaultProfileImpl::new(p);
1560
1561 for (script_id, script_config) in self.old_setup_scripts {
1566 if let indexmap::map::Entry::Vacant(entry) = self.scripts.setup.entry(script_id) {
1567 entry.insert(script_config);
1568 }
1569 }
1570
1571 NextestConfigImpl {
1572 store: self.store,
1573 default_profile,
1574 test_groups: self.test_groups,
1575 scripts: self.scripts,
1576 other_profiles: self.profiles,
1577 }
1578 }
1579}
1580
1581#[cfg(feature = "config-schema")]
1591pub fn nextest_config_schema() -> schemars::Schema {
1592 let mut schema = schemars::schema_for!(NextestConfigDeserialize);
1593 schema.insert(
1595 "x-tombi-toml-version".to_owned(),
1596 serde_json::Value::String("v1.1.0".to_owned()),
1597 );
1598 schema
1599}
1600
1601#[derive(Clone, Debug, Deserialize)]
1602#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1603#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1604#[serde(rename_all = "kebab-case")]
1605struct StoreConfigImpl {
1606 #[cfg_attr(
1608 feature = "config-schema",
1609 schemars(with = "Option<String>")
1611 )]
1612 dir: Utf8PathBuf,
1613}
1614
1615#[derive(Clone, Debug)]
1616pub(in crate::config) struct DefaultProfileImpl {
1617 default_filter: String,
1618 test_threads: TestThreads,
1619 threads_required: ThreadsRequired,
1620 run_extra_args: Vec<String>,
1621 retries: RetryPolicy,
1622 flaky_result: FlakyResult,
1623 status_level: StatusLevel,
1624 final_status_level: FinalStatusLevel,
1625 failure_output: TestOutputDisplay,
1626 success_output: TestOutputDisplay,
1627 max_fail: MaxFail,
1628 slow_timeout: SlowTimeout,
1629 global_timeout: GlobalTimeout,
1630 leak_timeout: LeakTimeout,
1631 overrides: Vec<DeserializedOverride>,
1632 scripts: Vec<DeserializedProfileScriptConfig>,
1633 junit: DefaultJunitImpl,
1634 archive: ArchiveConfig,
1635 bench: DefaultBenchConfig,
1636 inherits: Inherits,
1637}
1638
1639impl DefaultProfileImpl {
1640 fn new(p: CustomProfileImpl) -> Self {
1641 Self {
1642 default_filter: p
1643 .default_filter
1644 .expect("default-filter present in default profile"),
1645 test_threads: p
1646 .test_threads
1647 .expect("test-threads present in default profile"),
1648 threads_required: p
1649 .threads_required
1650 .expect("threads-required present in default profile"),
1651 run_extra_args: p
1652 .run_extra_args
1653 .expect("run-extra-args present in default profile"),
1654 retries: p.retries.expect("retries present in default profile"),
1655 flaky_result: p
1656 .flaky_result
1657 .expect("flaky-result present in default profile"),
1658 status_level: p
1659 .status_level
1660 .expect("status-level present in default profile"),
1661 final_status_level: p
1662 .final_status_level
1663 .expect("final-status-level present in default profile"),
1664 failure_output: p
1665 .failure_output
1666 .expect("failure-output present in default profile"),
1667 success_output: p
1668 .success_output
1669 .expect("success-output present in default profile"),
1670 max_fail: p.max_fail.expect("fail-fast present in default profile"),
1671 slow_timeout: p
1672 .slow_timeout
1673 .expect("slow-timeout present in default profile"),
1674 global_timeout: p
1675 .global_timeout
1676 .expect("global-timeout present in default profile"),
1677 leak_timeout: p
1678 .leak_timeout
1679 .expect("leak-timeout present in default profile"),
1680 overrides: p.overrides,
1681 scripts: p.scripts,
1682 junit: DefaultJunitImpl::for_default_profile(p.junit),
1683 archive: p.archive.expect("archive present in default profile"),
1684 bench: DefaultBenchConfig::for_default_profile(
1685 p.bench.expect("bench present in default profile"),
1686 ),
1687 inherits: Inherits::new(p.inherits),
1688 }
1689 }
1690
1691 pub(in crate::config) fn default_filter(&self) -> &str {
1692 &self.default_filter
1693 }
1694
1695 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1696 self.inherits.inherits_from()
1697 }
1698
1699 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1700 &self.overrides
1701 }
1702
1703 pub(in crate::config) fn setup_scripts(&self) -> &[DeserializedProfileScriptConfig] {
1704 &self.scripts
1705 }
1706
1707 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1708 match run_mode {
1709 NextestRunMode::Test => self.slow_timeout,
1710 NextestRunMode::Benchmark => self.bench.slow_timeout,
1711 }
1712 }
1713
1714 pub(in crate::config) fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1715 match run_mode {
1716 NextestRunMode::Test => self.global_timeout,
1717 NextestRunMode::Benchmark => self.bench.global_timeout,
1718 }
1719 }
1720}
1721
1722#[derive(Clone, Debug, Deserialize)]
1723#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1724#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1725#[serde(rename_all = "kebab-case")]
1726pub(in crate::config) struct CustomProfileImpl {
1727 #[serde(default)]
1730 default_filter: Option<String>,
1731 #[serde(default, deserialize_with = "deserialize_retry_policy")]
1733 retries: Option<RetryPolicy>,
1734 #[serde(default)]
1736 flaky_result: Option<FlakyResult>,
1737 #[serde(default)]
1739 test_threads: Option<TestThreads>,
1740 #[serde(default)]
1742 threads_required: Option<ThreadsRequired>,
1743 #[serde(default)]
1745 run_extra_args: Option<Vec<String>>,
1746 #[serde(default)]
1748 status_level: Option<StatusLevel>,
1749 #[serde(default)]
1751 final_status_level: Option<FinalStatusLevel>,
1752 #[serde(default)]
1754 failure_output: Option<TestOutputDisplay>,
1755 #[serde(default)]
1757 success_output: Option<TestOutputDisplay>,
1758 #[serde(
1760 default,
1761 rename = "fail-fast",
1762 deserialize_with = "deserialize_fail_fast"
1763 )]
1764 max_fail: Option<MaxFail>,
1765 #[serde(default, deserialize_with = "deserialize_slow_timeout")]
1768 slow_timeout: Option<SlowTimeout>,
1769 #[serde(default)]
1771 global_timeout: Option<GlobalTimeout>,
1772 #[serde(default, deserialize_with = "deserialize_leak_timeout")]
1774 leak_timeout: Option<LeakTimeout>,
1775 #[serde(default)]
1777 overrides: Vec<DeserializedOverride>,
1778 #[serde(default)]
1780 scripts: Vec<DeserializedProfileScriptConfig>,
1781 #[serde(default)]
1783 junit: JunitImpl,
1784 #[serde(default)]
1786 archive: Option<ArchiveConfig>,
1787 #[serde(default)]
1789 bench: Option<BenchConfig>,
1790 #[serde(default)]
1792 inherits: Option<String>,
1793}
1794
1795impl CustomProfileImpl {
1796 #[cfg(test)]
1797 pub(in crate::config) fn test_threads(&self) -> Option<TestThreads> {
1798 self.test_threads
1799 }
1800
1801 pub(in crate::config) fn default_filter(&self) -> Option<&str> {
1802 self.default_filter.as_deref()
1803 }
1804
1805 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> Option<SlowTimeout> {
1806 match run_mode {
1807 NextestRunMode::Test => self.slow_timeout,
1808 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.slow_timeout),
1809 }
1810 }
1811
1812 pub(in crate::config) fn global_timeout(
1813 &self,
1814 run_mode: NextestRunMode,
1815 ) -> Option<GlobalTimeout> {
1816 match run_mode {
1817 NextestRunMode::Test => self.global_timeout,
1818 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.global_timeout),
1819 }
1820 }
1821
1822 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1823 self.inherits.as_deref()
1824 }
1825
1826 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1827 &self.overrides
1828 }
1829
1830 pub(in crate::config) fn scripts(&self) -> &[DeserializedProfileScriptConfig] {
1831 &self.scripts
1832 }
1833}
1834
1835#[cfg(test)]
1836mod tests {
1837 use super::*;
1838 use crate::config::utils::test_helpers::*;
1839 use camino_tempfile::tempdir;
1840 use iddqd::{IdHashItem, IdHashMap, id_hash_map, id_upcast};
1841
1842 fn tool_name(s: &str) -> ToolName {
1843 ToolName::new(s.into()).unwrap()
1844 }
1845
1846 #[derive(Default)]
1848 struct TestConfigWarnings {
1849 unknown_keys: IdHashMap<UnknownKeys>,
1850 reserved_profiles: IdHashMap<ReservedProfiles>,
1851 deprecated_scripts: IdHashMap<DeprecatedScripts>,
1852 empty_script_warnings: IdHashMap<EmptyScriptSections>,
1853 }
1854
1855 impl ConfigWarnings for TestConfigWarnings {
1856 fn unknown_config_keys(
1857 &mut self,
1858 config_file: &Utf8Path,
1859 _workspace_root: &Utf8Path,
1860 tool: Option<&ToolName>,
1861 unknown: &BTreeSet<String>,
1862 ) {
1863 self.unknown_keys
1864 .insert_unique(UnknownKeys {
1865 tool: tool.cloned(),
1866 config_file: config_file.to_owned(),
1867 keys: unknown.clone(),
1868 })
1869 .unwrap();
1870 }
1871
1872 fn unknown_reserved_profiles(
1873 &mut self,
1874 config_file: &Utf8Path,
1875 _workspace_root: &Utf8Path,
1876 tool: Option<&ToolName>,
1877 profiles: &[&str],
1878 ) {
1879 self.reserved_profiles
1880 .insert_unique(ReservedProfiles {
1881 tool: tool.cloned(),
1882 config_file: config_file.to_owned(),
1883 profiles: profiles.iter().map(|&s| s.to_owned()).collect(),
1884 })
1885 .unwrap();
1886 }
1887
1888 fn empty_script_sections(
1889 &mut self,
1890 config_file: &Utf8Path,
1891 _workspace_root: &Utf8Path,
1892 tool: Option<&ToolName>,
1893 profile_name: &str,
1894 empty_count: usize,
1895 ) {
1896 self.empty_script_warnings
1897 .insert_unique(EmptyScriptSections {
1898 tool: tool.cloned(),
1899 config_file: config_file.to_owned(),
1900 profile_name: profile_name.to_owned(),
1901 empty_count,
1902 })
1903 .unwrap();
1904 }
1905
1906 fn deprecated_script_config(
1907 &mut self,
1908 config_file: &Utf8Path,
1909 _workspace_root: &Utf8Path,
1910 tool: Option<&ToolName>,
1911 ) {
1912 self.deprecated_scripts
1913 .insert_unique(DeprecatedScripts {
1914 tool: tool.cloned(),
1915 config_file: config_file.to_owned(),
1916 })
1917 .unwrap();
1918 }
1919 }
1920
1921 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1922 struct UnknownKeys {
1923 tool: Option<ToolName>,
1924 config_file: Utf8PathBuf,
1925 keys: BTreeSet<String>,
1926 }
1927
1928 impl IdHashItem for UnknownKeys {
1929 type Key<'a> = Option<&'a ToolName>;
1930 fn key(&self) -> Self::Key<'_> {
1931 self.tool.as_ref()
1932 }
1933 id_upcast!();
1934 }
1935
1936 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1937 struct ReservedProfiles {
1938 tool: Option<ToolName>,
1939 config_file: Utf8PathBuf,
1940 profiles: Vec<String>,
1941 }
1942
1943 impl IdHashItem for ReservedProfiles {
1944 type Key<'a> = Option<&'a ToolName>;
1945 fn key(&self) -> Self::Key<'_> {
1946 self.tool.as_ref()
1947 }
1948 id_upcast!();
1949 }
1950
1951 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1952 struct DeprecatedScripts {
1953 tool: Option<ToolName>,
1954 config_file: Utf8PathBuf,
1955 }
1956
1957 impl IdHashItem for DeprecatedScripts {
1958 type Key<'a> = Option<&'a ToolName>;
1959 fn key(&self) -> Self::Key<'_> {
1960 self.tool.as_ref()
1961 }
1962 id_upcast!();
1963 }
1964
1965 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1966 struct EmptyScriptSections {
1967 tool: Option<ToolName>,
1968 config_file: Utf8PathBuf,
1969 profile_name: String,
1970 empty_count: usize,
1971 }
1972
1973 impl IdHashItem for EmptyScriptSections {
1974 type Key<'a> = (&'a Option<ToolName>, &'a str);
1975 fn key(&self) -> Self::Key<'_> {
1976 (&self.tool, &self.profile_name)
1977 }
1978 id_upcast!();
1979 }
1980
1981 #[test]
1982 fn default_config_is_valid() {
1983 let default_config = NextestConfig::default_config("foo");
1984 default_config
1985 .profile(NextestConfig::DEFAULT_PROFILE)
1986 .expect("default profile should exist");
1987 }
1988
1989 #[test]
1990 fn ignored_keys() {
1991 let config_contents = r#"
1992 ignored1 = "test"
1993
1994 [profile.default]
1995 retries = 3
1996 ignored2 = "hi"
1997
1998 [profile.default-foo]
1999 retries = 5
2000
2001 [[profile.default.overrides]]
2002 filter = 'test(test_foo)'
2003 retries = 20
2004 ignored3 = 42
2005 "#;
2006
2007 let tool_config_contents = r#"
2008 [store]
2009 ignored4 = 20
2010
2011 [profile.default]
2012 retries = 4
2013 ignored5 = false
2014
2015 [profile.default-bar]
2016 retries = 5
2017
2018 [profile.tool]
2019 retries = 12
2020
2021 [[profile.tool.overrides]]
2022 filter = 'test(test_baz)'
2023 retries = 22
2024 ignored6 = 6.5
2025 "#;
2026
2027 let workspace_dir = tempdir().unwrap();
2028
2029 let graph = temp_workspace(&workspace_dir, config_contents);
2030 let workspace_root = graph.workspace().root();
2031 let tool_path = workspace_root.join(".config/tool.toml");
2032 std::fs::write(&tool_path, tool_config_contents).unwrap();
2033
2034 let pcx = ParseContext::new(&graph);
2035
2036 let mut warnings = TestConfigWarnings::default();
2037
2038 let _ = NextestConfig::from_sources_with_warnings(
2039 workspace_root,
2040 &pcx,
2041 None,
2042 &[ToolConfigFile {
2043 tool: tool_name("my-tool"),
2044 config_file: tool_path.clone(),
2045 }][..],
2046 &Default::default(),
2047 &mut warnings,
2048 )
2049 .expect("config is valid");
2050
2051 assert_eq!(
2052 warnings.unknown_keys.len(),
2053 2,
2054 "there are two files with unknown keys"
2055 );
2056
2057 assert_eq!(
2058 warnings.unknown_keys,
2059 id_hash_map! {
2060 UnknownKeys {
2061 tool: None,
2062 config_file: workspace_root.join(".config/nextest.toml"),
2063 keys: maplit::btreeset! {
2064 "ignored1".to_owned(),
2065 "profile.default.ignored2".to_owned(),
2066 "profile.default.overrides.0.ignored3".to_owned(),
2067 }
2068 },
2069 UnknownKeys {
2070 tool: Some(tool_name("my-tool")),
2071 config_file: tool_path.clone(),
2072 keys: maplit::btreeset! {
2073 "store.ignored4".to_owned(),
2074 "profile.default.ignored5".to_owned(),
2075 "profile.tool.overrides.0.ignored6".to_owned(),
2076 }
2077 }
2078 }
2079 );
2080 assert_eq!(
2081 warnings.reserved_profiles,
2082 id_hash_map! {
2083 ReservedProfiles {
2084 tool: None,
2085 config_file: workspace_root.join(".config/nextest.toml"),
2086 profiles: vec!["default-foo".to_owned()],
2087 },
2088 ReservedProfiles {
2089 tool: Some(tool_name("my-tool")),
2090 config_file: tool_path,
2091 profiles: vec!["default-bar".to_owned()],
2092 }
2093 },
2094 )
2095 }
2096
2097 #[test]
2098 fn script_warnings() {
2099 let config_contents = r#"
2100 experimental = ["setup-scripts", "wrapper-scripts"]
2101
2102 [scripts.wrapper.script1]
2103 command = "echo test"
2104
2105 [scripts.wrapper.script2]
2106 command = "echo test2"
2107
2108 [scripts.setup.script3]
2109 command = "echo setup"
2110
2111 [[profile.default.scripts]]
2112 filter = 'all()'
2113 # Empty - no setup or wrapper scripts
2114
2115 [[profile.default.scripts]]
2116 filter = 'test(foo)'
2117 setup = ["script3"]
2118
2119 [profile.custom]
2120 [[profile.custom.scripts]]
2121 filter = 'all()'
2122 # Empty - no setup or wrapper scripts
2123
2124 [[profile.custom.scripts]]
2125 filter = 'test(bar)'
2126 # Another empty section
2127 "#;
2128
2129 let tool_config_contents = r#"
2130 experimental = ["setup-scripts", "wrapper-scripts"]
2131
2132 [scripts.wrapper."@tool:tool:disabled_script"]
2133 command = "echo disabled"
2134
2135 [scripts.setup."@tool:tool:setup_script"]
2136 command = "echo setup"
2137
2138 [profile.tool]
2139 [[profile.tool.scripts]]
2140 filter = 'all()'
2141 # Empty section
2142
2143 [[profile.tool.scripts]]
2144 filter = 'test(foo)'
2145 setup = ["@tool:tool:setup_script"]
2146 "#;
2147
2148 let workspace_dir = tempdir().unwrap();
2149 let graph = temp_workspace(&workspace_dir, config_contents);
2150 let workspace_root = graph.workspace().root();
2151 let tool_path = workspace_root.join(".config/tool.toml");
2152 std::fs::write(&tool_path, tool_config_contents).unwrap();
2153
2154 let pcx = ParseContext::new(&graph);
2155
2156 let mut warnings = TestConfigWarnings::default();
2157
2158 let experimental = maplit::btreeset! {
2159 ConfigExperimental::SetupScripts,
2160 ConfigExperimental::WrapperScripts
2161 };
2162 let _ = NextestConfig::from_sources_with_warnings(
2163 workspace_root,
2164 &pcx,
2165 None,
2166 &[ToolConfigFile {
2167 tool: tool_name("tool"),
2168 config_file: tool_path.clone(),
2169 }][..],
2170 &experimental,
2171 &mut warnings,
2172 )
2173 .expect("config is valid");
2174
2175 assert_eq!(
2176 warnings.empty_script_warnings,
2177 id_hash_map! {
2178 EmptyScriptSections {
2179 tool: None,
2180 config_file: workspace_root.join(".config/nextest.toml"),
2181 profile_name: "default".to_owned(),
2182 empty_count: 1,
2183 },
2184 EmptyScriptSections {
2185 tool: None,
2186 config_file: workspace_root.join(".config/nextest.toml"),
2187 profile_name: "custom".to_owned(),
2188 empty_count: 2,
2189 },
2190 EmptyScriptSections {
2191 tool: Some(tool_name("tool")),
2192 config_file: tool_path,
2193 profile_name: "tool".to_owned(),
2194 empty_count: 1,
2195 }
2196 }
2197 );
2198 }
2199
2200 #[test]
2201 fn deprecated_script_config_warning() {
2202 let config_contents = r#"
2203 experimental = ["setup-scripts"]
2204
2205 [script.my-script]
2206 command = "echo hello"
2207"#;
2208
2209 let tool_config_contents = r#"
2210 experimental = ["setup-scripts"]
2211
2212 [script."@tool:my-tool:my-script"]
2213 command = "echo hello"
2214"#;
2215
2216 let temp_dir = tempdir().unwrap();
2217
2218 let graph = temp_workspace(&temp_dir, config_contents);
2219 let workspace_root = graph.workspace().root();
2220 let tool_path = workspace_root.join(".config/my-tool.toml");
2221 std::fs::write(&tool_path, tool_config_contents).unwrap();
2222 let pcx = ParseContext::new(&graph);
2223
2224 let mut warnings = TestConfigWarnings::default();
2225 NextestConfig::from_sources_with_warnings(
2226 graph.workspace().root(),
2227 &pcx,
2228 None,
2229 &[ToolConfigFile {
2230 tool: tool_name("my-tool"),
2231 config_file: tool_path.clone(),
2232 }],
2233 &maplit::btreeset! {ConfigExperimental::SetupScripts},
2234 &mut warnings,
2235 )
2236 .expect("config is valid");
2237
2238 assert_eq!(
2239 warnings.deprecated_scripts,
2240 id_hash_map! {
2241 DeprecatedScripts {
2242 tool: None,
2243 config_file: graph.workspace().root().join(".config/nextest.toml"),
2244 },
2245 DeprecatedScripts {
2246 tool: Some(tool_name("my-tool")),
2247 config_file: tool_path,
2248 }
2249 }
2250 );
2251 }
2252}