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