1use super::{
5 ArchiveConfig, CompiledByProfile, CompiledData, CompiledDefaultFilter, ConfigExperimental,
6 CustomTestGroup, DefaultJunitImpl, DeserializedOverride, DeserializedProfileScriptConfig,
7 JunitConfig, JunitImpl, MaxFail, NextestVersionDeserialize, RetryPolicy, ScriptConfig,
8 ScriptId, SettingSource, SetupScripts, SlowTimeout, TestGroup, TestGroupConfig, TestSettings,
9 TestThreads, ThreadsRequired, ToolConfigFile, leak_timeout::LeakTimeout,
10};
11use crate::{
12 config::{ListSettings, ProfileScriptType, ScriptInfo, SetupScriptConfig},
13 errors::{
14 ConfigParseError, ConfigParseErrorKind, ProfileListScriptUsesRunFiltersError,
15 ProfileNotFound, ProfileScriptErrors, ProfileUnknownScriptError,
16 ProfileWrongConfigScriptTypeError, UnknownTestGroupError, provided_by_tool,
17 },
18 helpers::plural,
19 list::TestList,
20 platform::BuildPlatforms,
21 reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplay},
22};
23use camino::{Utf8Path, Utf8PathBuf};
24use config::{
25 Config, ConfigBuilder, ConfigError, File, FileFormat, FileSourceFile, builder::DefaultState,
26};
27use iddqd::IdOrdMap;
28use indexmap::IndexMap;
29use nextest_filtering::{BinaryQuery, EvalContext, Filterset, ParseContext, TestQuery};
30use serde::Deserialize;
31use std::{
32 collections::{BTreeMap, BTreeSet, HashMap, hash_map},
33 sync::LazyLock,
34};
35use tracing::warn;
36
37pub trait ConfigWarnings {
42 fn unknown_config_keys(
44 &mut self,
45 config_file: &Utf8Path,
46 workspace_root: &Utf8Path,
47 tool: Option<&str>,
48 unknown: &BTreeSet<String>,
49 );
50
51 fn unknown_reserved_profiles(
53 &mut self,
54 config_file: &Utf8Path,
55 workspace_root: &Utf8Path,
56 tool: Option<&str>,
57 profiles: &[&str],
58 );
59
60 fn deprecated_script_config(
62 &mut self,
63 config_file: &Utf8Path,
64 workspace_root: &Utf8Path,
65 tool: Option<&str>,
66 );
67
68 fn empty_script_sections(
71 &mut self,
72 config_file: &Utf8Path,
73 workspace_root: &Utf8Path,
74 tool: Option<&str>,
75 profile_name: &str,
76 empty_count: usize,
77 );
78}
79
80pub struct DefaultConfigWarnings;
82
83impl ConfigWarnings for DefaultConfigWarnings {
84 fn unknown_config_keys(
85 &mut self,
86 config_file: &Utf8Path,
87 workspace_root: &Utf8Path,
88 tool: Option<&str>,
89 unknown: &BTreeSet<String>,
90 ) {
91 let mut unknown_str = String::new();
92 if unknown.len() == 1 {
93 unknown_str.push(' ');
95 unknown_str.push_str(unknown.iter().next().unwrap());
96 } else {
97 for ignored_key in unknown {
98 unknown_str.push('\n');
99 unknown_str.push_str(" - ");
100 unknown_str.push_str(ignored_key);
101 }
102 }
103
104 warn!(
105 "in config file {}{}, ignoring unknown configuration keys: {unknown_str}",
106 config_file
107 .strip_prefix(workspace_root)
108 .unwrap_or(config_file),
109 provided_by_tool(tool),
110 )
111 }
112
113 fn unknown_reserved_profiles(
114 &mut self,
115 config_file: &Utf8Path,
116 workspace_root: &Utf8Path,
117 tool: Option<&str>,
118 profiles: &[&str],
119 ) {
120 warn!(
121 "in config file {}{}, ignoring unknown profiles in the reserved `default-` namespace:",
122 config_file
123 .strip_prefix(workspace_root)
124 .unwrap_or(config_file),
125 provided_by_tool(tool),
126 );
127
128 for profile in profiles {
129 warn!(" {profile}");
130 }
131 }
132
133 fn deprecated_script_config(
134 &mut self,
135 config_file: &Utf8Path,
136 workspace_root: &Utf8Path,
137 tool: Option<&str>,
138 ) {
139 warn!(
140 "in config file {}{}, [script.*] is deprecated and will be removed in a \
141 future version of nextest; use the `scripts.setup` table instead",
142 config_file
143 .strip_prefix(workspace_root)
144 .unwrap_or(config_file),
145 provided_by_tool(tool),
146 );
147 }
148
149 fn empty_script_sections(
150 &mut self,
151 config_file: &Utf8Path,
152 workspace_root: &Utf8Path,
153 tool: Option<&str>,
154 profile_name: &str,
155 empty_count: usize,
156 ) {
157 warn!(
158 "in config file {}{}, [[profile.{}.scripts]] has {} {} \
159 with neither setup nor wrapper scripts",
160 config_file
161 .strip_prefix(workspace_root)
162 .unwrap_or(config_file),
163 provided_by_tool(tool),
164 profile_name,
165 empty_count,
166 plural::sections_str(empty_count),
167 );
168 }
169}
170
171#[inline]
173pub fn get_num_cpus() -> usize {
174 static NUM_CPUS: LazyLock<usize> =
175 LazyLock::new(|| match std::thread::available_parallelism() {
176 Ok(count) => count.into(),
177 Err(err) => {
178 warn!("unable to determine num-cpus ({err}), assuming 1 logical CPU");
179 1
180 }
181 });
182
183 *NUM_CPUS
184}
185
186#[derive(Clone, Debug)]
195pub struct NextestConfig {
196 workspace_root: Utf8PathBuf,
197 inner: NextestConfigImpl,
198 compiled: CompiledByProfile,
199}
200
201impl NextestConfig {
202 pub const CONFIG_PATH: &'static str = ".config/nextest.toml";
205
206 pub const DEFAULT_CONFIG: &'static str = include_str!("../../default-config.toml");
210
211 pub const ENVIRONMENT_PREFIX: &'static str = "NEXTEST";
213
214 pub const DEFAULT_PROFILE: &'static str = "default";
216
217 pub const DEFAULT_MIRI_PROFILE: &'static str = "default-miri";
219
220 pub const DEFAULT_PROFILES: &'static [&'static str] =
222 &[Self::DEFAULT_PROFILE, Self::DEFAULT_MIRI_PROFILE];
223
224 pub fn from_sources<'a, I>(
234 workspace_root: impl Into<Utf8PathBuf>,
235 pcx: &ParseContext<'_>,
236 config_file: Option<&Utf8Path>,
237 tool_config_files: impl IntoIterator<IntoIter = I>,
238 experimental: &BTreeSet<ConfigExperimental>,
239 ) -> Result<Self, ConfigParseError>
240 where
241 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
242 {
243 Self::from_sources_with_warnings(
244 workspace_root,
245 pcx,
246 config_file,
247 tool_config_files,
248 experimental,
249 &mut DefaultConfigWarnings,
250 )
251 }
252
253 pub fn from_sources_with_warnings<'a, I>(
255 workspace_root: impl Into<Utf8PathBuf>,
256 pcx: &ParseContext<'_>,
257 config_file: Option<&Utf8Path>,
258 tool_config_files: impl IntoIterator<IntoIter = I>,
259 experimental: &BTreeSet<ConfigExperimental>,
260 warnings: &mut impl ConfigWarnings,
261 ) -> Result<Self, ConfigParseError>
262 where
263 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
264 {
265 Self::from_sources_impl(
266 workspace_root,
267 pcx,
268 config_file,
269 tool_config_files,
270 experimental,
271 warnings,
272 )
273 }
274
275 fn from_sources_impl<'a, I>(
277 workspace_root: impl Into<Utf8PathBuf>,
278 pcx: &ParseContext<'_>,
279 config_file: Option<&Utf8Path>,
280 tool_config_files: impl IntoIterator<IntoIter = I>,
281 experimental: &BTreeSet<ConfigExperimental>,
282 warnings: &mut impl ConfigWarnings,
283 ) -> Result<Self, ConfigParseError>
284 where
285 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
286 {
287 let workspace_root = workspace_root.into();
288 let tool_config_files_rev = tool_config_files.into_iter().rev();
289 let (inner, compiled) = Self::read_from_sources(
290 pcx,
291 &workspace_root,
292 config_file,
293 tool_config_files_rev,
294 experimental,
295 warnings,
296 )?;
297 Ok(Self {
298 workspace_root,
299 inner,
300 compiled,
301 })
302 }
303
304 #[cfg(test)]
306 pub(crate) fn default_config(workspace_root: impl Into<Utf8PathBuf>) -> Self {
307 use itertools::Itertools;
308
309 let config = Self::make_default_config()
310 .build()
311 .expect("default config is always valid");
312
313 let mut unknown = BTreeSet::new();
314 let deserialized: NextestConfigDeserialize =
315 serde_ignored::deserialize(config, |path: serde_ignored::Path| {
316 unknown.insert(path.to_string());
317 })
318 .expect("default config is always valid");
319
320 if !unknown.is_empty() {
323 panic!(
324 "found unknown keys in default config: {}",
325 unknown.iter().join(", ")
326 );
327 }
328
329 Self {
330 workspace_root: workspace_root.into(),
331 inner: deserialized.into_config_impl(),
332 compiled: CompiledByProfile::for_default_config(),
334 }
335 }
336
337 pub fn profile(&self, name: impl AsRef<str>) -> Result<EarlyProfile<'_>, ProfileNotFound> {
340 self.make_profile(name.as_ref())
341 }
342
343 fn read_from_sources<'a>(
348 pcx: &ParseContext<'_>,
349 workspace_root: &Utf8Path,
350 file: Option<&Utf8Path>,
351 tool_config_files_rev: impl Iterator<Item = &'a ToolConfigFile>,
352 experimental: &BTreeSet<ConfigExperimental>,
353 warnings: &mut impl ConfigWarnings,
354 ) -> Result<(NextestConfigImpl, CompiledByProfile), ConfigParseError> {
355 let mut composite_builder = Self::make_default_config();
357
358 let mut compiled = CompiledByProfile::for_default_config();
361
362 let mut known_groups = BTreeSet::new();
363 let mut known_scripts = IdOrdMap::new();
364
365 for ToolConfigFile { config_file, tool } in tool_config_files_rev {
367 let source = File::new(config_file.as_str(), FileFormat::Toml);
368 Self::deserialize_individual_config(
369 pcx,
370 workspace_root,
371 config_file,
372 Some(tool),
373 source.clone(),
374 &mut compiled,
375 experimental,
376 warnings,
377 &mut known_groups,
378 &mut known_scripts,
379 )?;
380
381 composite_builder = composite_builder.add_source(source);
383 }
384
385 let (config_file, source) = match file {
387 Some(file) => (file.to_owned(), File::new(file.as_str(), FileFormat::Toml)),
388 None => {
389 let config_file = workspace_root.join(Self::CONFIG_PATH);
390 let source = File::new(config_file.as_str(), FileFormat::Toml).required(false);
391 (config_file, source)
392 }
393 };
394
395 Self::deserialize_individual_config(
396 pcx,
397 workspace_root,
398 &config_file,
399 None,
400 source.clone(),
401 &mut compiled,
402 experimental,
403 warnings,
404 &mut known_groups,
405 &mut known_scripts,
406 )?;
407
408 composite_builder = composite_builder.add_source(source);
409
410 let (config, _unknown) = Self::build_and_deserialize_config(&composite_builder)
413 .map_err(|kind| ConfigParseError::new(config_file, None, kind))?;
414
415 compiled.default.reverse();
417 for data in compiled.other.values_mut() {
418 data.reverse();
419 }
420
421 Ok((config.into_config_impl(), compiled))
422 }
423
424 #[expect(clippy::too_many_arguments)]
425 fn deserialize_individual_config(
426 pcx: &ParseContext<'_>,
427 workspace_root: &Utf8Path,
428 config_file: &Utf8Path,
429 tool: Option<&str>,
430 source: File<FileSourceFile, FileFormat>,
431 compiled_out: &mut CompiledByProfile,
432 experimental: &BTreeSet<ConfigExperimental>,
433 warnings: &mut impl ConfigWarnings,
434 known_groups: &mut BTreeSet<CustomTestGroup>,
435 known_scripts: &mut IdOrdMap<ScriptInfo>,
436 ) -> Result<(), ConfigParseError> {
437 let default_builder = Self::make_default_config();
440 let this_builder = default_builder.add_source(source);
441 let (mut this_config, unknown) = Self::build_and_deserialize_config(&this_builder)
442 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
443
444 if !unknown.is_empty() {
445 warnings.unknown_config_keys(config_file, workspace_root, tool, &unknown);
446 }
447
448 let (valid_groups, invalid_groups): (BTreeSet<_>, _) =
450 this_config.test_groups.keys().cloned().partition(|group| {
451 if let Some(tool) = tool {
452 group
454 .as_identifier()
455 .tool_components()
456 .is_some_and(|(tool_name, _)| tool_name == tool)
457 } else {
458 !group.as_identifier().is_tool_identifier()
460 }
461 });
462
463 if !invalid_groups.is_empty() {
464 let kind = if tool.is_some() {
465 ConfigParseErrorKind::InvalidTestGroupsDefinedByTool(invalid_groups)
466 } else {
467 ConfigParseErrorKind::InvalidTestGroupsDefined(invalid_groups)
468 };
469 return Err(ConfigParseError::new(config_file, tool, kind));
470 }
471
472 known_groups.extend(valid_groups);
473
474 if !this_config.scripts.is_empty() && !this_config.old_setup_scripts.is_empty() {
476 return Err(ConfigParseError::new(
477 config_file,
478 tool,
479 ConfigParseErrorKind::BothScriptAndScriptsSetupDefined,
480 ));
481 }
482
483 if !this_config.old_setup_scripts.is_empty() {
485 warnings.deprecated_script_config(config_file, workspace_root, tool);
486 this_config.scripts.setup = this_config.old_setup_scripts.clone();
487 }
488
489 {
491 let mut missing_features = BTreeSet::new();
492 if !this_config.scripts.setup.is_empty()
493 && !experimental.contains(&ConfigExperimental::SetupScripts)
494 {
495 missing_features.insert(ConfigExperimental::SetupScripts);
496 }
497 if !this_config.scripts.wrapper.is_empty()
498 && !experimental.contains(&ConfigExperimental::WrapperScripts)
499 {
500 missing_features.insert(ConfigExperimental::WrapperScripts);
501 }
502 if !missing_features.is_empty() {
503 return Err(ConfigParseError::new(
504 config_file,
505 tool,
506 ConfigParseErrorKind::ExperimentalFeaturesNotEnabled { missing_features },
507 ));
508 }
509 }
510
511 let duplicate_ids: BTreeSet<_> = this_config.scripts.duplicate_ids().cloned().collect();
512 if !duplicate_ids.is_empty() {
513 return Err(ConfigParseError::new(
514 config_file,
515 tool,
516 ConfigParseErrorKind::DuplicateConfigScriptNames(duplicate_ids),
517 ));
518 }
519
520 let (valid_scripts, invalid_scripts): (BTreeSet<_>, _) = this_config
522 .scripts
523 .all_script_ids()
524 .cloned()
525 .partition(|script| {
526 if let Some(tool) = tool {
527 script
529 .as_identifier()
530 .tool_components()
531 .is_some_and(|(tool_name, _)| tool_name == tool)
532 } else {
533 !script.as_identifier().is_tool_identifier()
535 }
536 });
537
538 if !invalid_scripts.is_empty() {
539 let kind = if tool.is_some() {
540 ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool(invalid_scripts)
541 } else {
542 ConfigParseErrorKind::InvalidConfigScriptsDefined(invalid_scripts)
543 };
544 return Err(ConfigParseError::new(config_file, tool, kind));
545 }
546
547 known_scripts.extend(
548 valid_scripts
549 .into_iter()
550 .map(|id| this_config.scripts.script_info(id)),
551 );
552
553 let this_config = this_config.into_config_impl();
554
555 let unknown_default_profiles: Vec<_> = this_config
556 .all_profiles()
557 .filter(|p| p.starts_with("default-") && !NextestConfig::DEFAULT_PROFILES.contains(p))
558 .collect();
559 if !unknown_default_profiles.is_empty() {
560 warnings.unknown_reserved_profiles(
561 config_file,
562 workspace_root,
563 tool,
564 &unknown_default_profiles,
565 );
566 }
567
568 let this_compiled = CompiledByProfile::new(pcx, &this_config)
570 .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
571
572 let mut unknown_group_errors = Vec::new();
574 let mut check_test_group = |profile_name: &str, test_group: Option<&TestGroup>| {
575 if let Some(TestGroup::Custom(group)) = test_group {
576 if !known_groups.contains(group) {
577 unknown_group_errors.push(UnknownTestGroupError {
578 profile_name: profile_name.to_owned(),
579 name: TestGroup::Custom(group.clone()),
580 });
581 }
582 }
583 };
584
585 this_compiled
586 .default
587 .overrides
588 .iter()
589 .for_each(|override_| {
590 check_test_group("default", override_.data.test_group.as_ref());
591 });
592
593 this_compiled.other.iter().for_each(|(profile_name, data)| {
595 data.overrides.iter().for_each(|override_| {
596 check_test_group(profile_name, override_.data.test_group.as_ref());
597 });
598 });
599
600 if !unknown_group_errors.is_empty() {
602 let known_groups = TestGroup::make_all_groups(known_groups.iter().cloned()).collect();
603 return Err(ConfigParseError::new(
604 config_file,
605 tool,
606 ConfigParseErrorKind::UnknownTestGroups {
607 errors: unknown_group_errors,
608 known_groups,
609 },
610 ));
611 }
612
613 let mut profile_script_errors = ProfileScriptErrors::default();
616 let mut check_script_ids = |profile_name: &str,
617 script_type: ProfileScriptType,
618 expr: Option<&Filterset>,
619 scripts: &[ScriptId]| {
620 for script in scripts {
621 if let Some(script_info) = known_scripts.get(script) {
622 if !script_info.script_type.matches(script_type) {
623 profile_script_errors.wrong_script_types.push(
624 ProfileWrongConfigScriptTypeError {
625 profile_name: profile_name.to_owned(),
626 name: script.clone(),
627 attempted: script_type,
628 actual: script_info.script_type,
629 },
630 );
631 }
632 if script_type == ProfileScriptType::ListWrapper {
633 if let Some(expr) = expr {
634 let runtime_only_leaves = expr.parsed.runtime_only_leaves();
635 if !runtime_only_leaves.is_empty() {
636 let filters = runtime_only_leaves
637 .iter()
638 .map(|leaf| leaf.to_string())
639 .collect();
640 profile_script_errors.list_scripts_using_run_filters.push(
641 ProfileListScriptUsesRunFiltersError {
642 profile_name: profile_name.to_owned(),
643 name: script.clone(),
644 script_type,
645 filters,
646 },
647 );
648 }
649 }
650 }
651 } else {
652 profile_script_errors
653 .unknown_scripts
654 .push(ProfileUnknownScriptError {
655 profile_name: profile_name.to_owned(),
656 name: script.clone(),
657 });
658 }
659 }
660 };
661
662 let mut empty_script_count = 0;
663
664 this_compiled.default.scripts.iter().for_each(|scripts| {
665 if scripts.setup.is_empty()
666 && scripts.list_wrapper.is_none()
667 && scripts.run_wrapper.is_none()
668 {
669 empty_script_count += 1;
670 }
671
672 check_script_ids(
673 "default",
674 ProfileScriptType::Setup,
675 scripts.data.expr(),
676 &scripts.setup,
677 );
678 check_script_ids(
679 "default",
680 ProfileScriptType::ListWrapper,
681 scripts.data.expr(),
682 scripts.list_wrapper.as_slice(),
683 );
684 check_script_ids(
685 "default",
686 ProfileScriptType::RunWrapper,
687 scripts.data.expr(),
688 scripts.run_wrapper.as_slice(),
689 );
690 });
691
692 if empty_script_count > 0 {
693 warnings.empty_script_sections(
694 config_file,
695 workspace_root,
696 tool,
697 "default",
698 empty_script_count,
699 );
700 }
701
702 this_compiled.other.iter().for_each(|(profile_name, data)| {
703 let mut empty_script_count = 0;
704 data.scripts.iter().for_each(|scripts| {
705 if scripts.setup.is_empty()
706 && scripts.list_wrapper.is_none()
707 && scripts.run_wrapper.is_none()
708 {
709 empty_script_count += 1;
710 }
711
712 check_script_ids(
713 profile_name,
714 ProfileScriptType::Setup,
715 scripts.data.expr(),
716 &scripts.setup,
717 );
718 check_script_ids(
719 profile_name,
720 ProfileScriptType::ListWrapper,
721 scripts.data.expr(),
722 scripts.list_wrapper.as_slice(),
723 );
724 check_script_ids(
725 profile_name,
726 ProfileScriptType::RunWrapper,
727 scripts.data.expr(),
728 scripts.run_wrapper.as_slice(),
729 );
730 });
731
732 if empty_script_count > 0 {
733 warnings.empty_script_sections(
734 config_file,
735 workspace_root,
736 tool,
737 profile_name,
738 empty_script_count,
739 );
740 }
741 });
742
743 if !profile_script_errors.is_empty() {
746 let known_scripts = known_scripts
747 .iter()
748 .map(|script| script.id.clone())
749 .collect();
750 return Err(ConfigParseError::new(
751 config_file,
752 tool,
753 ConfigParseErrorKind::ProfileScriptErrors {
754 errors: Box::new(profile_script_errors),
755 known_scripts,
756 },
757 ));
758 }
759
760 compiled_out.default.extend_reverse(this_compiled.default);
763 for (name, mut data) in this_compiled.other {
764 match compiled_out.other.entry(name) {
765 hash_map::Entry::Vacant(entry) => {
766 data.reverse();
768 entry.insert(data);
769 }
770 hash_map::Entry::Occupied(mut entry) => {
771 entry.get_mut().extend_reverse(data);
773 }
774 }
775 }
776
777 Ok(())
778 }
779
780 fn make_default_config() -> ConfigBuilder<DefaultState> {
781 Config::builder().add_source(File::from_str(Self::DEFAULT_CONFIG, FileFormat::Toml))
782 }
783
784 fn make_profile(&self, name: &str) -> Result<EarlyProfile<'_>, ProfileNotFound> {
785 let custom_profile = self.inner.get_profile(name)?;
786
787 let mut store_dir = self.workspace_root.join(&self.inner.store.dir);
789 store_dir.push(name);
790
791 let compiled_data = match self.compiled.other.get(name) {
793 Some(data) => data.clone().chain(self.compiled.default.clone()),
794 None => self.compiled.default.clone(),
795 };
796
797 Ok(EarlyProfile {
798 name: name.to_owned(),
799 store_dir,
800 default_profile: &self.inner.default_profile,
801 custom_profile,
802 test_groups: &self.inner.test_groups,
803 scripts: &self.inner.scripts,
804 compiled_data,
805 })
806 }
807
808 fn build_and_deserialize_config(
810 builder: &ConfigBuilder<DefaultState>,
811 ) -> Result<(NextestConfigDeserialize, BTreeSet<String>), ConfigParseErrorKind> {
812 let config = builder
813 .build_cloned()
814 .map_err(|error| ConfigParseErrorKind::BuildError(Box::new(error)))?;
815
816 let mut ignored = BTreeSet::new();
817 let mut cb = |path: serde_ignored::Path| {
818 ignored.insert(path.to_string());
819 };
820 let ignored_de = serde_ignored::Deserializer::new(config, &mut cb);
821 let config: NextestConfigDeserialize = serde_path_to_error::deserialize(ignored_de)
822 .map_err(|error| {
823 let path = error.path().clone();
827 let config_error = error.into_inner();
828 let error = match config_error {
829 ConfigError::At { error, .. } => *error,
830 other => other,
831 };
832 ConfigParseErrorKind::DeserializeError(Box::new(serde_path_to_error::Error::new(
833 path, error,
834 )))
835 })?;
836
837 Ok((config, ignored))
838 }
839}
840
841#[derive(Clone, Debug, Default)]
843pub(super) struct PreBuildPlatform {}
844
845#[derive(Clone, Debug)]
847pub(crate) struct FinalConfig {
848 pub(super) host_eval: bool,
850 pub(super) host_test_eval: bool,
853 pub(super) target_eval: bool,
856}
857
858pub struct EarlyProfile<'cfg> {
863 name: String,
864 store_dir: Utf8PathBuf,
865 default_profile: &'cfg DefaultProfileImpl,
866 custom_profile: Option<&'cfg CustomProfileImpl>,
867 test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
868 scripts: &'cfg ScriptConfig,
870 pub(super) compiled_data: CompiledData<PreBuildPlatform>,
872}
873
874impl<'cfg> EarlyProfile<'cfg> {
875 pub fn store_dir(&self) -> &Utf8Path {
877 &self.store_dir
878 }
879
880 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
882 self.test_groups
883 }
884
885 pub fn apply_build_platforms(
890 self,
891 build_platforms: &BuildPlatforms,
892 ) -> EvaluatableProfile<'cfg> {
893 let compiled_data = self.compiled_data.apply_build_platforms(build_platforms);
894
895 let resolved_default_filter = {
896 let found_filter = compiled_data
898 .overrides
899 .iter()
900 .find_map(|override_data| override_data.default_filter_if_matches_platform());
901 found_filter.unwrap_or_else(|| {
902 compiled_data
905 .profile_default_filter
906 .as_ref()
907 .expect("compiled data always has default set")
908 })
909 }
910 .clone();
911
912 EvaluatableProfile {
913 name: self.name,
914 store_dir: self.store_dir,
915 default_profile: self.default_profile,
916 custom_profile: self.custom_profile,
917 scripts: self.scripts,
918 test_groups: self.test_groups,
919 compiled_data,
920 resolved_default_filter,
921 }
922 }
923}
924
925#[derive(Clone, Debug)]
929pub struct EvaluatableProfile<'cfg> {
930 name: String,
931 store_dir: Utf8PathBuf,
932 default_profile: &'cfg DefaultProfileImpl,
933 custom_profile: Option<&'cfg CustomProfileImpl>,
934 test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
935 scripts: &'cfg ScriptConfig,
937 pub(super) compiled_data: CompiledData<FinalConfig>,
939 resolved_default_filter: CompiledDefaultFilter,
942}
943
944impl<'cfg> EvaluatableProfile<'cfg> {
945 pub fn name(&self) -> &str {
947 &self.name
948 }
949
950 pub fn store_dir(&self) -> &Utf8Path {
952 &self.store_dir
953 }
954
955 pub fn filterset_ecx(&self) -> EvalContext<'_> {
957 EvalContext {
958 default_filter: &self.default_filter().expr,
959 }
960 }
961
962 pub fn default_filter(&self) -> &CompiledDefaultFilter {
964 &self.resolved_default_filter
965 }
966
967 pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
969 self.test_groups
970 }
971
972 pub fn script_config(&self) -> &'cfg ScriptConfig {
974 self.scripts
975 }
976
977 pub fn retries(&self) -> RetryPolicy {
979 self.custom_profile
980 .and_then(|profile| profile.retries)
981 .unwrap_or(self.default_profile.retries)
982 }
983
984 pub fn test_threads(&self) -> TestThreads {
986 self.custom_profile
987 .and_then(|profile| profile.test_threads)
988 .unwrap_or(self.default_profile.test_threads)
989 }
990
991 pub fn threads_required(&self) -> ThreadsRequired {
993 self.custom_profile
994 .and_then(|profile| profile.threads_required)
995 .unwrap_or(self.default_profile.threads_required)
996 }
997
998 pub fn run_extra_args(&self) -> &'cfg [String] {
1000 self.custom_profile
1001 .and_then(|profile| profile.run_extra_args.as_deref())
1002 .unwrap_or(&self.default_profile.run_extra_args)
1003 }
1004
1005 pub fn slow_timeout(&self) -> SlowTimeout {
1007 self.custom_profile
1008 .and_then(|profile| profile.slow_timeout)
1009 .unwrap_or(self.default_profile.slow_timeout)
1010 }
1011
1012 pub fn leak_timeout(&self) -> LeakTimeout {
1015 self.custom_profile
1016 .and_then(|profile| profile.leak_timeout)
1017 .unwrap_or(self.default_profile.leak_timeout)
1018 }
1019
1020 pub fn status_level(&self) -> StatusLevel {
1022 self.custom_profile
1023 .and_then(|profile| profile.status_level)
1024 .unwrap_or(self.default_profile.status_level)
1025 }
1026
1027 pub fn final_status_level(&self) -> FinalStatusLevel {
1029 self.custom_profile
1030 .and_then(|profile| profile.final_status_level)
1031 .unwrap_or(self.default_profile.final_status_level)
1032 }
1033
1034 pub fn failure_output(&self) -> TestOutputDisplay {
1036 self.custom_profile
1037 .and_then(|profile| profile.failure_output)
1038 .unwrap_or(self.default_profile.failure_output)
1039 }
1040
1041 pub fn success_output(&self) -> TestOutputDisplay {
1043 self.custom_profile
1044 .and_then(|profile| profile.success_output)
1045 .unwrap_or(self.default_profile.success_output)
1046 }
1047
1048 pub fn max_fail(&self) -> MaxFail {
1050 self.custom_profile
1051 .and_then(|profile| profile.max_fail)
1052 .unwrap_or(self.default_profile.max_fail)
1053 }
1054
1055 pub fn archive_config(&self) -> &'cfg ArchiveConfig {
1057 self.custom_profile
1058 .and_then(|profile| profile.archive.as_ref())
1059 .unwrap_or(&self.default_profile.archive)
1060 }
1061
1062 pub fn setup_scripts(&self, test_list: &TestList<'_>) -> SetupScripts<'_> {
1064 SetupScripts::new(self, test_list)
1065 }
1066
1067 pub fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1069 ListSettings::new(self, query)
1070 }
1071
1072 pub fn settings_for(&self, query: &TestQuery<'_>) -> TestSettings {
1074 TestSettings::new(self, query)
1075 }
1076
1077 pub(crate) fn settings_with_source_for(
1079 &self,
1080 query: &TestQuery<'_>,
1081 ) -> TestSettings<SettingSource<'_>> {
1082 TestSettings::new(self, query)
1083 }
1084
1085 pub fn junit(&self) -> Option<JunitConfig<'cfg>> {
1087 JunitConfig::new(
1088 self.store_dir(),
1089 self.custom_profile.map(|p| &p.junit),
1090 &self.default_profile.junit,
1091 )
1092 }
1093
1094 #[cfg(test)]
1095 pub(super) fn custom_profile(&self) -> Option<&'cfg CustomProfileImpl> {
1096 self.custom_profile
1097 }
1098}
1099
1100#[derive(Clone, Debug)]
1101pub(super) struct NextestConfigImpl {
1102 store: StoreConfigImpl,
1103 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1104 scripts: ScriptConfig,
1105 default_profile: DefaultProfileImpl,
1106 other_profiles: HashMap<String, CustomProfileImpl>,
1107}
1108
1109impl NextestConfigImpl {
1110 fn get_profile(&self, profile: &str) -> Result<Option<&CustomProfileImpl>, ProfileNotFound> {
1111 let custom_profile = match profile {
1112 NextestConfig::DEFAULT_PROFILE => None,
1113 other => Some(
1114 self.other_profiles
1115 .get(other)
1116 .ok_or_else(|| ProfileNotFound::new(profile, self.all_profiles()))?,
1117 ),
1118 };
1119 Ok(custom_profile)
1120 }
1121
1122 fn all_profiles(&self) -> impl Iterator<Item = &str> {
1123 self.other_profiles
1124 .keys()
1125 .map(|key| key.as_str())
1126 .chain(std::iter::once(NextestConfig::DEFAULT_PROFILE))
1127 }
1128
1129 pub(super) fn default_profile(&self) -> &DefaultProfileImpl {
1130 &self.default_profile
1131 }
1132
1133 pub(super) fn other_profiles(&self) -> impl Iterator<Item = (&str, &CustomProfileImpl)> {
1134 self.other_profiles
1135 .iter()
1136 .map(|(key, value)| (key.as_str(), value))
1137 }
1138}
1139
1140#[derive(Clone, Debug, Deserialize)]
1142#[serde(rename_all = "kebab-case")]
1143struct NextestConfigDeserialize {
1144 store: StoreConfigImpl,
1145
1146 #[expect(unused)]
1149 #[serde(default)]
1150 nextest_version: Option<NextestVersionDeserialize>,
1151 #[expect(unused)]
1152 #[serde(default)]
1153 experimental: BTreeSet<String>,
1154
1155 #[serde(default)]
1156 test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
1157 #[serde(default, rename = "script")]
1159 old_setup_scripts: IndexMap<ScriptId, SetupScriptConfig>,
1160 #[serde(default)]
1161 scripts: ScriptConfig,
1162 #[serde(rename = "profile")]
1163 profiles: HashMap<String, CustomProfileImpl>,
1164}
1165
1166impl NextestConfigDeserialize {
1167 fn into_config_impl(mut self) -> NextestConfigImpl {
1168 let p = self
1169 .profiles
1170 .remove("default")
1171 .expect("default profile should exist");
1172 let default_profile = DefaultProfileImpl::new(p);
1173
1174 for (script_id, script_config) in self.old_setup_scripts {
1179 if let indexmap::map::Entry::Vacant(entry) = self.scripts.setup.entry(script_id) {
1180 entry.insert(script_config);
1181 }
1182 }
1183
1184 NextestConfigImpl {
1185 store: self.store,
1186 default_profile,
1187 test_groups: self.test_groups,
1188 scripts: self.scripts,
1189 other_profiles: self.profiles,
1190 }
1191 }
1192}
1193
1194#[derive(Clone, Debug, Deserialize)]
1195#[serde(rename_all = "kebab-case")]
1196struct StoreConfigImpl {
1197 dir: Utf8PathBuf,
1198}
1199
1200#[derive(Clone, Debug)]
1201pub(super) struct DefaultProfileImpl {
1202 default_filter: String,
1203 test_threads: TestThreads,
1204 threads_required: ThreadsRequired,
1205 run_extra_args: Vec<String>,
1206 retries: RetryPolicy,
1207 status_level: StatusLevel,
1208 final_status_level: FinalStatusLevel,
1209 failure_output: TestOutputDisplay,
1210 success_output: TestOutputDisplay,
1211 max_fail: MaxFail,
1212 slow_timeout: SlowTimeout,
1213 leak_timeout: LeakTimeout,
1214 overrides: Vec<DeserializedOverride>,
1215 scripts: Vec<DeserializedProfileScriptConfig>,
1216 junit: DefaultJunitImpl,
1217 archive: ArchiveConfig,
1218}
1219
1220impl DefaultProfileImpl {
1221 fn new(p: CustomProfileImpl) -> Self {
1222 Self {
1223 default_filter: p
1224 .default_filter
1225 .expect("default-filter present in default profile"),
1226 test_threads: p
1227 .test_threads
1228 .expect("test-threads present in default profile"),
1229 threads_required: p
1230 .threads_required
1231 .expect("threads-required present in default profile"),
1232 run_extra_args: p
1233 .run_extra_args
1234 .expect("run-extra-args present in default profile"),
1235 retries: p.retries.expect("retries present in default profile"),
1236 status_level: p
1237 .status_level
1238 .expect("status-level present in default profile"),
1239 final_status_level: p
1240 .final_status_level
1241 .expect("final-status-level present in default profile"),
1242 failure_output: p
1243 .failure_output
1244 .expect("failure-output present in default profile"),
1245 success_output: p
1246 .success_output
1247 .expect("success-output present in default profile"),
1248 max_fail: p.max_fail.expect("fail-fast present in default profile"),
1249 slow_timeout: p
1250 .slow_timeout
1251 .expect("slow-timeout present in default profile"),
1252 leak_timeout: p
1253 .leak_timeout
1254 .expect("leak-timeout present in default profile"),
1255 overrides: p.overrides,
1256 scripts: p.scripts,
1257 junit: DefaultJunitImpl::for_default_profile(p.junit),
1258 archive: p.archive.expect("archive present in default profile"),
1259 }
1260 }
1261
1262 pub(super) fn default_filter(&self) -> &str {
1263 &self.default_filter
1264 }
1265
1266 pub(super) fn overrides(&self) -> &[DeserializedOverride] {
1267 &self.overrides
1268 }
1269
1270 pub(super) fn setup_scripts(&self) -> &[DeserializedProfileScriptConfig] {
1271 &self.scripts
1272 }
1273}
1274
1275#[derive(Clone, Debug, Deserialize)]
1276#[serde(rename_all = "kebab-case")]
1277pub(super) struct CustomProfileImpl {
1278 #[serde(default)]
1280 default_filter: Option<String>,
1281 #[serde(default, deserialize_with = "super::deserialize_retry_policy")]
1282 retries: Option<RetryPolicy>,
1283 #[serde(default)]
1284 test_threads: Option<TestThreads>,
1285 #[serde(default)]
1286 threads_required: Option<ThreadsRequired>,
1287 #[serde(default)]
1288 run_extra_args: Option<Vec<String>>,
1289 #[serde(default)]
1290 status_level: Option<StatusLevel>,
1291 #[serde(default)]
1292 final_status_level: Option<FinalStatusLevel>,
1293 #[serde(default)]
1294 failure_output: Option<TestOutputDisplay>,
1295 #[serde(default)]
1296 success_output: Option<TestOutputDisplay>,
1297 #[serde(
1298 default,
1299 rename = "fail-fast",
1300 deserialize_with = "super::deserialize_fail_fast"
1301 )]
1302 max_fail: Option<MaxFail>,
1303 #[serde(default, deserialize_with = "super::deserialize_slow_timeout")]
1304 slow_timeout: Option<SlowTimeout>,
1305 #[serde(default, deserialize_with = "super::deserialize_leak_timeout")]
1306 leak_timeout: Option<LeakTimeout>,
1307 #[serde(default)]
1308 overrides: Vec<DeserializedOverride>,
1309 #[serde(default)]
1310 scripts: Vec<DeserializedProfileScriptConfig>,
1311 #[serde(default)]
1312 junit: JunitImpl,
1313 #[serde(default)]
1314 archive: Option<ArchiveConfig>,
1315}
1316
1317impl CustomProfileImpl {
1318 #[cfg(test)]
1319 pub(super) fn test_threads(&self) -> Option<TestThreads> {
1320 self.test_threads
1321 }
1322
1323 pub(super) fn default_filter(&self) -> Option<&str> {
1324 self.default_filter.as_deref()
1325 }
1326
1327 pub(super) fn overrides(&self) -> &[DeserializedOverride] {
1328 &self.overrides
1329 }
1330
1331 pub(super) fn scripts(&self) -> &[DeserializedProfileScriptConfig] {
1332 &self.scripts
1333 }
1334}
1335
1336#[cfg(test)]
1337mod tests {
1338 use super::*;
1339 use crate::config::test_helpers::*;
1340 use camino_tempfile::tempdir;
1341 use iddqd::{IdHashItem, IdHashMap, id_hash_map, id_upcast};
1342
1343 #[derive(Default)]
1345 struct TestConfigWarnings {
1346 unknown_keys: IdHashMap<UnknownKeys>,
1347 reserved_profiles: IdHashMap<ReservedProfiles>,
1348 deprecated_scripts: IdHashMap<DeprecatedScripts>,
1349 empty_script_warnings: IdHashMap<EmptyScriptSections>,
1350 }
1351
1352 impl ConfigWarnings for TestConfigWarnings {
1353 fn unknown_config_keys(
1354 &mut self,
1355 config_file: &Utf8Path,
1356 _workspace_root: &Utf8Path,
1357 tool: Option<&str>,
1358 unknown: &BTreeSet<String>,
1359 ) {
1360 self.unknown_keys
1361 .insert_unique(UnknownKeys {
1362 tool: tool.map(|s| s.to_owned()),
1363 config_file: config_file.to_owned(),
1364 keys: unknown.clone(),
1365 })
1366 .unwrap();
1367 }
1368
1369 fn unknown_reserved_profiles(
1370 &mut self,
1371 config_file: &Utf8Path,
1372 _workspace_root: &Utf8Path,
1373 tool: Option<&str>,
1374 profiles: &[&str],
1375 ) {
1376 self.reserved_profiles
1377 .insert_unique(ReservedProfiles {
1378 tool: tool.map(|s| s.to_owned()),
1379 config_file: config_file.to_owned(),
1380 profiles: profiles.iter().map(|&s| s.to_owned()).collect(),
1381 })
1382 .unwrap();
1383 }
1384
1385 fn empty_script_sections(
1386 &mut self,
1387 config_file: &Utf8Path,
1388 _workspace_root: &Utf8Path,
1389 tool: Option<&str>,
1390 profile_name: &str,
1391 empty_count: usize,
1392 ) {
1393 self.empty_script_warnings
1394 .insert_unique(EmptyScriptSections {
1395 tool: tool.map(|s| s.to_owned()),
1396 config_file: config_file.to_owned(),
1397 profile_name: profile_name.to_owned(),
1398 empty_count,
1399 })
1400 .unwrap();
1401 }
1402
1403 fn deprecated_script_config(
1404 &mut self,
1405 config_file: &Utf8Path,
1406 _workspace_root: &Utf8Path,
1407 tool: Option<&str>,
1408 ) {
1409 self.deprecated_scripts
1410 .insert_unique(DeprecatedScripts {
1411 tool: tool.map(|s| s.to_owned()),
1412 config_file: config_file.to_owned(),
1413 })
1414 .unwrap();
1415 }
1416 }
1417
1418 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1419 struct UnknownKeys {
1420 tool: Option<String>,
1421 config_file: Utf8PathBuf,
1422 keys: BTreeSet<String>,
1423 }
1424
1425 impl IdHashItem for UnknownKeys {
1426 type Key<'a> = Option<&'a str>;
1427 fn key(&self) -> Self::Key<'_> {
1428 self.tool.as_deref()
1429 }
1430 id_upcast!();
1431 }
1432
1433 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1434 struct ReservedProfiles {
1435 tool: Option<String>,
1436 config_file: Utf8PathBuf,
1437 profiles: Vec<String>,
1438 }
1439
1440 impl IdHashItem for ReservedProfiles {
1441 type Key<'a> = Option<&'a str>;
1442 fn key(&self) -> Self::Key<'_> {
1443 self.tool.as_deref()
1444 }
1445 id_upcast!();
1446 }
1447
1448 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1449 struct DeprecatedScripts {
1450 tool: Option<String>,
1451 config_file: Utf8PathBuf,
1452 }
1453
1454 impl IdHashItem for DeprecatedScripts {
1455 type Key<'a> = Option<&'a str>;
1456 fn key(&self) -> Self::Key<'_> {
1457 self.tool.as_deref()
1458 }
1459 id_upcast!();
1460 }
1461
1462 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1463 struct WrapperScriptWarning {
1464 tool: Option<String>,
1465 config_file: Utf8PathBuf,
1466 script_id: ScriptId,
1467 }
1468
1469 impl IdHashItem for WrapperScriptWarning {
1470 type Key<'a> = &'a ScriptId;
1471 fn key(&self) -> Self::Key<'_> {
1472 &self.script_id
1473 }
1474 id_upcast!();
1475 }
1476
1477 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1478 struct EmptyScriptSections {
1479 tool: Option<String>,
1480 config_file: Utf8PathBuf,
1481 profile_name: String,
1482 empty_count: usize,
1483 }
1484
1485 impl IdHashItem for EmptyScriptSections {
1486 type Key<'a> = (&'a Option<String>, &'a str);
1487 fn key(&self) -> Self::Key<'_> {
1488 (&self.tool, &self.profile_name)
1489 }
1490 id_upcast!();
1491 }
1492
1493 #[test]
1494 fn default_config_is_valid() {
1495 let default_config = NextestConfig::default_config("foo");
1496 default_config
1497 .profile(NextestConfig::DEFAULT_PROFILE)
1498 .expect("default profile should exist");
1499 }
1500
1501 #[test]
1502 fn ignored_keys() {
1503 let config_contents = r#"
1504 ignored1 = "test"
1505
1506 [profile.default]
1507 retries = 3
1508 ignored2 = "hi"
1509
1510 [profile.default-foo]
1511 retries = 5
1512
1513 [[profile.default.overrides]]
1514 filter = 'test(test_foo)'
1515 retries = 20
1516 ignored3 = 42
1517 "#;
1518
1519 let tool_config_contents = r#"
1520 [store]
1521 ignored4 = 20
1522
1523 [profile.default]
1524 retries = 4
1525 ignored5 = false
1526
1527 [profile.default-bar]
1528 retries = 5
1529
1530 [profile.tool]
1531 retries = 12
1532
1533 [[profile.tool.overrides]]
1534 filter = 'test(test_baz)'
1535 retries = 22
1536 ignored6 = 6.5
1537 "#;
1538
1539 let workspace_dir = tempdir().unwrap();
1540
1541 let graph = temp_workspace(&workspace_dir, config_contents);
1542 let workspace_root = graph.workspace().root();
1543 let tool_path = workspace_root.join(".config/tool.toml");
1544 std::fs::write(&tool_path, tool_config_contents).unwrap();
1545
1546 let pcx = ParseContext::new(&graph);
1547
1548 let mut warnings = TestConfigWarnings::default();
1549
1550 let _ = NextestConfig::from_sources_with_warnings(
1551 workspace_root,
1552 &pcx,
1553 None,
1554 &[ToolConfigFile {
1555 tool: "my-tool".to_owned(),
1556 config_file: tool_path.clone(),
1557 }][..],
1558 &Default::default(),
1559 &mut warnings,
1560 )
1561 .expect("config is valid");
1562
1563 assert_eq!(
1564 warnings.unknown_keys.len(),
1565 2,
1566 "there are two files with unknown keys"
1567 );
1568
1569 assert_eq!(
1570 warnings.unknown_keys,
1571 id_hash_map! {
1572 UnknownKeys {
1573 tool: None,
1574 config_file: workspace_root.join(".config/nextest.toml"),
1575 keys: maplit::btreeset! {
1576 "ignored1".to_owned(),
1577 "profile.default.ignored2".to_owned(),
1578 "profile.default.overrides.0.ignored3".to_owned(),
1579 }
1580 },
1581 UnknownKeys {
1582 tool: Some("my-tool".to_owned()),
1583 config_file: tool_path.clone(),
1584 keys: maplit::btreeset! {
1585 "store.ignored4".to_owned(),
1586 "profile.default.ignored5".to_owned(),
1587 "profile.tool.overrides.0.ignored6".to_owned(),
1588 }
1589 }
1590 }
1591 );
1592 assert_eq!(
1593 warnings.reserved_profiles,
1594 id_hash_map! {
1595 ReservedProfiles {
1596 tool: None,
1597 config_file: workspace_root.join(".config/nextest.toml"),
1598 profiles: vec!["default-foo".to_owned()],
1599 },
1600 ReservedProfiles {
1601 tool: Some("my-tool".to_owned()),
1602 config_file: tool_path,
1603 profiles: vec!["default-bar".to_owned()],
1604 }
1605 },
1606 )
1607 }
1608
1609 #[test]
1610 fn script_warnings() {
1611 let config_contents = r#"
1612 experimental = ["setup-scripts", "wrapper-scripts"]
1613
1614 [scripts.wrapper.script1]
1615 command = "echo test"
1616
1617 [scripts.wrapper.script2]
1618 command = "echo test2"
1619
1620 [scripts.setup.script3]
1621 command = "echo setup"
1622
1623 [[profile.default.scripts]]
1624 filter = 'all()'
1625 # Empty - no setup or wrapper scripts
1626
1627 [[profile.default.scripts]]
1628 filter = 'test(foo)'
1629 setup = ["script3"]
1630
1631 [profile.custom]
1632 [[profile.custom.scripts]]
1633 filter = 'all()'
1634 # Empty - no setup or wrapper scripts
1635
1636 [[profile.custom.scripts]]
1637 filter = 'test(bar)'
1638 # Another empty section
1639 "#;
1640
1641 let tool_config_contents = r#"
1642 experimental = ["setup-scripts", "wrapper-scripts"]
1643
1644 [scripts.wrapper."@tool:tool:disabled_script"]
1645 command = "echo disabled"
1646
1647 [scripts.setup."@tool:tool:setup_script"]
1648 command = "echo setup"
1649
1650 [profile.tool]
1651 [[profile.tool.scripts]]
1652 filter = 'all()'
1653 # Empty section
1654
1655 [[profile.tool.scripts]]
1656 filter = 'test(foo)'
1657 setup = ["@tool:tool:setup_script"]
1658 "#;
1659
1660 let workspace_dir = tempdir().unwrap();
1661 let graph = temp_workspace(&workspace_dir, config_contents);
1662 let workspace_root = graph.workspace().root();
1663 let tool_path = workspace_root.join(".config/tool.toml");
1664 std::fs::write(&tool_path, tool_config_contents).unwrap();
1665
1666 let pcx = ParseContext::new(&graph);
1667
1668 let mut warnings = TestConfigWarnings::default();
1669
1670 let experimental = maplit::btreeset! {
1671 ConfigExperimental::SetupScripts,
1672 ConfigExperimental::WrapperScripts
1673 };
1674 let _ = NextestConfig::from_sources_with_warnings(
1675 workspace_root,
1676 &pcx,
1677 None,
1678 &[ToolConfigFile {
1679 tool: "tool".to_owned(),
1680 config_file: tool_path.clone(),
1681 }][..],
1682 &experimental,
1683 &mut warnings,
1684 )
1685 .expect("config is valid");
1686
1687 assert_eq!(
1688 warnings.empty_script_warnings,
1689 id_hash_map! {
1690 EmptyScriptSections {
1691 tool: None,
1692 config_file: workspace_root.join(".config/nextest.toml"),
1693 profile_name: "default".to_owned(),
1694 empty_count: 1,
1695 },
1696 EmptyScriptSections {
1697 tool: None,
1698 config_file: workspace_root.join(".config/nextest.toml"),
1699 profile_name: "custom".to_owned(),
1700 empty_count: 2,
1701 },
1702 EmptyScriptSections {
1703 tool: Some("tool".to_owned()),
1704 config_file: tool_path,
1705 profile_name: "tool".to_owned(),
1706 empty_count: 1,
1707 }
1708 }
1709 );
1710 }
1711
1712 #[test]
1713 fn deprecated_script_config_warning() {
1714 let config_contents = r#"
1715 experimental = ["setup-scripts"]
1716
1717 [script.my-script]
1718 command = "echo hello"
1719"#;
1720
1721 let tool_config_contents = r#"
1722 experimental = ["setup-scripts"]
1723
1724 [script."@tool:my-tool:my-script"]
1725 command = "echo hello"
1726"#;
1727
1728 let temp_dir = tempdir().unwrap();
1729
1730 let graph = temp_workspace(&temp_dir, config_contents);
1731 let workspace_root = graph.workspace().root();
1732 let tool_path = workspace_root.join(".config/my-tool.toml");
1733 std::fs::write(&tool_path, tool_config_contents).unwrap();
1734 let pcx = ParseContext::new(&graph);
1735
1736 let mut warnings = TestConfigWarnings::default();
1737 NextestConfig::from_sources_with_warnings(
1738 graph.workspace().root(),
1739 &pcx,
1740 None,
1741 &[ToolConfigFile {
1742 tool: "my-tool".to_owned(),
1743 config_file: tool_path.clone(),
1744 }],
1745 &maplit::btreeset! {ConfigExperimental::SetupScripts},
1746 &mut warnings,
1747 )
1748 .expect("config is valid");
1749
1750 assert_eq!(
1751 warnings.deprecated_scripts,
1752 id_hash_map! {
1753 DeprecatedScripts {
1754 tool: None,
1755 config_file: graph.workspace().root().join(".config/nextest.toml"),
1756 },
1757 DeprecatedScripts {
1758 tool: Some("my-tool".to_owned()),
1759 config_file: tool_path,
1760 }
1761 }
1762 );
1763 }
1764}