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