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 },
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
914macro_rules! profile_field {
917 ($eval_prof:ident.$field:ident) => {
918 $eval_prof
919 .custom_profile
920 .iter()
921 .chain($eval_prof.inheritance_chain.iter())
922 .find_map(|p| p.$field)
923 .unwrap_or($eval_prof.default_profile.$field)
924 };
925 ($eval_prof:ident.$nested:ident.$field:ident) => {
926 $eval_prof
927 .custom_profile
928 .iter()
929 .chain($eval_prof.inheritance_chain.iter())
930 .find_map(|p| p.$nested.$field)
931 .unwrap_or($eval_prof.default_profile.$nested.$field)
932 };
933 ($eval_prof:ident.$method:ident($($arg:expr),*)) => {
935 $eval_prof
936 .custom_profile
937 .iter()
938 .chain($eval_prof.inheritance_chain.iter())
939 .find_map(|p| p.$method($($arg),*))
940 .unwrap_or_else(|| $eval_prof.default_profile.$method($($arg),*))
941 };
942}
943macro_rules! profile_field_from_ref {
944 ($eval_prof:ident.$field:ident.$ref_func:ident()) => {
945 $eval_prof
946 .custom_profile
947 .iter()
948 .chain($eval_prof.inheritance_chain.iter())
949 .find_map(|p| p.$field.$ref_func())
950 .unwrap_or(&$eval_prof.default_profile.$field)
951 };
952 ($eval_prof:ident.$nested:ident.$field:ident.$ref_func:ident()) => {
953 $eval_prof
954 .custom_profile
955 .iter()
956 .chain($eval_prof.inheritance_chain.iter())
957 .find_map(|p| p.$nested.$field.$ref_func())
958 .unwrap_or(&$eval_prof.default_profile.$nested.$field)
959 };
960}
961macro_rules! profile_field_optional {
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 .or($eval_prof.default_profile.$nested.$field.$ref_func())
970 };
971}
972
973impl<'cfg> EarlyProfile<'cfg> {
974 pub fn store_dir(&self) -> &Utf8Path {
976 &self.store_dir
977 }
978
979 pub fn has_junit(&self) -> bool {
981 profile_field_optional!(self.junit.path.as_deref()).is_some()
982 }
983
984 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
986 self.test_groups
987 }
988
989 pub fn apply_build_platforms(
994 self,
995 build_platforms: &BuildPlatforms,
996 ) -> EvaluatableProfile<'cfg> {
997 let compiled_data = self.compiled_data.apply_build_platforms(build_platforms);
998
999 let resolved_default_filter = {
1000 let found_filter = compiled_data
1002 .overrides
1003 .iter()
1004 .find_map(|override_data| override_data.default_filter_if_matches_platform());
1005 found_filter.unwrap_or_else(|| {
1006 compiled_data
1009 .profile_default_filter
1010 .as_ref()
1011 .expect("compiled data always has default set")
1012 })
1013 }
1014 .clone();
1015
1016 EvaluatableProfile {
1017 name: self.name,
1018 store_dir: self.store_dir,
1019 default_profile: self.default_profile,
1020 custom_profile: self.custom_profile,
1021 inheritance_chain: self.inheritance_chain,
1022 scripts: self.scripts,
1023 test_groups: self.test_groups,
1024 compiled_data,
1025 resolved_default_filter,
1026 }
1027 }
1028}
1029
1030#[derive(Clone, Debug)]
1034pub struct EvaluatableProfile<'cfg> {
1035 name: String,
1036 store_dir: Utf8PathBuf,
1037 default_profile: &'cfg DefaultProfileImpl,
1038 custom_profile: Option<&'cfg CustomProfileImpl>,
1039 inheritance_chain: Vec<&'cfg CustomProfileImpl>,
1040 test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
1041 scripts: &'cfg ScriptConfig,
1043 pub(in crate::config) compiled_data: CompiledData<FinalConfig>,
1045 resolved_default_filter: CompiledDefaultFilter,
1048}
1049
1050impl<'cfg> EvaluatableProfile<'cfg> {
1051 pub fn name(&self) -> &str {
1053 &self.name
1054 }
1055
1056 pub fn store_dir(&self) -> &Utf8Path {
1058 &self.store_dir
1059 }
1060
1061 pub fn filterset_ecx(&self) -> EvalContext<'_> {
1063 EvalContext {
1064 default_filter: &self.default_filter().expr,
1065 }
1066 }
1067
1068 pub fn default_filter(&self) -> &CompiledDefaultFilter {
1070 &self.resolved_default_filter
1071 }
1072
1073 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
1075 self.test_groups
1076 }
1077
1078 pub fn script_config(&self) -> &'cfg ScriptConfig {
1080 self.scripts
1081 }
1082
1083 pub fn retries(&self) -> RetryPolicy {
1085 profile_field!(self.retries)
1086 }
1087
1088 pub fn flaky_result(&self) -> FlakyResult {
1090 profile_field!(self.flaky_result)
1091 }
1092
1093 pub fn test_threads(&self) -> TestThreads {
1095 profile_field!(self.test_threads)
1096 }
1097
1098 pub fn threads_required(&self) -> ThreadsRequired {
1100 profile_field!(self.threads_required)
1101 }
1102
1103 pub fn run_extra_args(&self) -> &'cfg [String] {
1105 profile_field_from_ref!(self.run_extra_args.as_deref())
1106 }
1107
1108 pub fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1110 profile_field!(self.slow_timeout(run_mode))
1111 }
1112
1113 pub fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1115 profile_field!(self.global_timeout(run_mode))
1116 }
1117
1118 pub fn leak_timeout(&self) -> LeakTimeout {
1121 profile_field!(self.leak_timeout)
1122 }
1123
1124 pub fn status_level(&self) -> StatusLevel {
1126 profile_field!(self.status_level)
1127 }
1128
1129 pub fn final_status_level(&self) -> FinalStatusLevel {
1131 profile_field!(self.final_status_level)
1132 }
1133
1134 pub fn failure_output(&self) -> TestOutputDisplay {
1136 profile_field!(self.failure_output)
1137 }
1138
1139 pub fn success_output(&self) -> TestOutputDisplay {
1141 profile_field!(self.success_output)
1142 }
1143
1144 pub fn max_fail(&self) -> MaxFail {
1146 profile_field!(self.max_fail)
1147 }
1148
1149 pub fn archive_config(&self) -> &'cfg ArchiveConfig {
1151 profile_field_from_ref!(self.archive.as_ref())
1152 }
1153
1154 pub fn setup_scripts(&self, test_list: &TestList<'_>) -> SetupScripts<'_> {
1156 SetupScripts::new(self, test_list)
1157 }
1158
1159 pub fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1161 ListSettings::new(self, query)
1162 }
1163
1164 pub fn settings_for(
1166 &self,
1167 run_mode: NextestRunMode,
1168 query: &TestQuery<'_>,
1169 ) -> TestSettings<'_> {
1170 TestSettings::new(self, run_mode, query)
1171 }
1172
1173 pub(crate) fn settings_with_source_for(
1175 &self,
1176 run_mode: NextestRunMode,
1177 query: &TestQuery<'_>,
1178 ) -> TestSettings<'_, SettingSource<'_>> {
1179 TestSettings::new(self, run_mode, query)
1180 }
1181
1182 pub fn junit(&self) -> Option<JunitConfig<'cfg>> {
1184 let settings = JunitSettings {
1185 path: profile_field_optional!(self.junit.path.as_deref()),
1186 report_name: profile_field_from_ref!(self.junit.report_name.as_deref()),
1187 store_success_output: profile_field!(self.junit.store_success_output),
1188 store_failure_output: profile_field!(self.junit.store_failure_output),
1189 flaky_fail_status: profile_field!(self.junit.flaky_fail_status),
1190 };
1191 JunitConfig::new(self.store_dir(), settings)
1192 }
1193
1194 pub fn inherits(&self) -> Option<&str> {
1196 if let Some(custom_profile) = self.custom_profile {
1197 return custom_profile.inherits();
1198 }
1199 None
1200 }
1201
1202 #[cfg(test)]
1203 pub(in crate::config) fn custom_profile(&self) -> Option<&'cfg CustomProfileImpl> {
1204 self.custom_profile
1205 }
1206}
1207
1208#[derive(Clone, Debug)]
1209pub(in crate::config) struct NextestConfigImpl {
1210 store: StoreConfigImpl,
1211 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1212 scripts: ScriptConfig,
1213 default_profile: DefaultProfileImpl,
1214 other_profiles: HashMap<String, CustomProfileImpl>,
1215}
1216
1217impl NextestConfigImpl {
1218 fn get_profile(&self, profile: &str) -> Result<Option<&CustomProfileImpl>, ProfileNotFound> {
1219 let custom_profile = match profile {
1220 NextestConfig::DEFAULT_PROFILE => None,
1221 other => Some(
1222 self.other_profiles
1223 .get(other)
1224 .ok_or_else(|| ProfileNotFound::new(profile, self.all_profiles()))?,
1225 ),
1226 };
1227 Ok(custom_profile)
1228 }
1229
1230 fn all_profiles(&self) -> impl Iterator<Item = &str> {
1231 self.other_profiles
1232 .keys()
1233 .map(|key| key.as_str())
1234 .chain(std::iter::once(NextestConfig::DEFAULT_PROFILE))
1235 }
1236
1237 pub(in crate::config) fn default_profile(&self) -> &DefaultProfileImpl {
1238 &self.default_profile
1239 }
1240
1241 pub(in crate::config) fn other_profiles(
1242 &self,
1243 ) -> impl Iterator<Item = (&str, &CustomProfileImpl)> {
1244 self.other_profiles
1245 .iter()
1246 .map(|(key, value)| (key.as_str(), value))
1247 }
1248
1249 fn resolve_inheritance_chain(
1255 &self,
1256 profile_name: &str,
1257 ) -> Result<Vec<&CustomProfileImpl>, ProfileNotFound> {
1258 let mut chain = Vec::new();
1259
1260 let mut curr = self
1263 .get_profile(profile_name)?
1264 .and_then(|p| p.inherits.as_deref());
1265
1266 while let Some(name) = curr {
1267 let profile = self.get_profile(name)?;
1268 if let Some(profile) = profile {
1269 chain.push(profile);
1270 curr = profile.inherits.as_deref();
1271 } else {
1272 break;
1274 }
1275 }
1276
1277 Ok(chain)
1278 }
1279
1280 fn sanitize_profile_inherits(
1285 &self,
1286 known_profiles: &BTreeSet<String>,
1287 ) -> Result<(), ConfigParseErrorKind> {
1288 let mut inherit_err_collector = Vec::new();
1289
1290 self.sanitize_default_profile_inherits(&mut inherit_err_collector);
1291 self.sanitize_custom_profile_inherits(&mut inherit_err_collector, known_profiles);
1292
1293 if !inherit_err_collector.is_empty() {
1294 return Err(ConfigParseErrorKind::InheritanceErrors(
1295 inherit_err_collector,
1296 ));
1297 }
1298
1299 Ok(())
1300 }
1301
1302 fn sanitize_default_profile_inherits(&self, inherit_err_collector: &mut Vec<InheritsError>) {
1305 if self.default_profile().inherits().is_some() {
1306 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(
1307 NextestConfig::DEFAULT_PROFILE.to_string(),
1308 ));
1309 }
1310 }
1311
1312 fn sanitize_custom_profile_inherits(
1314 &self,
1315 inherit_err_collector: &mut Vec<InheritsError>,
1316 known_profiles: &BTreeSet<String>,
1317 ) {
1318 let mut profile_graph = Graph::<&str, (), Directed>::new();
1319 let mut profile_map = HashMap::new();
1320
1321 for (name, custom_profile) in self.other_profiles() {
1324 let starts_with_default = self.sanitize_custom_default_profile_inherits(
1325 name,
1326 custom_profile,
1327 inherit_err_collector,
1328 );
1329 if !starts_with_default {
1330 self.add_profile_to_graph(
1335 name,
1336 custom_profile,
1337 &mut profile_map,
1338 &mut profile_graph,
1339 inherit_err_collector,
1340 known_profiles,
1341 );
1342 }
1343 }
1344
1345 self.check_inheritance_cycles(profile_graph, inherit_err_collector);
1346 }
1347
1348 fn sanitize_custom_default_profile_inherits(
1351 &self,
1352 name: &str,
1353 custom_profile: &CustomProfileImpl,
1354 inherit_err_collector: &mut Vec<InheritsError>,
1355 ) -> bool {
1356 let starts_with_default = name.starts_with("default-");
1357
1358 if starts_with_default && custom_profile.inherits().is_some() {
1359 inherit_err_collector.push(InheritsError::DefaultProfileInheritance(name.to_string()));
1360 }
1361
1362 starts_with_default
1363 }
1364
1365 fn add_profile_to_graph<'cfg>(
1370 &self,
1371 name: &'cfg str,
1372 custom_profile: &'cfg CustomProfileImpl,
1373 profile_map: &mut HashMap<&'cfg str, NodeIndex>,
1374 profile_graph: &mut Graph<&'cfg str, ()>,
1375 inherit_err_collector: &mut Vec<InheritsError>,
1376 known_profiles: &BTreeSet<String>,
1377 ) {
1378 if let Some(inherits_name) = custom_profile.inherits() {
1379 if inherits_name == name {
1380 inherit_err_collector
1381 .push(InheritsError::SelfReferentialInheritance(name.to_string()))
1382 } else if self.get_profile(inherits_name).is_ok() {
1383 let from_node = match profile_map.get(name) {
1385 None => {
1386 let profile_node = profile_graph.add_node(name);
1387 profile_map.insert(name, profile_node);
1388 profile_node
1389 }
1390 Some(node_idx) => *node_idx,
1391 };
1392 let to_node = match profile_map.get(inherits_name) {
1393 None => {
1394 let profile_node = profile_graph.add_node(inherits_name);
1395 profile_map.insert(inherits_name, profile_node);
1396 profile_node
1397 }
1398 Some(node_idx) => *node_idx,
1399 };
1400 profile_graph.add_edge(from_node, to_node, ());
1401 } else if known_profiles.contains(inherits_name) {
1402 } else {
1406 inherit_err_collector.push(InheritsError::UnknownInheritance(
1407 name.to_string(),
1408 inherits_name.to_string(),
1409 ))
1410 }
1411 }
1412 }
1413
1414 fn check_inheritance_cycles(
1416 &self,
1417 profile_graph: Graph<&str, ()>,
1418 inherit_err_collector: &mut Vec<InheritsError>,
1419 ) {
1420 let profile_sccs: Vec<Vec<NodeIndex>> = kosaraju_scc(&profile_graph);
1421 let profile_sccs: Vec<Vec<NodeIndex>> = profile_sccs
1422 .into_iter()
1423 .filter(|scc| scc.len() >= 2)
1424 .collect();
1425
1426 if !profile_sccs.is_empty() {
1427 inherit_err_collector.push(InheritsError::InheritanceCycle(
1428 profile_sccs
1429 .iter()
1430 .map(|node_idxs| {
1431 let profile_names: Vec<String> = node_idxs
1432 .iter()
1433 .map(|node_idx| profile_graph[*node_idx].to_string())
1434 .collect();
1435 profile_names
1436 })
1437 .collect(),
1438 ));
1439 }
1440 }
1441}
1442
1443#[derive(Clone, Debug, Deserialize)]
1445#[serde(rename_all = "kebab-case")]
1446struct NextestConfigDeserialize {
1447 store: StoreConfigImpl,
1448
1449 #[expect(unused)]
1452 #[serde(default)]
1453 nextest_version: Option<NextestVersionDeserialize>,
1454 #[expect(unused)]
1455 #[serde(default)]
1456 experimental: ExperimentalDeserialize,
1457
1458 #[serde(default)]
1459 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1460 #[serde(default, rename = "script")]
1462 old_setup_scripts: IndexMap<ScriptId, SetupScriptConfig>,
1463 #[serde(default)]
1464 scripts: ScriptConfig,
1465 #[serde(rename = "profile")]
1466 profiles: HashMap<String, CustomProfileImpl>,
1467}
1468
1469impl NextestConfigDeserialize {
1470 fn into_config_impl(mut self) -> NextestConfigImpl {
1471 let p = self
1472 .profiles
1473 .remove("default")
1474 .expect("default profile should exist");
1475 let default_profile = DefaultProfileImpl::new(p);
1476
1477 for (script_id, script_config) in self.old_setup_scripts {
1482 if let indexmap::map::Entry::Vacant(entry) = self.scripts.setup.entry(script_id) {
1483 entry.insert(script_config);
1484 }
1485 }
1486
1487 NextestConfigImpl {
1488 store: self.store,
1489 default_profile,
1490 test_groups: self.test_groups,
1491 scripts: self.scripts,
1492 other_profiles: self.profiles,
1493 }
1494 }
1495}
1496
1497#[derive(Clone, Debug, Deserialize)]
1498#[serde(rename_all = "kebab-case")]
1499struct StoreConfigImpl {
1500 dir: Utf8PathBuf,
1501}
1502
1503#[derive(Clone, Debug)]
1504pub(in crate::config) struct DefaultProfileImpl {
1505 default_filter: String,
1506 test_threads: TestThreads,
1507 threads_required: ThreadsRequired,
1508 run_extra_args: Vec<String>,
1509 retries: RetryPolicy,
1510 flaky_result: FlakyResult,
1511 status_level: StatusLevel,
1512 final_status_level: FinalStatusLevel,
1513 failure_output: TestOutputDisplay,
1514 success_output: TestOutputDisplay,
1515 max_fail: MaxFail,
1516 slow_timeout: SlowTimeout,
1517 global_timeout: GlobalTimeout,
1518 leak_timeout: LeakTimeout,
1519 overrides: Vec<DeserializedOverride>,
1520 scripts: Vec<DeserializedProfileScriptConfig>,
1521 junit: DefaultJunitImpl,
1522 archive: ArchiveConfig,
1523 bench: DefaultBenchConfig,
1524 inherits: Inherits,
1525}
1526
1527impl DefaultProfileImpl {
1528 fn new(p: CustomProfileImpl) -> Self {
1529 Self {
1530 default_filter: p
1531 .default_filter
1532 .expect("default-filter present in default profile"),
1533 test_threads: p
1534 .test_threads
1535 .expect("test-threads present in default profile"),
1536 threads_required: p
1537 .threads_required
1538 .expect("threads-required present in default profile"),
1539 run_extra_args: p
1540 .run_extra_args
1541 .expect("run-extra-args present in default profile"),
1542 retries: p.retries.expect("retries present in default profile"),
1543 flaky_result: p
1544 .flaky_result
1545 .expect("flaky-result present in default profile"),
1546 status_level: p
1547 .status_level
1548 .expect("status-level present in default profile"),
1549 final_status_level: p
1550 .final_status_level
1551 .expect("final-status-level present in default profile"),
1552 failure_output: p
1553 .failure_output
1554 .expect("failure-output present in default profile"),
1555 success_output: p
1556 .success_output
1557 .expect("success-output present in default profile"),
1558 max_fail: p.max_fail.expect("fail-fast present in default profile"),
1559 slow_timeout: p
1560 .slow_timeout
1561 .expect("slow-timeout present in default profile"),
1562 global_timeout: p
1563 .global_timeout
1564 .expect("global-timeout present in default profile"),
1565 leak_timeout: p
1566 .leak_timeout
1567 .expect("leak-timeout present in default profile"),
1568 overrides: p.overrides,
1569 scripts: p.scripts,
1570 junit: DefaultJunitImpl::for_default_profile(p.junit),
1571 archive: p.archive.expect("archive present in default profile"),
1572 bench: DefaultBenchConfig::for_default_profile(
1573 p.bench.expect("bench present in default profile"),
1574 ),
1575 inherits: Inherits::new(p.inherits),
1576 }
1577 }
1578
1579 pub(in crate::config) fn default_filter(&self) -> &str {
1580 &self.default_filter
1581 }
1582
1583 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1584 self.inherits.inherits_from()
1585 }
1586
1587 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1588 &self.overrides
1589 }
1590
1591 pub(in crate::config) fn setup_scripts(&self) -> &[DeserializedProfileScriptConfig] {
1592 &self.scripts
1593 }
1594
1595 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> SlowTimeout {
1596 match run_mode {
1597 NextestRunMode::Test => self.slow_timeout,
1598 NextestRunMode::Benchmark => self.bench.slow_timeout,
1599 }
1600 }
1601
1602 pub(in crate::config) fn global_timeout(&self, run_mode: NextestRunMode) -> GlobalTimeout {
1603 match run_mode {
1604 NextestRunMode::Test => self.global_timeout,
1605 NextestRunMode::Benchmark => self.bench.global_timeout,
1606 }
1607 }
1608}
1609
1610#[derive(Clone, Debug, Deserialize)]
1611#[serde(rename_all = "kebab-case")]
1612pub(in crate::config) struct CustomProfileImpl {
1613 #[serde(default)]
1615 default_filter: Option<String>,
1616 #[serde(default, deserialize_with = "deserialize_retry_policy")]
1617 retries: Option<RetryPolicy>,
1618 #[serde(default)]
1619 flaky_result: Option<FlakyResult>,
1620 #[serde(default)]
1621 test_threads: Option<TestThreads>,
1622 #[serde(default)]
1623 threads_required: Option<ThreadsRequired>,
1624 #[serde(default)]
1625 run_extra_args: Option<Vec<String>>,
1626 #[serde(default)]
1627 status_level: Option<StatusLevel>,
1628 #[serde(default)]
1629 final_status_level: Option<FinalStatusLevel>,
1630 #[serde(default)]
1631 failure_output: Option<TestOutputDisplay>,
1632 #[serde(default)]
1633 success_output: Option<TestOutputDisplay>,
1634 #[serde(
1635 default,
1636 rename = "fail-fast",
1637 deserialize_with = "deserialize_fail_fast"
1638 )]
1639 max_fail: Option<MaxFail>,
1640 #[serde(default, deserialize_with = "deserialize_slow_timeout")]
1641 slow_timeout: Option<SlowTimeout>,
1642 #[serde(default)]
1643 global_timeout: Option<GlobalTimeout>,
1644 #[serde(default, deserialize_with = "deserialize_leak_timeout")]
1645 leak_timeout: Option<LeakTimeout>,
1646 #[serde(default)]
1647 overrides: Vec<DeserializedOverride>,
1648 #[serde(default)]
1649 scripts: Vec<DeserializedProfileScriptConfig>,
1650 #[serde(default)]
1651 junit: JunitImpl,
1652 #[serde(default)]
1653 archive: Option<ArchiveConfig>,
1654 #[serde(default)]
1655 bench: Option<BenchConfig>,
1656 #[serde(default)]
1657 inherits: Option<String>,
1658}
1659
1660impl CustomProfileImpl {
1661 #[cfg(test)]
1662 pub(in crate::config) fn test_threads(&self) -> Option<TestThreads> {
1663 self.test_threads
1664 }
1665
1666 pub(in crate::config) fn default_filter(&self) -> Option<&str> {
1667 self.default_filter.as_deref()
1668 }
1669
1670 pub(in crate::config) fn slow_timeout(&self, run_mode: NextestRunMode) -> Option<SlowTimeout> {
1671 match run_mode {
1672 NextestRunMode::Test => self.slow_timeout,
1673 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.slow_timeout),
1674 }
1675 }
1676
1677 pub(in crate::config) fn global_timeout(
1678 &self,
1679 run_mode: NextestRunMode,
1680 ) -> Option<GlobalTimeout> {
1681 match run_mode {
1682 NextestRunMode::Test => self.global_timeout,
1683 NextestRunMode::Benchmark => self.bench.as_ref().and_then(|b| b.global_timeout),
1684 }
1685 }
1686
1687 pub(in crate::config) fn inherits(&self) -> Option<&str> {
1688 self.inherits.as_deref()
1689 }
1690
1691 pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] {
1692 &self.overrides
1693 }
1694
1695 pub(in crate::config) fn scripts(&self) -> &[DeserializedProfileScriptConfig] {
1696 &self.scripts
1697 }
1698}
1699
1700#[cfg(test)]
1701mod tests {
1702 use super::*;
1703 use crate::config::utils::test_helpers::*;
1704 use camino_tempfile::tempdir;
1705 use iddqd::{IdHashItem, IdHashMap, id_hash_map, id_upcast};
1706
1707 fn tool_name(s: &str) -> ToolName {
1708 ToolName::new(s.into()).unwrap()
1709 }
1710
1711 #[derive(Default)]
1713 struct TestConfigWarnings {
1714 unknown_keys: IdHashMap<UnknownKeys>,
1715 reserved_profiles: IdHashMap<ReservedProfiles>,
1716 deprecated_scripts: IdHashMap<DeprecatedScripts>,
1717 empty_script_warnings: IdHashMap<EmptyScriptSections>,
1718 }
1719
1720 impl ConfigWarnings for TestConfigWarnings {
1721 fn unknown_config_keys(
1722 &mut self,
1723 config_file: &Utf8Path,
1724 _workspace_root: &Utf8Path,
1725 tool: Option<&ToolName>,
1726 unknown: &BTreeSet<String>,
1727 ) {
1728 self.unknown_keys
1729 .insert_unique(UnknownKeys {
1730 tool: tool.cloned(),
1731 config_file: config_file.to_owned(),
1732 keys: unknown.clone(),
1733 })
1734 .unwrap();
1735 }
1736
1737 fn unknown_reserved_profiles(
1738 &mut self,
1739 config_file: &Utf8Path,
1740 _workspace_root: &Utf8Path,
1741 tool: Option<&ToolName>,
1742 profiles: &[&str],
1743 ) {
1744 self.reserved_profiles
1745 .insert_unique(ReservedProfiles {
1746 tool: tool.cloned(),
1747 config_file: config_file.to_owned(),
1748 profiles: profiles.iter().map(|&s| s.to_owned()).collect(),
1749 })
1750 .unwrap();
1751 }
1752
1753 fn empty_script_sections(
1754 &mut self,
1755 config_file: &Utf8Path,
1756 _workspace_root: &Utf8Path,
1757 tool: Option<&ToolName>,
1758 profile_name: &str,
1759 empty_count: usize,
1760 ) {
1761 self.empty_script_warnings
1762 .insert_unique(EmptyScriptSections {
1763 tool: tool.cloned(),
1764 config_file: config_file.to_owned(),
1765 profile_name: profile_name.to_owned(),
1766 empty_count,
1767 })
1768 .unwrap();
1769 }
1770
1771 fn deprecated_script_config(
1772 &mut self,
1773 config_file: &Utf8Path,
1774 _workspace_root: &Utf8Path,
1775 tool: Option<&ToolName>,
1776 ) {
1777 self.deprecated_scripts
1778 .insert_unique(DeprecatedScripts {
1779 tool: tool.cloned(),
1780 config_file: config_file.to_owned(),
1781 })
1782 .unwrap();
1783 }
1784 }
1785
1786 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1787 struct UnknownKeys {
1788 tool: Option<ToolName>,
1789 config_file: Utf8PathBuf,
1790 keys: BTreeSet<String>,
1791 }
1792
1793 impl IdHashItem for UnknownKeys {
1794 type Key<'a> = Option<&'a ToolName>;
1795 fn key(&self) -> Self::Key<'_> {
1796 self.tool.as_ref()
1797 }
1798 id_upcast!();
1799 }
1800
1801 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1802 struct ReservedProfiles {
1803 tool: Option<ToolName>,
1804 config_file: Utf8PathBuf,
1805 profiles: Vec<String>,
1806 }
1807
1808 impl IdHashItem for ReservedProfiles {
1809 type Key<'a> = Option<&'a ToolName>;
1810 fn key(&self) -> Self::Key<'_> {
1811 self.tool.as_ref()
1812 }
1813 id_upcast!();
1814 }
1815
1816 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1817 struct DeprecatedScripts {
1818 tool: Option<ToolName>,
1819 config_file: Utf8PathBuf,
1820 }
1821
1822 impl IdHashItem for DeprecatedScripts {
1823 type Key<'a> = Option<&'a ToolName>;
1824 fn key(&self) -> Self::Key<'_> {
1825 self.tool.as_ref()
1826 }
1827 id_upcast!();
1828 }
1829
1830 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1831 struct EmptyScriptSections {
1832 tool: Option<ToolName>,
1833 config_file: Utf8PathBuf,
1834 profile_name: String,
1835 empty_count: usize,
1836 }
1837
1838 impl IdHashItem for EmptyScriptSections {
1839 type Key<'a> = (&'a Option<ToolName>, &'a str);
1840 fn key(&self) -> Self::Key<'_> {
1841 (&self.tool, &self.profile_name)
1842 }
1843 id_upcast!();
1844 }
1845
1846 #[test]
1847 fn default_config_is_valid() {
1848 let default_config = NextestConfig::default_config("foo");
1849 default_config
1850 .profile(NextestConfig::DEFAULT_PROFILE)
1851 .expect("default profile should exist");
1852 }
1853
1854 #[test]
1855 fn ignored_keys() {
1856 let config_contents = r#"
1857 ignored1 = "test"
1858
1859 [profile.default]
1860 retries = 3
1861 ignored2 = "hi"
1862
1863 [profile.default-foo]
1864 retries = 5
1865
1866 [[profile.default.overrides]]
1867 filter = 'test(test_foo)'
1868 retries = 20
1869 ignored3 = 42
1870 "#;
1871
1872 let tool_config_contents = r#"
1873 [store]
1874 ignored4 = 20
1875
1876 [profile.default]
1877 retries = 4
1878 ignored5 = false
1879
1880 [profile.default-bar]
1881 retries = 5
1882
1883 [profile.tool]
1884 retries = 12
1885
1886 [[profile.tool.overrides]]
1887 filter = 'test(test_baz)'
1888 retries = 22
1889 ignored6 = 6.5
1890 "#;
1891
1892 let workspace_dir = tempdir().unwrap();
1893
1894 let graph = temp_workspace(&workspace_dir, config_contents);
1895 let workspace_root = graph.workspace().root();
1896 let tool_path = workspace_root.join(".config/tool.toml");
1897 std::fs::write(&tool_path, tool_config_contents).unwrap();
1898
1899 let pcx = ParseContext::new(&graph);
1900
1901 let mut warnings = TestConfigWarnings::default();
1902
1903 let _ = NextestConfig::from_sources_with_warnings(
1904 workspace_root,
1905 &pcx,
1906 None,
1907 &[ToolConfigFile {
1908 tool: tool_name("my-tool"),
1909 config_file: tool_path.clone(),
1910 }][..],
1911 &Default::default(),
1912 &mut warnings,
1913 )
1914 .expect("config is valid");
1915
1916 assert_eq!(
1917 warnings.unknown_keys.len(),
1918 2,
1919 "there are two files with unknown keys"
1920 );
1921
1922 assert_eq!(
1923 warnings.unknown_keys,
1924 id_hash_map! {
1925 UnknownKeys {
1926 tool: None,
1927 config_file: workspace_root.join(".config/nextest.toml"),
1928 keys: maplit::btreeset! {
1929 "ignored1".to_owned(),
1930 "profile.default.ignored2".to_owned(),
1931 "profile.default.overrides.0.ignored3".to_owned(),
1932 }
1933 },
1934 UnknownKeys {
1935 tool: Some(tool_name("my-tool")),
1936 config_file: tool_path.clone(),
1937 keys: maplit::btreeset! {
1938 "store.ignored4".to_owned(),
1939 "profile.default.ignored5".to_owned(),
1940 "profile.tool.overrides.0.ignored6".to_owned(),
1941 }
1942 }
1943 }
1944 );
1945 assert_eq!(
1946 warnings.reserved_profiles,
1947 id_hash_map! {
1948 ReservedProfiles {
1949 tool: None,
1950 config_file: workspace_root.join(".config/nextest.toml"),
1951 profiles: vec!["default-foo".to_owned()],
1952 },
1953 ReservedProfiles {
1954 tool: Some(tool_name("my-tool")),
1955 config_file: tool_path,
1956 profiles: vec!["default-bar".to_owned()],
1957 }
1958 },
1959 )
1960 }
1961
1962 #[test]
1963 fn script_warnings() {
1964 let config_contents = r#"
1965 experimental = ["setup-scripts", "wrapper-scripts"]
1966
1967 [scripts.wrapper.script1]
1968 command = "echo test"
1969
1970 [scripts.wrapper.script2]
1971 command = "echo test2"
1972
1973 [scripts.setup.script3]
1974 command = "echo setup"
1975
1976 [[profile.default.scripts]]
1977 filter = 'all()'
1978 # Empty - no setup or wrapper scripts
1979
1980 [[profile.default.scripts]]
1981 filter = 'test(foo)'
1982 setup = ["script3"]
1983
1984 [profile.custom]
1985 [[profile.custom.scripts]]
1986 filter = 'all()'
1987 # Empty - no setup or wrapper scripts
1988
1989 [[profile.custom.scripts]]
1990 filter = 'test(bar)'
1991 # Another empty section
1992 "#;
1993
1994 let tool_config_contents = r#"
1995 experimental = ["setup-scripts", "wrapper-scripts"]
1996
1997 [scripts.wrapper."@tool:tool:disabled_script"]
1998 command = "echo disabled"
1999
2000 [scripts.setup."@tool:tool:setup_script"]
2001 command = "echo setup"
2002
2003 [profile.tool]
2004 [[profile.tool.scripts]]
2005 filter = 'all()'
2006 # Empty section
2007
2008 [[profile.tool.scripts]]
2009 filter = 'test(foo)'
2010 setup = ["@tool:tool:setup_script"]
2011 "#;
2012
2013 let workspace_dir = tempdir().unwrap();
2014 let graph = temp_workspace(&workspace_dir, config_contents);
2015 let workspace_root = graph.workspace().root();
2016 let tool_path = workspace_root.join(".config/tool.toml");
2017 std::fs::write(&tool_path, tool_config_contents).unwrap();
2018
2019 let pcx = ParseContext::new(&graph);
2020
2021 let mut warnings = TestConfigWarnings::default();
2022
2023 let experimental = maplit::btreeset! {
2024 ConfigExperimental::SetupScripts,
2025 ConfigExperimental::WrapperScripts
2026 };
2027 let _ = NextestConfig::from_sources_with_warnings(
2028 workspace_root,
2029 &pcx,
2030 None,
2031 &[ToolConfigFile {
2032 tool: tool_name("tool"),
2033 config_file: tool_path.clone(),
2034 }][..],
2035 &experimental,
2036 &mut warnings,
2037 )
2038 .expect("config is valid");
2039
2040 assert_eq!(
2041 warnings.empty_script_warnings,
2042 id_hash_map! {
2043 EmptyScriptSections {
2044 tool: None,
2045 config_file: workspace_root.join(".config/nextest.toml"),
2046 profile_name: "default".to_owned(),
2047 empty_count: 1,
2048 },
2049 EmptyScriptSections {
2050 tool: None,
2051 config_file: workspace_root.join(".config/nextest.toml"),
2052 profile_name: "custom".to_owned(),
2053 empty_count: 2,
2054 },
2055 EmptyScriptSections {
2056 tool: Some(tool_name("tool")),
2057 config_file: tool_path,
2058 profile_name: "tool".to_owned(),
2059 empty_count: 1,
2060 }
2061 }
2062 );
2063 }
2064
2065 #[test]
2066 fn deprecated_script_config_warning() {
2067 let config_contents = r#"
2068 experimental = ["setup-scripts"]
2069
2070 [script.my-script]
2071 command = "echo hello"
2072"#;
2073
2074 let tool_config_contents = r#"
2075 experimental = ["setup-scripts"]
2076
2077 [script."@tool:my-tool:my-script"]
2078 command = "echo hello"
2079"#;
2080
2081 let temp_dir = tempdir().unwrap();
2082
2083 let graph = temp_workspace(&temp_dir, config_contents);
2084 let workspace_root = graph.workspace().root();
2085 let tool_path = workspace_root.join(".config/my-tool.toml");
2086 std::fs::write(&tool_path, tool_config_contents).unwrap();
2087 let pcx = ParseContext::new(&graph);
2088
2089 let mut warnings = TestConfigWarnings::default();
2090 NextestConfig::from_sources_with_warnings(
2091 graph.workspace().root(),
2092 &pcx,
2093 None,
2094 &[ToolConfigFile {
2095 tool: tool_name("my-tool"),
2096 config_file: tool_path.clone(),
2097 }],
2098 &maplit::btreeset! {ConfigExperimental::SetupScripts},
2099 &mut warnings,
2100 )
2101 .expect("config is valid");
2102
2103 assert_eq!(
2104 warnings.deprecated_scripts,
2105 id_hash_map! {
2106 DeprecatedScripts {
2107 tool: None,
2108 config_file: graph.workspace().root().join(".config/nextest.toml"),
2109 },
2110 DeprecatedScripts {
2111 tool: Some(tool_name("my-tool")),
2112 config_file: tool_path,
2113 }
2114 }
2115 );
2116 }
2117}