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