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