1use super::{NextestVersionDeserialize, ToolConfigFile, ToolName};
5use crate::{
6 config::{
7 core::ConfigExperimental,
8 elements::{
9 ArchiveConfig, BenchConfig, CustomTestGroup, DefaultBenchConfig, DefaultJunitImpl,
10 GlobalTimeout, Inherits, JunitConfig, JunitImpl, JunitSettings, LeakTimeout, MaxFail,
11 RetryPolicy, SlowTimeout, TestGroup, TestGroupConfig, TestThreads, ThreadsRequired,
12 deserialize_fail_fast, deserialize_leak_timeout, deserialize_retry_policy,
13 deserialize_slow_timeout,
14 },
15 overrides::{
16 CompiledByProfile, CompiledData, CompiledDefaultFilter, DeserializedOverride,
17 ListSettings, SettingSource, TestSettings,
18 },
19 scripts::{
20 DeserializedProfileScriptConfig, ProfileScriptType, ScriptConfig, ScriptId, ScriptInfo,
21 SetupScriptConfig, SetupScripts,
22 },
23 },
24 errors::{
25 ConfigParseError, ConfigParseErrorKind, InheritsError,
26 ProfileListScriptUsesRunFiltersError, ProfileNotFound, ProfileScriptErrors,
27 ProfileUnknownScriptError, ProfileWrongConfigScriptTypeError, UnknownTestGroupError,
28 provided_by_tool,
29 },
30 helpers::plural,
31 list::TestList,
32 platform::BuildPlatforms,
33 reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplay},
34 run_mode::NextestRunMode,
35};
36use camino::{Utf8Path, Utf8PathBuf};
37use config::{
38 Config, ConfigBuilder, ConfigError, File, FileFormat, FileSourceFile, builder::DefaultState,
39};
40use iddqd::IdOrdMap;
41use indexmap::IndexMap;
42use nextest_filtering::{BinaryQuery, EvalContext, Filterset, ParseContext, TestQuery};
43use petgraph::{Directed, Graph, algo::scc::kosaraju_scc, graph::NodeIndex};
44use serde::Deserialize;
45use std::{
46 collections::{BTreeMap, BTreeSet, HashMap, hash_map},
47 sync::LazyLock,
48};
49use tracing::warn;
50
51pub trait ConfigWarnings {
56 fn unknown_config_keys(
58 &mut self,
59 config_file: &Utf8Path,
60 workspace_root: &Utf8Path,
61 tool: Option<&ToolName>,
62 unknown: &BTreeSet<String>,
63 );
64
65 fn unknown_reserved_profiles(
67 &mut self,
68 config_file: &Utf8Path,
69 workspace_root: &Utf8Path,
70 tool: Option<&ToolName>,
71 profiles: &[&str],
72 );
73
74 fn deprecated_script_config(
76 &mut self,
77 config_file: &Utf8Path,
78 workspace_root: &Utf8Path,
79 tool: Option<&ToolName>,
80 );
81
82 fn empty_script_sections(
85 &mut self,
86 config_file: &Utf8Path,
87 workspace_root: &Utf8Path,
88 tool: Option<&ToolName>,
89 profile_name: &str,
90 empty_count: usize,
91 );
92}
93
94pub struct DefaultConfigWarnings;
96
97impl ConfigWarnings for DefaultConfigWarnings {
98 fn unknown_config_keys(
99 &mut self,
100 config_file: &Utf8Path,
101 workspace_root: &Utf8Path,
102 tool: Option<&ToolName>,
103 unknown: &BTreeSet<String>,
104 ) {
105 let mut unknown_str = String::new();
106 if unknown.len() == 1 {
107 unknown_str.push_str("key: ");
109 unknown_str.push_str(unknown.iter().next().unwrap());
110 } else {
111 unknown_str.push_str("keys:\n");
112 for ignored_key in unknown {
113 unknown_str.push('\n');
114 unknown_str.push_str(" - ");
115 unknown_str.push_str(ignored_key);
116 }
117 }
118
119 warn!(
120 "in config file {}{}, ignoring unknown configuration {unknown_str}",
121 config_file
122 .strip_prefix(workspace_root)
123 .unwrap_or(config_file),
124 provided_by_tool(tool),
125 )
126 }
127
128 fn unknown_reserved_profiles(
129 &mut self,
130 config_file: &Utf8Path,
131 workspace_root: &Utf8Path,
132 tool: Option<&ToolName>,
133 profiles: &[&str],
134 ) {
135 warn!(
136 "in config file {}{}, ignoring unknown profiles in the reserved `default-` namespace:",
137 config_file
138 .strip_prefix(workspace_root)
139 .unwrap_or(config_file),
140 provided_by_tool(tool),
141 );
142
143 for profile in profiles {
144 warn!(" {profile}");
145 }
146 }
147
148 fn deprecated_script_config(
149 &mut self,
150 config_file: &Utf8Path,
151 workspace_root: &Utf8Path,
152 tool: Option<&ToolName>,
153 ) {
154 warn!(
155 "in config file {}{}, [script.*] is deprecated and will be removed in a \
156 future version of nextest; use the `scripts.setup` table instead",
157 config_file
158 .strip_prefix(workspace_root)
159 .unwrap_or(config_file),
160 provided_by_tool(tool),
161 );
162 }
163
164 fn empty_script_sections(
165 &mut self,
166 config_file: &Utf8Path,
167 workspace_root: &Utf8Path,
168 tool: Option<&ToolName>,
169 profile_name: &str,
170 empty_count: usize,
171 ) {
172 warn!(
173 "in config file {}{}, [[profile.{}.scripts]] has {} {} \
174 with neither setup nor wrapper scripts",
175 config_file
176 .strip_prefix(workspace_root)
177 .unwrap_or(config_file),
178 provided_by_tool(tool),
179 profile_name,
180 empty_count,
181 plural::sections_str(empty_count),
182 );
183 }
184}
185
186#[inline]
188pub fn get_num_cpus() -> usize {
189 static NUM_CPUS: LazyLock<usize> =
190 LazyLock::new(|| match std::thread::available_parallelism() {
191 Ok(count) => count.into(),
192 Err(err) => {
193 warn!("unable to determine num-cpus ({err}), assuming 1 logical CPU");
194 1
195 }
196 });
197
198 *NUM_CPUS
199}
200
201#[derive(Clone, Debug)]
210pub struct NextestConfig {
211 workspace_root: Utf8PathBuf,
212 inner: NextestConfigImpl,
213 compiled: CompiledByProfile,
214}
215
216impl NextestConfig {
217 pub const CONFIG_PATH: &'static str = ".config/nextest.toml";
220
221 pub const DEFAULT_CONFIG: &'static str = include_str!("../../../default-config.toml");
225
226 pub const ENVIRONMENT_PREFIX: &'static str = "NEXTEST";
228
229 pub const DEFAULT_PROFILE: &'static str = "default";
231
232 pub const DEFAULT_MIRI_PROFILE: &'static str = "default-miri";
234
235 pub const DEFAULT_PROFILES: &'static [&'static str] =
237 &[Self::DEFAULT_PROFILE, Self::DEFAULT_MIRI_PROFILE];
238
239 pub fn from_sources<'a, I>(
249 workspace_root: impl Into<Utf8PathBuf>,
250 pcx: &ParseContext<'_>,
251 config_file: Option<&Utf8Path>,
252 tool_config_files: impl IntoIterator<IntoIter = I>,
253 experimental: &BTreeSet<ConfigExperimental>,
254 ) -> Result<Self, ConfigParseError>
255 where
256 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
257 {
258 Self::from_sources_with_warnings(
259 workspace_root,
260 pcx,
261 config_file,
262 tool_config_files,
263 experimental,
264 &mut DefaultConfigWarnings,
265 )
266 }
267
268 pub fn from_sources_with_warnings<'a, I>(
270 workspace_root: impl Into<Utf8PathBuf>,
271 pcx: &ParseContext<'_>,
272 config_file: Option<&Utf8Path>,
273 tool_config_files: impl IntoIterator<IntoIter = I>,
274 experimental: &BTreeSet<ConfigExperimental>,
275 warnings: &mut impl ConfigWarnings,
276 ) -> Result<Self, ConfigParseError>
277 where
278 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
279 {
280 Self::from_sources_impl(
281 workspace_root,
282 pcx,
283 config_file,
284 tool_config_files,
285 experimental,
286 warnings,
287 )
288 }
289
290 fn from_sources_impl<'a, I>(
292 workspace_root: impl Into<Utf8PathBuf>,
293 pcx: &ParseContext<'_>,
294 config_file: Option<&Utf8Path>,
295 tool_config_files: impl IntoIterator<IntoIter = I>,
296 experimental: &BTreeSet<ConfigExperimental>,
297 warnings: &mut impl ConfigWarnings,
298 ) -> Result<Self, ConfigParseError>
299 where
300 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
301 {
302 let workspace_root = workspace_root.into();
303 let tool_config_files_rev = tool_config_files.into_iter().rev();
304 let (inner, compiled) = Self::read_from_sources(
305 pcx,
306 &workspace_root,
307 config_file,
308 tool_config_files_rev,
309 experimental,
310 warnings,
311 )?;
312 Ok(Self {
313 workspace_root,
314 inner,
315 compiled,
316 })
317 }
318
319 #[cfg(test)]
321 pub(crate) fn default_config(workspace_root: impl Into<Utf8PathBuf>) -> Self {
322 use itertools::Itertools;
323
324 let config = Self::make_default_config()
325 .build()
326 .expect("default config is always valid");
327
328 let mut unknown = BTreeSet::new();
329 let deserialized: NextestConfigDeserialize =
330 serde_ignored::deserialize(config, |path: serde_ignored::Path| {
331 unknown.insert(path.to_string());
332 })
333 .expect("default config is always valid");
334
335 if !unknown.is_empty() {
338 panic!(
339 "found unknown keys in default config: {}",
340 unknown.iter().join(", ")
341 );
342 }
343
344 Self {
345 workspace_root: workspace_root.into(),
346 inner: deserialized.into_config_impl(),
347 compiled: CompiledByProfile::for_default_config(),
349 }
350 }
351
352 pub fn profile(&self, name: impl AsRef<str>) -> Result<EarlyProfile<'_>, ProfileNotFound> {
355 self.make_profile(name.as_ref())
356 }
357
358 fn read_from_sources<'a>(
363 pcx: &ParseContext<'_>,
364 workspace_root: &Utf8Path,
365 file: Option<&Utf8Path>,
366 tool_config_files_rev: impl Iterator<Item = &'a ToolConfigFile>,
367 experimental: &BTreeSet<ConfigExperimental>,
368 warnings: &mut impl ConfigWarnings,
369 ) -> Result<(NextestConfigImpl, CompiledByProfile), ConfigParseError> {
370 let mut composite_builder = Self::make_default_config();
372
373 let mut compiled = CompiledByProfile::for_default_config();
376
377 let mut known_groups = BTreeSet::new();
378 let mut known_scripts = IdOrdMap::new();
379 let mut known_profiles = BTreeSet::new();
382
383 for ToolConfigFile { config_file, tool } in tool_config_files_rev {
385 let source = File::new(config_file.as_str(), FileFormat::Toml);
386 Self::deserialize_individual_config(
387 pcx,
388 workspace_root,
389 config_file,
390 Some(tool),
391 source.clone(),
392 &mut compiled,
393 experimental,
394 warnings,
395 &mut known_groups,
396 &mut known_scripts,
397 &mut known_profiles,
398 )?;
399
400 composite_builder = composite_builder.add_source(source);
402 }
403
404 let (config_file, source) = match file {
406 Some(file) => (file.to_owned(), File::new(file.as_str(), FileFormat::Toml)),
407 None => {
408 let config_file = workspace_root.join(Self::CONFIG_PATH);
409 let source = File::new(config_file.as_str(), FileFormat::Toml).required(false);
410 (config_file, source)
411 }
412 };
413
414 Self::deserialize_individual_config(
415 pcx,
416 workspace_root,
417 &config_file,
418 None,
419 source.clone(),
420 &mut compiled,
421 experimental,
422 warnings,
423 &mut known_groups,
424 &mut known_scripts,
425 &mut known_profiles,
426 )?;
427
428 composite_builder = composite_builder.add_source(source);
429
430 let (config, _unknown) = Self::build_and_deserialize_config(&composite_builder)
433 .map_err(|kind| ConfigParseError::new(&config_file, None, kind))?;
434
435 compiled.default.reverse();
437 for data in compiled.other.values_mut() {
438 data.reverse();
439 }
440
441 Ok((config.into_config_impl(), compiled))
442 }
443
444 #[expect(clippy::too_many_arguments)]
445 fn deserialize_individual_config(
446 pcx: &ParseContext<'_>,
447 workspace_root: &Utf8Path,
448 config_file: &Utf8Path,
449 tool: Option<&ToolName>,
450 source: File<FileSourceFile, FileFormat>,
451 compiled_out: &mut CompiledByProfile,
452 experimental: &BTreeSet<ConfigExperimental>,
453 warnings: &mut impl ConfigWarnings,
454 known_groups: &mut BTreeSet<CustomTestGroup>,
455 known_scripts: &mut IdOrdMap<ScriptInfo>,
456 known_profiles: &mut BTreeSet<String>,
457 ) -> Result<(), ConfigParseError> {
458 let default_builder = Self::make_default_config();
461 let this_builder = default_builder.add_source(source);
462 let (mut this_config, unknown) = Self::build_and_deserialize_config(&this_builder)
463 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
464
465 if !unknown.is_empty() {
466 warnings.unknown_config_keys(config_file, workspace_root, tool, &unknown);
467 }
468
469 let (valid_groups, invalid_groups): (BTreeSet<_>, _) =
471 this_config.test_groups.keys().cloned().partition(|group| {
472 if let Some(tool) = tool {
473 group
475 .as_identifier()
476 .tool_components()
477 .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
478 } else {
479 !group.as_identifier().is_tool_identifier()
481 }
482 });
483
484 if !invalid_groups.is_empty() {
485 let kind = if tool.is_some() {
486 ConfigParseErrorKind::InvalidTestGroupsDefinedByTool(invalid_groups)
487 } else {
488 ConfigParseErrorKind::InvalidTestGroupsDefined(invalid_groups)
489 };
490 return Err(ConfigParseError::new(config_file, tool, kind));
491 }
492
493 known_groups.extend(valid_groups);
494
495 if !this_config.scripts.is_empty() && !this_config.old_setup_scripts.is_empty() {
497 return Err(ConfigParseError::new(
498 config_file,
499 tool,
500 ConfigParseErrorKind::BothScriptAndScriptsDefined,
501 ));
502 }
503
504 if !this_config.old_setup_scripts.is_empty() {
506 warnings.deprecated_script_config(config_file, workspace_root, tool);
507 this_config.scripts.setup = this_config.old_setup_scripts.clone();
508 }
509
510 {
512 let mut missing_features = BTreeSet::new();
513 if !this_config.scripts.setup.is_empty()
514 && !experimental.contains(&ConfigExperimental::SetupScripts)
515 {
516 missing_features.insert(ConfigExperimental::SetupScripts);
517 }
518 if !this_config.scripts.wrapper.is_empty()
519 && !experimental.contains(&ConfigExperimental::WrapperScripts)
520 {
521 missing_features.insert(ConfigExperimental::WrapperScripts);
522 }
523 if !missing_features.is_empty() {
524 return Err(ConfigParseError::new(
525 config_file,
526 tool,
527 ConfigParseErrorKind::ExperimentalFeaturesNotEnabled { missing_features },
528 ));
529 }
530 }
531
532 let duplicate_ids: BTreeSet<_> = this_config.scripts.duplicate_ids().cloned().collect();
533 if !duplicate_ids.is_empty() {
534 return Err(ConfigParseError::new(
535 config_file,
536 tool,
537 ConfigParseErrorKind::DuplicateConfigScriptNames(duplicate_ids),
538 ));
539 }
540
541 let (valid_scripts, invalid_scripts): (BTreeSet<_>, _) = this_config
543 .scripts
544 .all_script_ids()
545 .cloned()
546 .partition(|script| {
547 if let Some(tool) = tool {
548 script
550 .as_identifier()
551 .tool_components()
552 .is_some_and(|(tool_name, _)| tool_name == tool.as_str())
553 } else {
554 !script.as_identifier().is_tool_identifier()
556 }
557 });
558
559 if !invalid_scripts.is_empty() {
560 let kind = if tool.is_some() {
561 ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool(invalid_scripts)
562 } else {
563 ConfigParseErrorKind::InvalidConfigScriptsDefined(invalid_scripts)
564 };
565 return Err(ConfigParseError::new(config_file, tool, kind));
566 }
567
568 known_scripts.extend(
569 valid_scripts
570 .into_iter()
571 .map(|id| this_config.scripts.script_info(id)),
572 );
573
574 let this_config = this_config.into_config_impl();
575
576 let unknown_default_profiles: Vec<_> = this_config
577 .all_profiles()
578 .filter(|p| p.starts_with("default-") && !NextestConfig::DEFAULT_PROFILES.contains(p))
579 .collect();
580 if !unknown_default_profiles.is_empty() {
581 warnings.unknown_reserved_profiles(
582 config_file,
583 workspace_root,
584 tool,
585 &unknown_default_profiles,
586 );
587 }
588
589 this_config
593 .sanitize_profile_inherits(known_profiles)
594 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
595
596 known_profiles.extend(
598 this_config
599 .other_profiles()
600 .map(|(name, _)| name.to_owned()),
601 );
602
603 let this_compiled = CompiledByProfile::new(pcx, &this_config)
605 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
606
607 let mut unknown_group_errors = Vec::new();
609 let mut check_test_group = |profile_name: &str, test_group: Option<&TestGroup>| {
610 if let Some(TestGroup::Custom(group)) = test_group
611 && !known_groups.contains(group)
612 {
613 unknown_group_errors.push(UnknownTestGroupError {
614 profile_name: profile_name.to_owned(),
615 name: TestGroup::Custom(group.clone()),
616 });
617 }
618 };
619
620 this_compiled
621 .default
622 .overrides
623 .iter()
624 .for_each(|override_| {
625 check_test_group("default", override_.data.test_group.as_ref());
626 });
627
628 this_compiled.other.iter().for_each(|(profile_name, data)| {
630 data.overrides.iter().for_each(|override_| {
631 check_test_group(profile_name, override_.data.test_group.as_ref());
632 });
633 });
634
635 if !unknown_group_errors.is_empty() {
637 let known_groups = TestGroup::make_all_groups(known_groups.iter().cloned()).collect();
638 return Err(ConfigParseError::new(
639 config_file,
640 tool,
641 ConfigParseErrorKind::UnknownTestGroups {
642 errors: unknown_group_errors,
643 known_groups,
644 },
645 ));
646 }
647
648 let mut profile_script_errors = ProfileScriptErrors::default();
651 let mut check_script_ids = |profile_name: &str,
652 script_type: ProfileScriptType,
653 expr: Option<&Filterset>,
654 scripts: &[ScriptId]| {
655 for script in scripts {
656 if let Some(script_info) = known_scripts.get(script) {
657 if !script_info.script_type.matches(script_type) {
658 profile_script_errors.wrong_script_types.push(
659 ProfileWrongConfigScriptTypeError {
660 profile_name: profile_name.to_owned(),
661 name: script.clone(),
662 attempted: script_type,
663 actual: script_info.script_type,
664 },
665 );
666 }
667 if script_type == ProfileScriptType::ListWrapper
668 && let Some(expr) = expr
669 {
670 let runtime_only_leaves = expr.parsed.runtime_only_leaves();
671 if !runtime_only_leaves.is_empty() {
672 let filters = runtime_only_leaves
673 .iter()
674 .map(|leaf| leaf.to_string())
675 .collect();
676 profile_script_errors.list_scripts_using_run_filters.push(
677 ProfileListScriptUsesRunFiltersError {
678 profile_name: profile_name.to_owned(),
679 name: script.clone(),
680 script_type,
681 filters,
682 },
683 );
684 }
685 }
686 } else {
687 profile_script_errors
688 .unknown_scripts
689 .push(ProfileUnknownScriptError {
690 profile_name: profile_name.to_owned(),
691 name: script.clone(),
692 });
693 }
694 }
695 };
696
697 let mut empty_script_count = 0;
698
699 this_compiled.default.scripts.iter().for_each(|scripts| {
700 if scripts.setup.is_empty()
701 && scripts.list_wrapper.is_none()
702 && scripts.run_wrapper.is_none()
703 {
704 empty_script_count += 1;
705 }
706
707 check_script_ids(
708 "default",
709 ProfileScriptType::Setup,
710 scripts.data.expr(),
711 &scripts.setup,
712 );
713 check_script_ids(
714 "default",
715 ProfileScriptType::ListWrapper,
716 scripts.data.expr(),
717 scripts.list_wrapper.as_slice(),
718 );
719 check_script_ids(
720 "default",
721 ProfileScriptType::RunWrapper,
722 scripts.data.expr(),
723 scripts.run_wrapper.as_slice(),
724 );
725 });
726
727 if empty_script_count > 0 {
728 warnings.empty_script_sections(
729 config_file,
730 workspace_root,
731 tool,
732 "default",
733 empty_script_count,
734 );
735 }
736
737 this_compiled.other.iter().for_each(|(profile_name, data)| {
738 let mut empty_script_count = 0;
739 data.scripts.iter().for_each(|scripts| {
740 if scripts.setup.is_empty()
741 && scripts.list_wrapper.is_none()
742 && scripts.run_wrapper.is_none()
743 {
744 empty_script_count += 1;
745 }
746
747 check_script_ids(
748 profile_name,
749 ProfileScriptType::Setup,
750 scripts.data.expr(),
751 &scripts.setup,
752 );
753 check_script_ids(
754 profile_name,
755 ProfileScriptType::ListWrapper,
756 scripts.data.expr(),
757 scripts.list_wrapper.as_slice(),
758 );
759 check_script_ids(
760 profile_name,
761 ProfileScriptType::RunWrapper,
762 scripts.data.expr(),
763 scripts.run_wrapper.as_slice(),
764 );
765 });
766
767 if empty_script_count > 0 {
768 warnings.empty_script_sections(
769 config_file,
770 workspace_root,
771 tool,
772 profile_name,
773 empty_script_count,
774 );
775 }
776 });
777
778 if !profile_script_errors.is_empty() {
781 let known_scripts = known_scripts
782 .iter()
783 .map(|script| script.id.clone())
784 .collect();
785 return Err(ConfigParseError::new(
786 config_file,
787 tool,
788 ConfigParseErrorKind::ProfileScriptErrors {
789 errors: Box::new(profile_script_errors),
790 known_scripts,
791 },
792 ));
793 }
794
795 compiled_out.default.extend_reverse(this_compiled.default);
798 for (name, mut data) in this_compiled.other {
799 match compiled_out.other.entry(name) {
800 hash_map::Entry::Vacant(entry) => {
801 data.reverse();
803 entry.insert(data);
804 }
805 hash_map::Entry::Occupied(mut entry) => {
806 entry.get_mut().extend_reverse(data);
808 }
809 }
810 }
811
812 Ok(())
813 }
814
815 fn make_default_config() -> ConfigBuilder<DefaultState> {
816 Config::builder().add_source(File::from_str(Self::DEFAULT_CONFIG, FileFormat::Toml))
817 }
818
819 fn make_profile(&self, name: &str) -> Result<EarlyProfile<'_>, ProfileNotFound> {
820 let custom_profile = self.inner.get_profile(name)?;
821
822 let inheritance_chain = self.inner.resolve_inheritance_chain(name)?;
824
825 let mut store_dir = self.workspace_root.join(&self.inner.store.dir);
827 store_dir.push(name);
828
829 let compiled_data = match self.compiled.other.get(name) {
831 Some(data) => data.clone().chain(self.compiled.default.clone()),
832 None => self.compiled.default.clone(),
833 };
834
835 Ok(EarlyProfile {
836 name: name.to_owned(),
837 store_dir,
838 default_profile: &self.inner.default_profile,
839 custom_profile,
840 inheritance_chain,
841 test_groups: &self.inner.test_groups,
842 scripts: &self.inner.scripts,
843 compiled_data,
844 })
845 }
846
847 fn build_and_deserialize_config(
849 builder: &ConfigBuilder<DefaultState>,
850 ) -> Result<(NextestConfigDeserialize, BTreeSet<String>), ConfigParseErrorKind> {
851 let config = builder
852 .build_cloned()
853 .map_err(|error| ConfigParseErrorKind::BuildError(Box::new(error)))?;
854
855 let mut ignored = BTreeSet::new();
856 let mut cb = |path: serde_ignored::Path| {
857 ignored.insert(path.to_string());
858 };
859 let ignored_de = serde_ignored::Deserializer::new(config, &mut cb);
860 let config: NextestConfigDeserialize = serde_path_to_error::deserialize(ignored_de)
861 .map_err(|error| {
862 let path = error.path().clone();
866 let config_error = error.into_inner();
867 let error = match config_error {
868 ConfigError::At { error, .. } => *error,
869 other => other,
870 };
871 ConfigParseErrorKind::DeserializeError(Box::new(serde_path_to_error::Error::new(
872 path, error,
873 )))
874 })?;
875
876 Ok((config, ignored))
877 }
878}
879
880#[derive(Clone, Debug, Default)]
882pub(in crate::config) struct PreBuildPlatform {}
883
884#[derive(Clone, Debug)]
886pub(crate) struct FinalConfig {
887 pub(in crate::config) host_eval: bool,
889 pub(in crate::config) host_test_eval: bool,
892 pub(in crate::config) target_eval: bool,
895}
896
897pub struct EarlyProfile<'cfg> {
902 name: String,
903 store_dir: Utf8PathBuf,
904 default_profile: &'cfg DefaultProfileImpl,
905 custom_profile: Option<&'cfg CustomProfileImpl>,
906 inheritance_chain: Vec<&'cfg CustomProfileImpl>,
907 test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
908 scripts: &'cfg ScriptConfig,
910 pub(in crate::config) compiled_data: CompiledData<PreBuildPlatform>,
912}
913
914impl<'cfg> EarlyProfile<'cfg> {
915 pub fn store_dir(&self) -> &Utf8Path {
917 &self.store_dir
918 }
919
920 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
922 self.test_groups
923 }
924
925 pub fn apply_build_platforms(
930 self,
931 build_platforms: &BuildPlatforms,
932 ) -> EvaluatableProfile<'cfg> {
933 let compiled_data = self.compiled_data.apply_build_platforms(build_platforms);
934
935 let resolved_default_filter = {
936 let found_filter = compiled_data
938 .overrides
939 .iter()
940 .find_map(|override_data| override_data.default_filter_if_matches_platform());
941 found_filter.unwrap_or_else(|| {
942 compiled_data
945 .profile_default_filter
946 .as_ref()
947 .expect("compiled data always has default set")
948 })
949 }
950 .clone();
951
952 EvaluatableProfile {
953 name: self.name,
954 store_dir: self.store_dir,
955 default_profile: self.default_profile,
956 custom_profile: self.custom_profile,
957 inheritance_chain: self.inheritance_chain,
958 scripts: self.scripts,
959 test_groups: self.test_groups,
960 compiled_data,
961 resolved_default_filter,
962 }
963 }
964}
965
966#[derive(Clone, Debug)]
970pub struct EvaluatableProfile<'cfg> {
971 name: String,
972 store_dir: Utf8PathBuf,
973 default_profile: &'cfg DefaultProfileImpl,
974 custom_profile: Option<&'cfg CustomProfileImpl>,
975 inheritance_chain: Vec<&'cfg CustomProfileImpl>,
976 test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
977 scripts: &'cfg ScriptConfig,
979 pub(in crate::config) compiled_data: CompiledData<FinalConfig>,
981 resolved_default_filter: CompiledDefaultFilter,
984}
985
986macro_rules! profile_field {
989 ($eval_prof:ident.$field:ident) => {
990 $eval_prof
991 .custom_profile
992 .iter()
993 .chain($eval_prof.inheritance_chain.iter())
994 .find_map(|p| p.$field)
995 .unwrap_or($eval_prof.default_profile.$field)
996 };
997 ($eval_prof:ident.$nested:ident.$field:ident) => {
998 $eval_prof
999 .custom_profile
1000 .iter()
1001 .chain($eval_prof.inheritance_chain.iter())
1002 .find_map(|p| p.$nested.$field)
1003 .unwrap_or($eval_prof.default_profile.$nested.$field)
1004 };
1005 ($eval_prof:ident.$method:ident($($arg:expr),*)) => {
1007 $eval_prof
1008 .custom_profile
1009 .iter()
1010 .chain($eval_prof.inheritance_chain.iter())
1011 .find_map(|p| p.$method($($arg),*))
1012 .unwrap_or_else(|| $eval_prof.default_profile.$method($($arg),*))
1013 };
1014}
1015macro_rules! profile_field_from_ref {
1016 ($eval_prof:ident.$field:ident.$ref_func:ident()) => {
1017 $eval_prof
1018 .custom_profile
1019 .iter()
1020 .chain($eval_prof.inheritance_chain.iter())
1021 .find_map(|p| p.$field.$ref_func())
1022 .unwrap_or(&$eval_prof.default_profile.$field)
1023 };
1024 ($eval_prof:ident.$nested:ident.$field:ident.$ref_func:ident()) => {
1025 $eval_prof
1026 .custom_profile
1027 .iter()
1028 .chain($eval_prof.inheritance_chain.iter())
1029 .find_map(|p| p.$nested.$field.$ref_func())
1030 .unwrap_or(&$eval_prof.default_profile.$nested.$field)
1031 };
1032}
1033macro_rules! profile_field_optional {
1035 ($eval_prof:ident.$nested:ident.$field:ident.$ref_func:ident()) => {
1036 $eval_prof
1037 .custom_profile
1038 .iter()
1039 .chain($eval_prof.inheritance_chain.iter())
1040 .find_map(|p| p.$nested.$field.$ref_func())
1041 .or($eval_prof.default_profile.$nested.$field.$ref_func())
1042 };
1043}
1044
1045impl<'cfg> EvaluatableProfile<'cfg> {
1046 pub fn name(&self) -> &str {
1048 &self.name
1049 }
1050
1051 pub fn store_dir(&self) -> &Utf8Path {
1053 &self.store_dir
1054 }
1055
1056 pub fn filterset_ecx(&self) -> EvalContext<'_> {
1058 EvalContext {
1059 default_filter: &self.default_filter().expr,
1060 }
1061 }
1062
1063 pub fn default_filter(&self) -> &CompiledDefaultFilter {
1065 &self.resolved_default_filter
1066 }
1067
1068 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
1070 self.test_groups
1071 }
1072
1073 pub fn script_config(&self) -> &'cfg ScriptConfig {
1075 self.scripts
1076 }
1077
1078 pub fn retries(&self) -> RetryPolicy {
1080 profile_field!(self.retries)
1081 }
1082
1083 pub fn test_threads(&self) -> TestThreads {
1085 profile_field!(self.test_threads)
1086 }
1087
1088 pub fn threads_required(&self) -> ThreadsRequired {
1090 profile_field!(self.threads_required)
1091 }
1092
1093 pub fn run_extra_args(&self) -> &'cfg [String] {
1095 profile_field_from_ref!(self.run_extra_args.as_deref())
1096 }
1097
1098 pub fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1100 profile_field!(self.slow_timeout(run_mode))
1101 }
1102
1103 pub fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1105 profile_field!(self.global_timeout(run_mode))
1106 }
1107
1108 pub fn leak_timeout(&self) -> LeakTimeout {
1111 profile_field!(self.leak_timeout)
1112 }
1113
1114 pub fn status_level(&self) -> StatusLevel {
1116 profile_field!(self.status_level)
1117 }
1118
1119 pub fn final_status_level(&self) -> FinalStatusLevel {
1121 profile_field!(self.final_status_level)
1122 }
1123
1124 pub fn failure_output(&self) -> TestOutputDisplay {
1126 profile_field!(self.failure_output)
1127 }
1128
1129 pub fn success_output(&self) -> TestOutputDisplay {
1131 profile_field!(self.success_output)
1132 }
1133
1134 pub fn max_fail(&self) -> MaxFail {
1136 profile_field!(self.max_fail)
1137 }
1138
1139 pub fn archive_config(&self) -> &'cfg ArchiveConfig {
1141 profile_field_from_ref!(self.archive.as_ref())
1142 }
1143
1144 pub fn setup_scripts(&self, test_list: &TestList<'_>) -> SetupScripts<'_> {
1146 SetupScripts::new(self, test_list)
1147 }
1148
1149 pub fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1151 ListSettings::new(self, query)
1152 }
1153
1154 pub fn settings_for(
1156 &self,
1157 run_mode: NextestRunMode,
1158 query: &TestQuery<'_>,
1159 ) -> TestSettings<'_> {
1160 TestSettings::new(self, run_mode, query)
1161 }
1162
1163 pub(crate) fn settings_with_source_for(
1165 &self,
1166 run_mode: NextestRunMode,
1167 query: &TestQuery<'_>,
1168 ) -> TestSettings<'_, SettingSource<'_>> {
1169 TestSettings::new(self, run_mode, query)
1170 }
1171
1172 pub fn junit(&self) -> Option<JunitConfig<'cfg>> {
1174 let settings = JunitSettings {
1175 path: profile_field_optional!(self.junit.path.as_deref()),
1176 report_name: profile_field_from_ref!(self.junit.report_name.as_deref()),
1177 store_success_output: profile_field!(self.junit.store_success_output),
1178 store_failure_output: profile_field!(self.junit.store_failure_output),
1179 };
1180 JunitConfig::new(self.store_dir(), settings)
1181 }
1182
1183 pub fn inherits(&self) -> Option<&str> {
1185 if let Some(custom_profile) = self.custom_profile {
1186 return custom_profile.inherits();
1187 }
1188 None
1189 }
1190
1191 #[cfg(test)]
1192 pub(in crate::config) fn custom_profile(&self) -> Option<&'cfg CustomProfileImpl> {
1193 self.custom_profile
1194 }
1195}
1196
1197#[derive(Clone, Debug)]
1198pub(in crate::config) struct NextestConfigImpl {
1199 store: StoreConfigImpl,
1200 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1201 scripts: ScriptConfig,
1202 default_profile: DefaultProfileImpl,
1203 other_profiles: HashMap<String, CustomProfileImpl>,
1204}
1205
1206impl NextestConfigImpl {
1207 fn get_profile(&self, profile: &str) -> Result<Option<&CustomProfileImpl>, ProfileNotFound> {
1208 let custom_profile = match profile {
1209 NextestConfig::DEFAULT_PROFILE => None,
1210 other => Some(
1211 self.other_profiles
1212 .get(other)
1213 .ok_or_else(|| ProfileNotFound::new(profile, self.all_profiles()))?,
1214 ),
1215 };
1216 Ok(custom_profile)
1217 }
1218
1219 fn all_profiles(&self) -> impl Iterator<Item = &str> {
1220 self.other_profiles
1221 .keys()
1222 .map(|key| key.as_str())
1223 .chain(std::iter::once(NextestConfig::DEFAULT_PROFILE))
1224 }
1225
1226 pub(in crate::config) fn default_profile(&self) -> &DefaultProfileImpl {
1227 &self.default_profile
1228 }
1229
1230 pub(in crate::config) fn other_profiles(
1231 &self,
1232 ) -> impl Iterator<Item = (&str, &CustomProfileImpl)> {
1233 self.other_profiles
1234 .iter()
1235 .map(|(key, value)| (key.as_str(), value))
1236 }
1237
1238 fn resolve_inheritance_chain(
1244 &self,
1245 profile_name: &str,
1246 ) -> Result<Vec<&CustomProfileImpl>, ProfileNotFound> {
1247 let mut chain = Vec::new();
1248
1249 let mut curr = self
1252 .get_profile(profile_name)?
1253 .and_then(|p| p.inherits.as_deref());
1254
1255 while let Some(name) = curr {
1256 let profile = self.get_profile(name)?;
1257 if let Some(profile) = profile {
1258 chain.push(profile);
1259 curr = profile.inherits.as_deref();
1260 } else {
1261 break;
1263 }
1264 }
1265
1266 Ok(chain)
1267 }
1268
1269 fn sanitize_profile_inherits(
1274 &self,
1275 known_profiles: &BTreeSet<String>,
1276 ) -> Result<(), ConfigParseErrorKind> {
1277 let mut inherit_err_collector = Vec::new();
1278
1279 self.sanitize_default_profile_inherits(&mut inherit_err_collector);
1280 self.sanitize_custom_profile_inherits(&mut inherit_err_collector, known_profiles);
1281
1282 if !inherit_err_collector.is_empty() {
1283 return Err(ConfigParseErrorKind::InheritanceErrors(
1284 inherit_err_collector,
1285 ));
1286 }
1287
1288 Ok(())
1289 }
1290
1291 fn sanitize_default_profile_inherits(&self, inherit_err_collector: &mut Vec<InheritsError>) {
1294 if self.default_profile().inherits().is_some() {
1295 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(
1296 NextestConfig::DEFAULT_PROFILE.to_string(),
1297 ));
1298 }
1299 }
1300
1301 fn sanitize_custom_profile_inherits(
1303 &self,
1304 inherit_err_collector: &mut Vec<InheritsError>,
1305 known_profiles: &BTreeSet<String>,
1306 ) {
1307 let mut profile_graph = Graph::<&str, (), Directed>::new();
1308 let mut profile_map = HashMap::new();
1309
1310 for (name, custom_profile) in self.other_profiles() {
1313 let starts_with_default = self.sanitize_custom_default_profile_inherits(
1314 name,
1315 custom_profile,
1316 inherit_err_collector,
1317 );
1318 if !starts_with_default {
1319 self.add_profile_to_graph(
1324 name,
1325 custom_profile,
1326 &mut profile_map,
1327 &mut profile_graph,
1328 inherit_err_collector,
1329 known_profiles,
1330 );
1331 }
1332 }
1333
1334 self.check_inheritance_cycles(profile_graph, inherit_err_collector);
1335 }
1336
1337 fn sanitize_custom_default_profile_inherits(
1340 &self,
1341 name: &str,
1342 custom_profile: &CustomProfileImpl,
1343 inherit_err_collector: &mut Vec<InheritsError>,
1344 ) -> bool {
1345 let starts_with_default = name.starts_with("default-");
1346
1347 if starts_with_default && custom_profile.inherits().is_some() {
1348 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(name.to_string()));
1349 }
1350
1351 starts_with_default
1352 }
1353
1354 fn add_profile_to_graph<'cfg>(
1359 &self,
1360 name: &'cfg str,
1361 custom_profile: &'cfg CustomProfileImpl,
1362 profile_map: &mut HashMap<&'cfg str, NodeIndex>,
1363 profile_graph: &mut Graph<&'cfg str, ()>,
1364 inherit_err_collector: &mut Vec<InheritsError>,
1365 known_profiles: &BTreeSet<String>,
1366 ) {
1367 if let Some(inherits_name) = custom_profile.inherits() {
1368 if inherits_name == name {
1369 inherit_err_collector
1370 .push(InheritsError::SelfReferentialInheritance(name.to_string()))
1371 } else if self.get_profile(inherits_name).is_ok() {
1372 let from_node = match profile_map.get(name) {
1374 None => {
1375 let profile_node = profile_graph.add_node(name);
1376 profile_map.insert(name, profile_node);
1377 profile_node
1378 }
1379 Some(node_idx) => *node_idx,
1380 };
1381 let to_node = match profile_map.get(inherits_name) {
1382 None => {
1383 let profile_node = profile_graph.add_node(inherits_name);
1384 profile_map.insert(inherits_name, profile_node);
1385 profile_node
1386 }
1387 Some(node_idx) => *node_idx,
1388 };
1389 profile_graph.add_edge(from_node, to_node, ());
1390 } else if known_profiles.contains(inherits_name) {
1391 } else {
1395 inherit_err_collector.push(InheritsError::UnknownInheritance(
1396 name.to_string(),
1397 inherits_name.to_string(),
1398 ))
1399 }
1400 }
1401 }
1402
1403 fn check_inheritance_cycles(
1405 &self,
1406 profile_graph: Graph<&str, ()>,
1407 inherit_err_collector: &mut Vec<InheritsError>,
1408 ) {
1409 let profile_sccs: Vec<Vec<NodeIndex>> = kosaraju_scc(&profile_graph);
1410 let profile_sccs: Vec<Vec<NodeIndex>> = profile_sccs
1411 .into_iter()
1412 .filter(|scc| scc.len() >= 2)
1413 .collect();
1414
1415 if !profile_sccs.is_empty() {
1416 inherit_err_collector.push(InheritsError::InheritanceCycle(
1417 profile_sccs
1418 .iter()
1419 .map(|node_idxs| {
1420 let profile_names: Vec<String> = node_idxs
1421 .iter()
1422 .map(|node_idx| profile_graph[*node_idx].to_string())
1423 .collect();
1424 profile_names
1425 })
1426 .collect(),
1427 ));
1428 }
1429 }
1430}
1431
1432#[derive(Clone, Debug, Deserialize)]
1434#[serde(rename_all = "kebab-case")]
1435struct NextestConfigDeserialize {
1436 store: StoreConfigImpl,
1437
1438 #[expect(unused)]
1441 #[serde(default)]
1442 nextest_version: Option<NextestVersionDeserialize>,
1443 #[expect(unused)]
1444 #[serde(default)]
1445 experimental: BTreeSet<String>,
1446
1447 #[serde(default)]
1448 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1449 #[serde(default, rename = "script")]
1451 old_setup_scripts: IndexMap<ScriptId, SetupScriptConfig>,
1452 #[serde(default)]
1453 scripts: ScriptConfig,
1454 #[serde(rename = "profile")]
1455 profiles: HashMap<String, CustomProfileImpl>,
1456}
1457
1458impl NextestConfigDeserialize {
1459 fn into_config_impl(mut self) -> NextestConfigImpl {
1460 let p = self
1461 .profiles
1462 .remove("default")
1463 .expect("default profile should exist");
1464 let default_profile = DefaultProfileImpl::new(p);
1465
1466 for (script_id, script_config) in self.old_setup_scripts {
1471 if let indexmap::map::Entry::Vacant(entry) = self.scripts.setup.entry(script_id) {
1472 entry.insert(script_config);
1473 }
1474 }
1475
1476 NextestConfigImpl {
1477 store: self.store,
1478 default_profile,
1479 test_groups: self.test_groups,
1480 scripts: self.scripts,
1481 other_profiles: self.profiles,
1482 }
1483 }
1484}
1485
1486#[derive(Clone, Debug, Deserialize)]
1487#[serde(rename_all = "kebab-case")]
1488struct StoreConfigImpl {
1489 dir: Utf8PathBuf,
1490}
1491
1492#[derive(Clone, Debug)]
1493pub(in crate::config) struct DefaultProfileImpl {
1494 default_filter: String,
1495 test_threads: TestThreads,
1496 threads_required: ThreadsRequired,
1497 run_extra_args: Vec<String>,
1498 retries: RetryPolicy,
1499 status_level: StatusLevel,
1500 final_status_level: FinalStatusLevel,
1501 failure_output: TestOutputDisplay,
1502 success_output: TestOutputDisplay,
1503 max_fail: MaxFail,
1504 slow_timeout: SlowTimeout,
1505 global_timeout: GlobalTimeout,
1506 leak_timeout: LeakTimeout,
1507 overrides: Vec<DeserializedOverride>,
1508 scripts: Vec<DeserializedProfileScriptConfig>,
1509 junit: DefaultJunitImpl,
1510 archive: ArchiveConfig,
1511 bench: DefaultBenchConfig,
1512 inherits: Inherits,
1513}
1514
1515impl DefaultProfileImpl {
1516 fn new(p: CustomProfileImpl) -> Self {
1517 Self {
1518 default_filter: p
1519 .default_filter
1520 .expect("default-filter present in default profile"),
1521 test_threads: p
1522 .test_threads
1523 .expect("test-threads present in default profile"),
1524 threads_required: p
1525 .threads_required
1526 .expect("threads-required present in default profile"),
1527 run_extra_args: p
1528 .run_extra_args
1529 .expect("run-extra-args present in default profile"),
1530 retries: p.retries.expect("retries present in default profile"),
1531 status_level: p
1532 .status_level
1533 .expect("status-level present in default profile"),
1534 final_status_level: p
1535 .final_status_level
1536 .expect("final-status-level present in default profile"),
1537 failure_output: p
1538 .failure_output
1539 .expect("failure-output present in default profile"),
1540 success_output: p
1541 .success_output
1542 .expect("success-output present in default profile"),
1543 max_fail: p.max_fail.expect("fail-fast present in default profile"),
1544 slow_timeout: p
1545 .slow_timeout
1546 .expect("slow-timeout present in default profile"),
1547 global_timeout: p
1548 .global_timeout
1549 .expect("global-timeout present in default profile"),
1550 leak_timeout: p
1551 .leak_timeout
1552 .expect("leak-timeout present in default profile"),
1553 overrides: p.overrides,
1554 scripts: p.scripts,
1555 junit: DefaultJunitImpl::for_default_profile(p.junit),
1556 archive: p.archive.expect("archive present in default profile"),
1557 bench: DefaultBenchConfig::for_default_profile(
1558 p.bench.expect("bench present in default profile"),
1559 ),
1560 inherits: Inherits::new(p.inherits),
1561 }
1562 }
1563
1564 pub(in crate::config) fn default_filter(&self) -> &str {
1565 &self.default_filter
1566 }
1567
1568 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1569 self.inherits.inherits_from()
1570 }
1571
1572 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1573 &self.overrides
1574 }
1575
1576 pub(in crate::config) fn setup_scripts(&self) -> &[DeserializedProfileScriptConfig] {
1577 &self.scripts
1578 }
1579
1580 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1581 match run_mode {
1582 NextestRunMode::Test => self.slow_timeout,
1583 NextestRunMode::Benchmark => self.bench.slow_timeout,
1584 }
1585 }
1586
1587 pub(in crate::config) fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1588 match run_mode {
1589 NextestRunMode::Test => self.global_timeout,
1590 NextestRunMode::Benchmark => self.bench.global_timeout,
1591 }
1592 }
1593}
1594
1595#[derive(Clone, Debug, Deserialize)]
1596#[serde(rename_all = "kebab-case")]
1597pub(in crate::config) struct CustomProfileImpl {
1598 #[serde(default)]
1600 default_filter: Option<String>,
1601 #[serde(default, deserialize_with = "deserialize_retry_policy")]
1602 retries: Option<RetryPolicy>,
1603 #[serde(default)]
1604 test_threads: Option<TestThreads>,
1605 #[serde(default)]
1606 threads_required: Option<ThreadsRequired>,
1607 #[serde(default)]
1608 run_extra_args: Option<Vec<String>>,
1609 #[serde(default)]
1610 status_level: Option<StatusLevel>,
1611 #[serde(default)]
1612 final_status_level: Option<FinalStatusLevel>,
1613 #[serde(default)]
1614 failure_output: Option<TestOutputDisplay>,
1615 #[serde(default)]
1616 success_output: Option<TestOutputDisplay>,
1617 #[serde(
1618 default,
1619 rename = "fail-fast",
1620 deserialize_with = "deserialize_fail_fast"
1621 )]
1622 max_fail: Option<MaxFail>,
1623 #[serde(default, deserialize_with = "deserialize_slow_timeout")]
1624 slow_timeout: Option<SlowTimeout>,
1625 #[serde(default)]
1626 global_timeout: Option<GlobalTimeout>,
1627 #[serde(default, deserialize_with = "deserialize_leak_timeout")]
1628 leak_timeout: Option<LeakTimeout>,
1629 #[serde(default)]
1630 overrides: Vec<DeserializedOverride>,
1631 #[serde(default)]
1632 scripts: Vec<DeserializedProfileScriptConfig>,
1633 #[serde(default)]
1634 junit: JunitImpl,
1635 #[serde(default)]
1636 archive: Option<ArchiveConfig>,
1637 #[serde(default)]
1638 bench: Option<BenchConfig>,
1639 #[serde(default)]
1640 inherits: Option<String>,
1641}
1642
1643impl CustomProfileImpl {
1644 #[cfg(test)]
1645 pub(in crate::config) fn test_threads(&self) -> Option<TestThreads> {
1646 self.test_threads
1647 }
1648
1649 pub(in crate::config) fn default_filter(&self) -> Option<&str> {
1650 self.default_filter.as_deref()
1651 }
1652
1653 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> Option<SlowTimeout> {
1654 match run_mode {
1655 NextestRunMode::Test => self.slow_timeout,
1656 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.slow_timeout),
1657 }
1658 }
1659
1660 pub(in crate::config) fn global_timeout(
1661 &self,
1662 run_mode: NextestRunMode,
1663 ) -> Option<GlobalTimeout> {
1664 match run_mode {
1665 NextestRunMode::Test => self.global_timeout,
1666 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.global_timeout),
1667 }
1668 }
1669
1670 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1671 self.inherits.as_deref()
1672 }
1673
1674 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1675 &self.overrides
1676 }
1677
1678 pub(in crate::config) fn scripts(&self) -> &[DeserializedProfileScriptConfig] {
1679 &self.scripts
1680 }
1681}
1682
1683#[cfg(test)]
1684mod tests {
1685 use super::*;
1686 use crate::config::utils::test_helpers::*;
1687 use camino_tempfile::tempdir;
1688 use iddqd::{IdHashItem, IdHashMap, id_hash_map, id_upcast};
1689
1690 fn tool_name(s: &str) -> ToolName {
1691 ToolName::new(s.into()).unwrap()
1692 }
1693
1694 #[derive(Default)]
1696 struct TestConfigWarnings {
1697 unknown_keys: IdHashMap<UnknownKeys>,
1698 reserved_profiles: IdHashMap<ReservedProfiles>,
1699 deprecated_scripts: IdHashMap<DeprecatedScripts>,
1700 empty_script_warnings: IdHashMap<EmptyScriptSections>,
1701 }
1702
1703 impl ConfigWarnings for TestConfigWarnings {
1704 fn unknown_config_keys(
1705 &mut self,
1706 config_file: &Utf8Path,
1707 _workspace_root: &Utf8Path,
1708 tool: Option<&ToolName>,
1709 unknown: &BTreeSet<String>,
1710 ) {
1711 self.unknown_keys
1712 .insert_unique(UnknownKeys {
1713 tool: tool.cloned(),
1714 config_file: config_file.to_owned(),
1715 keys: unknown.clone(),
1716 })
1717 .unwrap();
1718 }
1719
1720 fn unknown_reserved_profiles(
1721 &mut self,
1722 config_file: &Utf8Path,
1723 _workspace_root: &Utf8Path,
1724 tool: Option<&ToolName>,
1725 profiles: &[&str],
1726 ) {
1727 self.reserved_profiles
1728 .insert_unique(ReservedProfiles {
1729 tool: tool.cloned(),
1730 config_file: config_file.to_owned(),
1731 profiles: profiles.iter().map(|&s| s.to_owned()).collect(),
1732 })
1733 .unwrap();
1734 }
1735
1736 fn empty_script_sections(
1737 &mut self,
1738 config_file: &Utf8Path,
1739 _workspace_root: &Utf8Path,
1740 tool: Option<&ToolName>,
1741 profile_name: &str,
1742 empty_count: usize,
1743 ) {
1744 self.empty_script_warnings
1745 .insert_unique(EmptyScriptSections {
1746 tool: tool.cloned(),
1747 config_file: config_file.to_owned(),
1748 profile_name: profile_name.to_owned(),
1749 empty_count,
1750 })
1751 .unwrap();
1752 }
1753
1754 fn deprecated_script_config(
1755 &mut self,
1756 config_file: &Utf8Path,
1757 _workspace_root: &Utf8Path,
1758 tool: Option<&ToolName>,
1759 ) {
1760 self.deprecated_scripts
1761 .insert_unique(DeprecatedScripts {
1762 tool: tool.cloned(),
1763 config_file: config_file.to_owned(),
1764 })
1765 .unwrap();
1766 }
1767 }
1768
1769 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1770 struct UnknownKeys {
1771 tool: Option<ToolName>,
1772 config_file: Utf8PathBuf,
1773 keys: BTreeSet<String>,
1774 }
1775
1776 impl IdHashItem for UnknownKeys {
1777 type Key<'a> = Option<&'a ToolName>;
1778 fn key(&self) -> Self::Key<'_> {
1779 self.tool.as_ref()
1780 }
1781 id_upcast!();
1782 }
1783
1784 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1785 struct ReservedProfiles {
1786 tool: Option<ToolName>,
1787 config_file: Utf8PathBuf,
1788 profiles: Vec<String>,
1789 }
1790
1791 impl IdHashItem for ReservedProfiles {
1792 type Key<'a> = Option<&'a ToolName>;
1793 fn key(&self) -> Self::Key<'_> {
1794 self.tool.as_ref()
1795 }
1796 id_upcast!();
1797 }
1798
1799 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1800 struct DeprecatedScripts {
1801 tool: Option<ToolName>,
1802 config_file: Utf8PathBuf,
1803 }
1804
1805 impl IdHashItem for DeprecatedScripts {
1806 type Key<'a> = Option<&'a ToolName>;
1807 fn key(&self) -> Self::Key<'_> {
1808 self.tool.as_ref()
1809 }
1810 id_upcast!();
1811 }
1812
1813 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1814 struct EmptyScriptSections {
1815 tool: Option<ToolName>,
1816 config_file: Utf8PathBuf,
1817 profile_name: String,
1818 empty_count: usize,
1819 }
1820
1821 impl IdHashItem for EmptyScriptSections {
1822 type Key<'a> = (&'a Option<ToolName>, &'a str);
1823 fn key(&self) -> Self::Key<'_> {
1824 (&self.tool, &self.profile_name)
1825 }
1826 id_upcast!();
1827 }
1828
1829 #[test]
1830 fn default_config_is_valid() {
1831 let default_config = NextestConfig::default_config("foo");
1832 default_config
1833 .profile(NextestConfig::DEFAULT_PROFILE)
1834 .expect("default profile should exist");
1835 }
1836
1837 #[test]
1838 fn ignored_keys() {
1839 let config_contents = r#"
1840 ignored1 = "test"
1841
1842 [profile.default]
1843 retries = 3
1844 ignored2 = "hi"
1845
1846 [profile.default-foo]
1847 retries = 5
1848
1849 [[profile.default.overrides]]
1850 filter = 'test(test_foo)'
1851 retries = 20
1852 ignored3 = 42
1853 "#;
1854
1855 let tool_config_contents = r#"
1856 [store]
1857 ignored4 = 20
1858
1859 [profile.default]
1860 retries = 4
1861 ignored5 = false
1862
1863 [profile.default-bar]
1864 retries = 5
1865
1866 [profile.tool]
1867 retries = 12
1868
1869 [[profile.tool.overrides]]
1870 filter = 'test(test_baz)'
1871 retries = 22
1872 ignored6 = 6.5
1873 "#;
1874
1875 let workspace_dir = tempdir().unwrap();
1876
1877 let graph = temp_workspace(&workspace_dir, config_contents);
1878 let workspace_root = graph.workspace().root();
1879 let tool_path = workspace_root.join(".config/tool.toml");
1880 std::fs::write(&tool_path, tool_config_contents).unwrap();
1881
1882 let pcx = ParseContext::new(&graph);
1883
1884 let mut warnings = TestConfigWarnings::default();
1885
1886 let _ = NextestConfig::from_sources_with_warnings(
1887 workspace_root,
1888 &pcx,
1889 None,
1890 &[ToolConfigFile {
1891 tool: tool_name("my-tool"),
1892 config_file: tool_path.clone(),
1893 }][..],
1894 &Default::default(),
1895 &mut warnings,
1896 )
1897 .expect("config is valid");
1898
1899 assert_eq!(
1900 warnings.unknown_keys.len(),
1901 2,
1902 "there are two files with unknown keys"
1903 );
1904
1905 assert_eq!(
1906 warnings.unknown_keys,
1907 id_hash_map! {
1908 UnknownKeys {
1909 tool: None,
1910 config_file: workspace_root.join(".config/nextest.toml"),
1911 keys: maplit::btreeset! {
1912 "ignored1".to_owned(),
1913 "profile.default.ignored2".to_owned(),
1914 "profile.default.overrides.0.ignored3".to_owned(),
1915 }
1916 },
1917 UnknownKeys {
1918 tool: Some(tool_name("my-tool")),
1919 config_file: tool_path.clone(),
1920 keys: maplit::btreeset! {
1921 "store.ignored4".to_owned(),
1922 "profile.default.ignored5".to_owned(),
1923 "profile.tool.overrides.0.ignored6".to_owned(),
1924 }
1925 }
1926 }
1927 );
1928 assert_eq!(
1929 warnings.reserved_profiles,
1930 id_hash_map! {
1931 ReservedProfiles {
1932 tool: None,
1933 config_file: workspace_root.join(".config/nextest.toml"),
1934 profiles: vec!["default-foo".to_owned()],
1935 },
1936 ReservedProfiles {
1937 tool: Some(tool_name("my-tool")),
1938 config_file: tool_path,
1939 profiles: vec!["default-bar".to_owned()],
1940 }
1941 },
1942 )
1943 }
1944
1945 #[test]
1946 fn script_warnings() {
1947 let config_contents = r#"
1948 experimental = ["setup-scripts", "wrapper-scripts"]
1949
1950 [scripts.wrapper.script1]
1951 command = "echo test"
1952
1953 [scripts.wrapper.script2]
1954 command = "echo test2"
1955
1956 [scripts.setup.script3]
1957 command = "echo setup"
1958
1959 [[profile.default.scripts]]
1960 filter = 'all()'
1961 # Empty - no setup or wrapper scripts
1962
1963 [[profile.default.scripts]]
1964 filter = 'test(foo)'
1965 setup = ["script3"]
1966
1967 [profile.custom]
1968 [[profile.custom.scripts]]
1969 filter = 'all()'
1970 # Empty - no setup or wrapper scripts
1971
1972 [[profile.custom.scripts]]
1973 filter = 'test(bar)'
1974 # Another empty section
1975 "#;
1976
1977 let tool_config_contents = r#"
1978 experimental = ["setup-scripts", "wrapper-scripts"]
1979
1980 [scripts.wrapper."@tool:tool:disabled_script"]
1981 command = "echo disabled"
1982
1983 [scripts.setup."@tool:tool:setup_script"]
1984 command = "echo setup"
1985
1986 [profile.tool]
1987 [[profile.tool.scripts]]
1988 filter = 'all()'
1989 # Empty section
1990
1991 [[profile.tool.scripts]]
1992 filter = 'test(foo)'
1993 setup = ["@tool:tool:setup_script"]
1994 "#;
1995
1996 let workspace_dir = tempdir().unwrap();
1997 let graph = temp_workspace(&workspace_dir, config_contents);
1998 let workspace_root = graph.workspace().root();
1999 let tool_path = workspace_root.join(".config/tool.toml");
2000 std::fs::write(&tool_path, tool_config_contents).unwrap();
2001
2002 let pcx = ParseContext::new(&graph);
2003
2004 let mut warnings = TestConfigWarnings::default();
2005
2006 let experimental = maplit::btreeset! {
2007 ConfigExperimental::SetupScripts,
2008 ConfigExperimental::WrapperScripts
2009 };
2010 let _ = NextestConfig::from_sources_with_warnings(
2011 workspace_root,
2012 &pcx,
2013 None,
2014 &[ToolConfigFile {
2015 tool: tool_name("tool"),
2016 config_file: tool_path.clone(),
2017 }][..],
2018 &experimental,
2019 &mut warnings,
2020 )
2021 .expect("config is valid");
2022
2023 assert_eq!(
2024 warnings.empty_script_warnings,
2025 id_hash_map! {
2026 EmptyScriptSections {
2027 tool: None,
2028 config_file: workspace_root.join(".config/nextest.toml"),
2029 profile_name: "default".to_owned(),
2030 empty_count: 1,
2031 },
2032 EmptyScriptSections {
2033 tool: None,
2034 config_file: workspace_root.join(".config/nextest.toml"),
2035 profile_name: "custom".to_owned(),
2036 empty_count: 2,
2037 },
2038 EmptyScriptSections {
2039 tool: Some(tool_name("tool")),
2040 config_file: tool_path,
2041 profile_name: "tool".to_owned(),
2042 empty_count: 1,
2043 }
2044 }
2045 );
2046 }
2047
2048 #[test]
2049 fn deprecated_script_config_warning() {
2050 let config_contents = r#"
2051 experimental = ["setup-scripts"]
2052
2053 [script.my-script]
2054 command = "echo hello"
2055"#;
2056
2057 let tool_config_contents = r#"
2058 experimental = ["setup-scripts"]
2059
2060 [script."@tool:my-tool:my-script"]
2061 command = "echo hello"
2062"#;
2063
2064 let temp_dir = tempdir().unwrap();
2065
2066 let graph = temp_workspace(&temp_dir, config_contents);
2067 let workspace_root = graph.workspace().root();
2068 let tool_path = workspace_root.join(".config/my-tool.toml");
2069 std::fs::write(&tool_path, tool_config_contents).unwrap();
2070 let pcx = ParseContext::new(&graph);
2071
2072 let mut warnings = TestConfigWarnings::default();
2073 NextestConfig::from_sources_with_warnings(
2074 graph.workspace().root(),
2075 &pcx,
2076 None,
2077 &[ToolConfigFile {
2078 tool: tool_name("my-tool"),
2079 config_file: tool_path.clone(),
2080 }],
2081 &maplit::btreeset! {ConfigExperimental::SetupScripts},
2082 &mut warnings,
2083 )
2084 .expect("config is valid");
2085
2086 assert_eq!(
2087 warnings.deprecated_scripts,
2088 id_hash_map! {
2089 DeprecatedScripts {
2090 tool: None,
2091 config_file: graph.workspace().root().join(".config/nextest.toml"),
2092 },
2093 DeprecatedScripts {
2094 tool: Some(tool_name("my-tool")),
2095 config_file: tool_path,
2096 }
2097 }
2098 );
2099 }
2100}