1use crate::{
7 config::{
8 core::{ConfigIdentifier, EvaluatableProfile, FinalConfig, PreBuildPlatform},
9 elements::{LeakTimeout, SlowTimeout},
10 overrides::{MaybeTargetSpec, PlatformStrings},
11 },
12 double_spawn::{DoubleSpawnContext, DoubleSpawnInfo},
13 errors::{
14 ChildStartError, ConfigCompileError, ConfigCompileErrorKind, ConfigCompileSection,
15 InvalidConfigScriptName,
16 },
17 helpers::convert_rel_path_to_main_sep,
18 list::TestList,
19 platform::BuildPlatforms,
20 reporter::events::SetupScriptEnvMap,
21 test_command::{apply_ld_dyld_env, create_command},
22};
23use camino::Utf8Path;
24use camino_tempfile::Utf8TempPath;
25use guppy::graph::cargo::BuildPlatform;
26use iddqd::{IdOrdItem, id_upcast};
27use indexmap::IndexMap;
28use nextest_filtering::{
29 BinaryQuery, EvalContext, Filterset, FiltersetKind, ParseContext, TestQuery,
30};
31use serde::{Deserialize, de::Error};
32use smol_str::SmolStr;
33use std::{
34 collections::{HashMap, HashSet},
35 fmt,
36 process::Command,
37 sync::Arc,
38};
39
40#[derive(Clone, Debug, Default, Deserialize)]
42#[serde(rename_all = "kebab-case")]
43pub struct ScriptConfig {
44 #[serde(default)]
47 pub setup: IndexMap<ScriptId, SetupScriptConfig>,
48 #[serde(default)]
50 pub wrapper: IndexMap<ScriptId, WrapperScriptConfig>,
51}
52
53impl ScriptConfig {
54 pub(in crate::config) fn is_empty(&self) -> bool {
55 self.setup.is_empty() && self.wrapper.is_empty()
56 }
57
58 pub(in crate::config) fn script_info(&self, id: ScriptId) -> ScriptInfo {
62 let script_type = if self.setup.contains_key(&id) {
63 ScriptType::Setup
64 } else if self.wrapper.contains_key(&id) {
65 ScriptType::Wrapper
66 } else {
67 panic!("ScriptConfig::script_info called with invalid script ID: {id}")
68 };
69
70 ScriptInfo {
71 id: id.clone(),
72 script_type,
73 }
74 }
75
76 pub(in crate::config) fn all_script_ids(&self) -> impl Iterator<Item = &ScriptId> {
78 self.setup.keys().chain(self.wrapper.keys())
79 }
80
81 pub(in crate::config) fn duplicate_ids(&self) -> impl Iterator<Item = &ScriptId> {
84 self.wrapper.keys().filter(|k| self.setup.contains_key(*k))
85 }
86}
87
88#[derive(Clone, Debug)]
90pub struct ScriptInfo {
91 pub id: ScriptId,
93
94 pub script_type: ScriptType,
96}
97
98impl IdOrdItem for ScriptInfo {
99 type Key<'a> = &'a ScriptId;
100 fn key(&self) -> Self::Key<'_> {
101 &self.id
102 }
103 id_upcast!();
104}
105
106#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
108pub enum ScriptType {
109 Setup,
111
112 Wrapper,
114}
115
116impl ScriptType {
117 pub(in crate::config) fn matches(self, profile_script_type: ProfileScriptType) -> bool {
118 match self {
119 ScriptType::Setup => profile_script_type == ProfileScriptType::Setup,
120 ScriptType::Wrapper => {
121 profile_script_type == ProfileScriptType::ListWrapper
122 || profile_script_type == ProfileScriptType::RunWrapper
123 }
124 }
125 }
126}
127
128impl fmt::Display for ScriptType {
129 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
130 match self {
131 ScriptType::Setup => f.write_str("setup"),
132 ScriptType::Wrapper => f.write_str("wrapper"),
133 }
134 }
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139pub enum ProfileScriptType {
140 Setup,
142
143 ListWrapper,
145
146 RunWrapper,
148}
149
150impl fmt::Display for ProfileScriptType {
151 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
152 match self {
153 ProfileScriptType::Setup => f.write_str("setup"),
154 ProfileScriptType::ListWrapper => f.write_str("list-wrapper"),
155 ProfileScriptType::RunWrapper => f.write_str("run-wrapper"),
156 }
157 }
158}
159
160pub struct SetupScripts<'profile> {
162 enabled_scripts: IndexMap<&'profile ScriptId, SetupScript<'profile>>,
163}
164
165impl<'profile> SetupScripts<'profile> {
166 pub(in crate::config) fn new(
167 profile: &'profile EvaluatableProfile<'_>,
168 test_list: &TestList<'_>,
169 ) -> Self {
170 Self::new_with_queries(
171 profile,
172 test_list
173 .iter_tests()
174 .filter(|test| test.test_info.filter_match.is_match())
175 .map(|test| test.to_test_query()),
176 )
177 }
178
179 fn new_with_queries<'a>(
181 profile: &'profile EvaluatableProfile<'_>,
182 matching_tests: impl IntoIterator<Item = TestQuery<'a>>,
183 ) -> Self {
184 let script_config = profile.script_config();
185 let profile_scripts = &profile.compiled_data.scripts;
186 if profile_scripts.is_empty() {
187 return Self {
188 enabled_scripts: IndexMap::new(),
189 };
190 }
191
192 let mut by_script_id = HashMap::new();
194 for profile_script in profile_scripts {
195 for script_id in &profile_script.setup {
196 by_script_id
197 .entry(script_id)
198 .or_insert_with(Vec::new)
199 .push(profile_script);
200 }
201 }
202
203 let env = profile.filterset_ecx();
204
205 let mut enabled_ids = HashSet::new();
207 for test in matching_tests {
208 for (&script_id, compiled) in &by_script_id {
210 if enabled_ids.contains(script_id) {
211 continue;
213 }
214 if compiled.iter().any(|data| data.is_enabled(&test, &env)) {
215 enabled_ids.insert(script_id);
216 }
217 }
218 }
219
220 let mut enabled_scripts = IndexMap::new();
222 for (script_id, config) in &script_config.setup {
223 if enabled_ids.contains(script_id) {
224 let compiled = by_script_id
225 .remove(script_id)
226 .expect("script id must be present");
227 enabled_scripts.insert(
228 script_id,
229 SetupScript {
230 id: script_id.clone(),
231 config,
232 compiled,
233 },
234 );
235 }
236 }
237
238 Self { enabled_scripts }
239 }
240
241 #[inline]
243 pub fn len(&self) -> usize {
244 self.enabled_scripts.len()
245 }
246
247 #[inline]
249 pub fn is_empty(&self) -> bool {
250 self.enabled_scripts.is_empty()
251 }
252
253 #[inline]
255 pub(crate) fn into_iter(self) -> impl Iterator<Item = SetupScript<'profile>> {
256 self.enabled_scripts.into_values()
257 }
258}
259
260#[derive(Clone, Debug)]
264#[non_exhaustive]
265pub(crate) struct SetupScript<'profile> {
266 pub(crate) id: ScriptId,
268
269 pub(crate) config: &'profile SetupScriptConfig,
271
272 pub(crate) compiled: Vec<&'profile CompiledProfileScripts<FinalConfig>>,
274}
275
276impl SetupScript<'_> {
277 pub(crate) fn is_enabled(&self, test: &TestQuery<'_>, cx: &EvalContext<'_>) -> bool {
278 self.compiled
279 .iter()
280 .any(|compiled| compiled.is_enabled(test, cx))
281 }
282}
283
284pub(crate) struct SetupScriptCommand {
286 command: std::process::Command,
288 env_path: Utf8TempPath,
290 double_spawn: Option<DoubleSpawnContext>,
292}
293
294impl SetupScriptCommand {
295 pub(crate) fn new(
297 config: &SetupScriptConfig,
298 profile_name: &str,
299 double_spawn: &DoubleSpawnInfo,
300 test_list: &TestList<'_>,
301 ) -> Result<Self, ChildStartError> {
302 let mut cmd = create_command(
303 config.command.program(
304 test_list.workspace_root(),
305 &test_list.rust_build_meta().target_directory,
306 ),
307 &config.command.args,
308 double_spawn,
309 );
310
311 test_list.cargo_env().apply_env(&mut cmd);
314
315 let env_path = camino_tempfile::Builder::new()
316 .prefix("nextest-env")
317 .tempfile()
318 .map_err(|error| ChildStartError::TempPath(Arc::new(error)))?
319 .into_temp_path();
320
321 cmd.current_dir(test_list.workspace_root())
322 .env("NEXTEST", "1")
324 .env("NEXTEST_PROFILE", profile_name)
326 .env("NEXTEST_ENV", &env_path);
328
329 apply_ld_dyld_env(&mut cmd, test_list.updated_dylib_path());
330
331 let double_spawn = double_spawn.spawn_context();
332
333 Ok(Self {
334 command: cmd,
335 env_path,
336 double_spawn,
337 })
338 }
339
340 #[inline]
342 pub(crate) fn command_mut(&mut self) -> &mut std::process::Command {
343 &mut self.command
344 }
345
346 pub(crate) fn spawn(self) -> std::io::Result<(tokio::process::Child, Utf8TempPath)> {
347 let mut command = tokio::process::Command::from(self.command);
348 let res = command.spawn();
349 if let Some(ctx) = self.double_spawn {
350 ctx.finish();
351 }
352 let child = res?;
353 Ok((child, self.env_path))
354 }
355}
356
357#[derive(Clone, Debug, Default)]
359pub(crate) struct SetupScriptExecuteData<'profile> {
360 env_maps: Vec<(SetupScript<'profile>, SetupScriptEnvMap)>,
361}
362
363impl<'profile> SetupScriptExecuteData<'profile> {
364 pub(crate) fn new() -> Self {
365 Self::default()
366 }
367
368 pub(crate) fn add_script(&mut self, script: SetupScript<'profile>, env_map: SetupScriptEnvMap) {
369 self.env_maps.push((script, env_map));
370 }
371
372 pub(crate) fn apply(&self, test: &TestQuery<'_>, cx: &EvalContext<'_>, command: &mut Command) {
374 for (script, env_map) in &self.env_maps {
375 if script.is_enabled(test, cx) {
376 for (key, value) in env_map.env_map.iter() {
377 command.env(key, value);
378 }
379 }
380 }
381 }
382}
383
384#[derive(Clone, Debug)]
385pub(crate) struct CompiledProfileScripts<State> {
386 pub(in crate::config) setup: Vec<ScriptId>,
387 pub(in crate::config) list_wrapper: Option<ScriptId>,
388 pub(in crate::config) run_wrapper: Option<ScriptId>,
389 pub(in crate::config) data: ProfileScriptData,
390 pub(in crate::config) state: State,
391}
392
393impl CompiledProfileScripts<PreBuildPlatform> {
394 pub(in crate::config) fn new(
395 pcx: &ParseContext<'_>,
396 profile_name: &str,
397 index: usize,
398 source: &DeserializedProfileScriptConfig,
399 errors: &mut Vec<ConfigCompileError>,
400 ) -> Option<Self> {
401 if source.platform.host.is_none()
402 && source.platform.target.is_none()
403 && source.filter.is_none()
404 {
405 errors.push(ConfigCompileError {
406 profile_name: profile_name.to_owned(),
407 section: ConfigCompileSection::Script(index),
408 kind: ConfigCompileErrorKind::ConstraintsNotSpecified {
409 default_filter_specified: false,
412 },
413 });
414 return None;
415 }
416
417 let host_spec = MaybeTargetSpec::new(source.platform.host.as_deref());
418 let target_spec = MaybeTargetSpec::new(source.platform.target.as_deref());
419
420 let filter_expr = source.filter.as_ref().map_or(Ok(None), |filter| {
421 Some(Filterset::parse(
424 filter.clone(),
425 pcx,
426 FiltersetKind::DefaultFilter,
427 ))
428 .transpose()
429 });
430
431 match (host_spec, target_spec, filter_expr) {
432 (Ok(host_spec), Ok(target_spec), Ok(expr)) => Some(Self {
433 setup: source.setup.clone(),
434 list_wrapper: source.list_wrapper.clone(),
435 run_wrapper: source.run_wrapper.clone(),
436 data: ProfileScriptData {
437 host_spec,
438 target_spec,
439 expr,
440 },
441 state: PreBuildPlatform {},
442 }),
443 (maybe_host_err, maybe_platform_err, maybe_parse_err) => {
444 let host_platform_parse_error = maybe_host_err.err();
445 let platform_parse_error = maybe_platform_err.err();
446 let parse_errors = maybe_parse_err.err();
447
448 errors.push(ConfigCompileError {
449 profile_name: profile_name.to_owned(),
450 section: ConfigCompileSection::Script(index),
451 kind: ConfigCompileErrorKind::Parse {
452 host_parse_error: host_platform_parse_error,
453 target_parse_error: platform_parse_error,
454 filter_parse_errors: parse_errors.into_iter().collect(),
455 },
456 });
457 None
458 }
459 }
460 }
461
462 pub(in crate::config) fn apply_build_platforms(
463 self,
464 build_platforms: &BuildPlatforms,
465 ) -> CompiledProfileScripts<FinalConfig> {
466 let host_eval = self.data.host_spec.eval(&build_platforms.host.platform);
467 let host_test_eval = self.data.target_spec.eval(&build_platforms.host.platform);
468 let target_eval = build_platforms
469 .target
470 .as_ref()
471 .map_or(host_test_eval, |target| {
472 self.data.target_spec.eval(&target.triple.platform)
473 });
474
475 CompiledProfileScripts {
476 setup: self.setup,
477 list_wrapper: self.list_wrapper,
478 run_wrapper: self.run_wrapper,
479 data: self.data,
480 state: FinalConfig {
481 host_eval,
482 host_test_eval,
483 target_eval,
484 },
485 }
486 }
487}
488
489impl CompiledProfileScripts<FinalConfig> {
490 pub(in crate::config) fn is_enabled_binary(
491 &self,
492 query: &BinaryQuery<'_>,
493 cx: &EvalContext<'_>,
494 ) -> Option<bool> {
495 if !self.state.host_eval {
496 return Some(false);
497 }
498 if query.platform == BuildPlatform::Host && !self.state.host_test_eval {
499 return Some(false);
500 }
501 if query.platform == BuildPlatform::Target && !self.state.target_eval {
502 return Some(false);
503 }
504
505 if let Some(expr) = &self.data.expr {
506 expr.matches_binary(query, cx)
507 } else {
508 Some(true)
509 }
510 }
511
512 pub(in crate::config) fn is_enabled(
513 &self,
514 query: &TestQuery<'_>,
515 cx: &EvalContext<'_>,
516 ) -> bool {
517 if !self.state.host_eval {
518 return false;
519 }
520 if query.binary_query.platform == BuildPlatform::Host && !self.state.host_test_eval {
521 return false;
522 }
523 if query.binary_query.platform == BuildPlatform::Target && !self.state.target_eval {
524 return false;
525 }
526
527 if let Some(expr) = &self.data.expr {
528 expr.matches_test(query, cx)
529 } else {
530 true
531 }
532 }
533}
534
535#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
537pub struct ScriptId(pub ConfigIdentifier);
538
539impl ScriptId {
540 pub fn new(identifier: SmolStr) -> Result<Self, InvalidConfigScriptName> {
542 let identifier = ConfigIdentifier::new(identifier).map_err(InvalidConfigScriptName)?;
543 Ok(Self(identifier))
544 }
545
546 pub fn as_identifier(&self) -> &ConfigIdentifier {
548 &self.0
549 }
550
551 #[cfg(test)]
552 pub(super) fn as_str(&self) -> &str {
553 self.0.as_str()
554 }
555}
556
557impl<'de> Deserialize<'de> for ScriptId {
558 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
559 where
560 D: serde::Deserializer<'de>,
561 {
562 let identifier = SmolStr::deserialize(deserializer)?;
564 Self::new(identifier).map_err(serde::de::Error::custom)
565 }
566}
567
568impl fmt::Display for ScriptId {
569 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
570 write!(f, "{}", self.0)
571 }
572}
573
574#[derive(Clone, Debug)]
575pub(in crate::config) struct ProfileScriptData {
576 host_spec: MaybeTargetSpec,
577 target_spec: MaybeTargetSpec,
578 expr: Option<Filterset>,
579}
580
581impl ProfileScriptData {
582 pub(in crate::config) fn expr(&self) -> Option<&Filterset> {
583 self.expr.as_ref()
584 }
585}
586
587#[derive(Clone, Debug, Deserialize)]
589#[serde(rename_all = "kebab-case")]
590pub(in crate::config) struct DeserializedProfileScriptConfig {
591 #[serde(default)]
593 pub(in crate::config) platform: PlatformStrings,
594
595 #[serde(default)]
597 filter: Option<String>,
598
599 #[serde(default, deserialize_with = "deserialize_script_ids")]
601 setup: Vec<ScriptId>,
602
603 #[serde(default)]
605 list_wrapper: Option<ScriptId>,
606
607 #[serde(default)]
609 run_wrapper: Option<ScriptId>,
610}
611
612#[derive(Clone, Debug, Deserialize)]
616#[serde(rename_all = "kebab-case")]
617pub struct SetupScriptConfig {
618 pub command: ScriptCommand,
621
622 #[serde(
624 default,
625 deserialize_with = "crate::config::elements::deserialize_slow_timeout"
626 )]
627 pub slow_timeout: Option<SlowTimeout>,
628
629 #[serde(
631 default,
632 deserialize_with = "crate::config::elements::deserialize_leak_timeout"
633 )]
634 pub leak_timeout: Option<LeakTimeout>,
635
636 #[serde(default)]
638 pub capture_stdout: bool,
639
640 #[serde(default)]
642 pub capture_stderr: bool,
643
644 #[serde(default)]
646 pub junit: SetupScriptJunitConfig,
647}
648
649impl SetupScriptConfig {
650 #[inline]
652 pub fn no_capture(&self) -> bool {
653 !(self.capture_stdout && self.capture_stderr)
654 }
655}
656
657#[derive(Copy, Clone, Debug, Deserialize)]
659#[serde(rename_all = "kebab-case")]
660pub struct SetupScriptJunitConfig {
661 #[serde(default = "default_true")]
665 pub store_success_output: bool,
666
667 #[serde(default = "default_true")]
671 pub store_failure_output: bool,
672}
673
674impl Default for SetupScriptJunitConfig {
675 fn default() -> Self {
676 Self {
677 store_success_output: true,
678 store_failure_output: true,
679 }
680 }
681}
682
683#[derive(Clone, Debug, Deserialize)]
687#[serde(rename_all = "kebab-case")]
688pub struct WrapperScriptConfig {
689 pub command: ScriptCommand,
691
692 #[serde(default)]
695 pub target_runner: WrapperScriptTargetRunner,
696}
697
698#[derive(Clone, Debug, Default)]
700pub enum WrapperScriptTargetRunner {
701 #[default]
703 Ignore,
704
705 OverridesWrapper,
707
708 WithinWrapper,
711
712 AroundWrapper,
715}
716
717impl<'de> Deserialize<'de> for WrapperScriptTargetRunner {
718 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
719 where
720 D: serde::Deserializer<'de>,
721 {
722 let s = String::deserialize(deserializer)?;
723 match s.as_str() {
724 "ignore" => Ok(WrapperScriptTargetRunner::Ignore),
725 "overrides-wrapper" => Ok(WrapperScriptTargetRunner::OverridesWrapper),
726 "within-wrapper" => Ok(WrapperScriptTargetRunner::WithinWrapper),
727 "around-wrapper" => Ok(WrapperScriptTargetRunner::AroundWrapper),
728 _ => Err(serde::de::Error::unknown_variant(
729 &s,
730 &[
731 "ignore",
732 "overrides-wrapper",
733 "within-wrapper",
734 "around-wrapper",
735 ],
736 )),
737 }
738 }
739}
740
741fn default_true() -> bool {
742 true
743}
744
745fn deserialize_script_ids<'de, D>(deserializer: D) -> Result<Vec<ScriptId>, D::Error>
746where
747 D: serde::Deserializer<'de>,
748{
749 struct ScriptIdVisitor;
750
751 impl<'de> serde::de::Visitor<'de> for ScriptIdVisitor {
752 type Value = Vec<ScriptId>;
753
754 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
755 formatter.write_str("a script ID (string) or a list of script IDs")
756 }
757
758 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
759 where
760 E: serde::de::Error,
761 {
762 Ok(vec![ScriptId::new(value.into()).map_err(E::custom)?])
763 }
764
765 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
766 where
767 A: serde::de::SeqAccess<'de>,
768 {
769 let mut ids = Vec::new();
770 while let Some(value) = seq.next_element::<String>()? {
771 ids.push(ScriptId::new(value.into()).map_err(A::Error::custom)?);
772 }
773 Ok(ids)
774 }
775 }
776
777 deserializer.deserialize_any(ScriptIdVisitor)
778}
779
780#[derive(Clone, Debug)]
782pub struct ScriptCommand {
783 pub program: String,
785
786 pub args: Vec<String>,
788
789 pub relative_to: ScriptCommandRelativeTo,
794}
795
796impl ScriptCommand {
797 pub fn program(&self, workspace_root: &Utf8Path, target_dir: &Utf8Path) -> String {
799 match self.relative_to {
800 ScriptCommandRelativeTo::None => self.program.clone(),
801 ScriptCommandRelativeTo::WorkspaceRoot => {
802 let path = Utf8Path::new(&self.program);
804 if path.is_relative() {
805 workspace_root
806 .join(convert_rel_path_to_main_sep(path))
807 .to_string()
808 } else {
809 path.to_string()
810 }
811 }
812 ScriptCommandRelativeTo::Target => {
813 let path = Utf8Path::new(&self.program);
815 if path.is_relative() {
816 target_dir
817 .join(convert_rel_path_to_main_sep(path))
818 .to_string()
819 } else {
820 path.to_string()
821 }
822 }
823 }
824 }
825}
826
827impl<'de> Deserialize<'de> for ScriptCommand {
828 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
829 where
830 D: serde::Deserializer<'de>,
831 {
832 struct CommandVisitor;
833
834 impl<'de> serde::de::Visitor<'de> for CommandVisitor {
835 type Value = ScriptCommand;
836
837 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
838 formatter.write_str("a Unix shell command, a list of arguments, or a table with command-line and relative-to")
839 }
840
841 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
842 where
843 E: serde::de::Error,
844 {
845 let mut args = shell_words::split(value).map_err(E::custom)?;
846 if args.is_empty() {
847 return Err(E::invalid_value(serde::de::Unexpected::Str(value), &self));
848 }
849 let program = args.remove(0);
850 Ok(ScriptCommand {
851 program,
852 args,
853 relative_to: ScriptCommandRelativeTo::None,
854 })
855 }
856
857 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
858 where
859 A: serde::de::SeqAccess<'de>,
860 {
861 let Some(program) = seq.next_element::<String>()? else {
862 return Err(A::Error::invalid_length(0, &self));
863 };
864 let mut args = Vec::new();
865 while let Some(value) = seq.next_element::<String>()? {
866 args.push(value);
867 }
868 Ok(ScriptCommand {
869 program,
870 args,
871 relative_to: ScriptCommandRelativeTo::None,
872 })
873 }
874
875 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
876 where
877 A: serde::de::MapAccess<'de>,
878 {
879 let mut command_line = None;
880 let mut relative_to = None;
881
882 while let Some(key) = map.next_key::<String>()? {
883 match key.as_str() {
884 "command-line" => {
885 if command_line.is_some() {
886 return Err(A::Error::duplicate_field("command-line"));
887 }
888 command_line = Some(map.next_value_seed(CommandInnerSeed)?);
889 }
890 "relative-to" => {
891 if relative_to.is_some() {
892 return Err(A::Error::duplicate_field("relative-to"));
893 }
894 relative_to = Some(map.next_value::<ScriptCommandRelativeTo>()?);
895 }
896 _ => {
897 return Err(A::Error::unknown_field(
898 &key,
899 &["command-line", "relative-to"],
900 ));
901 }
902 }
903 }
904
905 let (program, arguments) =
906 command_line.ok_or_else(|| A::Error::missing_field("command-line"))?;
907 let relative_to = relative_to.unwrap_or(ScriptCommandRelativeTo::None);
908
909 Ok(ScriptCommand {
910 program,
911 args: arguments,
912 relative_to,
913 })
914 }
915 }
916
917 deserializer.deserialize_any(CommandVisitor)
918 }
919}
920
921struct CommandInnerSeed;
922
923impl<'de> serde::de::DeserializeSeed<'de> for CommandInnerSeed {
924 type Value = (String, Vec<String>);
925
926 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
927 where
928 D: serde::Deserializer<'de>,
929 {
930 struct CommandInnerVisitor;
931
932 impl<'de> serde::de::Visitor<'de> for CommandInnerVisitor {
933 type Value = (String, Vec<String>);
934
935 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
936 formatter.write_str("a string or array of strings")
937 }
938
939 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
940 where
941 E: serde::de::Error,
942 {
943 let mut args = shell_words::split(value).map_err(E::custom)?;
944 if args.is_empty() {
945 return Err(E::invalid_value(
946 serde::de::Unexpected::Str(value),
947 &"a non-empty command string",
948 ));
949 }
950 let program = args.remove(0);
951 Ok((program, args))
952 }
953
954 fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
955 where
956 S: serde::de::SeqAccess<'de>,
957 {
958 let mut args = Vec::new();
959 while let Some(value) = seq.next_element::<String>()? {
960 args.push(value);
961 }
962 if args.is_empty() {
963 return Err(S::Error::invalid_length(0, &self));
964 }
965 let program = args.remove(0);
966 Ok((program, args))
967 }
968 }
969
970 deserializer.deserialize_any(CommandInnerVisitor)
971 }
972}
973
974#[derive(Clone, Copy, Debug)]
979pub enum ScriptCommandRelativeTo {
980 None,
982
983 WorkspaceRoot,
985
986 Target,
988 }
990
991impl<'de> Deserialize<'de> for ScriptCommandRelativeTo {
992 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
993 where
994 D: serde::Deserializer<'de>,
995 {
996 let s = String::deserialize(deserializer)?;
997 match s.as_str() {
998 "none" => Ok(ScriptCommandRelativeTo::None),
999 "workspace-root" => Ok(ScriptCommandRelativeTo::WorkspaceRoot),
1000 "target" => Ok(ScriptCommandRelativeTo::Target),
1001 _ => Err(serde::de::Error::unknown_variant(&s, &["none", "target"])),
1002 }
1003 }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009 use crate::{
1010 config::{
1011 core::{ConfigExperimental, NextestConfig, ToolConfigFile},
1012 utils::test_helpers::*,
1013 },
1014 errors::{
1015 ConfigParseErrorKind, DisplayErrorChain, ProfileListScriptUsesRunFiltersError,
1016 ProfileScriptErrors, ProfileUnknownScriptError, ProfileWrongConfigScriptTypeError,
1017 },
1018 };
1019 use camino_tempfile::tempdir;
1020 use camino_tempfile_ext::prelude::*;
1021 use indoc::indoc;
1022 use maplit::btreeset;
1023 use test_case::test_case;
1024
1025 #[test]
1026 fn test_scripts_basic() {
1027 let config_contents = indoc! {r#"
1028 [[profile.default.scripts]]
1029 platform = { host = "x86_64-unknown-linux-gnu" }
1030 filter = "test(script1)"
1031 setup = ["foo", "bar"]
1032
1033 [[profile.default.scripts]]
1034 platform = { target = "aarch64-apple-darwin" }
1035 filter = "test(script2)"
1036 setup = "baz"
1037
1038 [[profile.default.scripts]]
1039 filter = "test(script3)"
1040 # No matter which order scripts are specified here, they must always be run in the
1041 # order defined below.
1042 setup = ["baz", "foo", "@tool:my-tool:toolscript"]
1043
1044 [scripts.setup.foo]
1045 command = "command foo"
1046
1047 [scripts.setup.bar]
1048 command = ["cargo", "run", "-p", "bar"]
1049 slow-timeout = { period = "60s", terminate-after = 2 }
1050
1051 [scripts.setup.baz]
1052 command = "baz"
1053 slow-timeout = "1s"
1054 leak-timeout = "1s"
1055 capture-stdout = true
1056 capture-stderr = true
1057 "#
1058 };
1059
1060 let tool_config_contents = indoc! {r#"
1061 [scripts.setup.'@tool:my-tool:toolscript']
1062 command = "tool-command"
1063 "#
1064 };
1065
1066 let workspace_dir = tempdir().unwrap();
1067
1068 let graph = temp_workspace(&workspace_dir, config_contents);
1069 let tool_path = workspace_dir.child(".config/my-tool.toml");
1070 tool_path.write_str(tool_config_contents).unwrap();
1071
1072 let package_id = graph.workspace().iter().next().unwrap().id();
1073
1074 let pcx = ParseContext::new(&graph);
1075
1076 let tool_config_files = [ToolConfigFile {
1077 tool: "my-tool".to_owned(),
1078 config_file: tool_path.to_path_buf(),
1079 }];
1080
1081 let nextest_config_error = NextestConfig::from_sources(
1083 graph.workspace().root(),
1084 &pcx,
1085 None,
1086 &tool_config_files,
1087 &Default::default(),
1088 )
1089 .unwrap_err();
1090 match nextest_config_error.kind() {
1091 ConfigParseErrorKind::ExperimentalFeaturesNotEnabled { missing_features } => {
1092 assert_eq!(
1093 *missing_features,
1094 btreeset! { ConfigExperimental::SetupScripts }
1095 );
1096 }
1097 other => panic!("unexpected error kind: {other:?}"),
1098 }
1099
1100 let nextest_config_result = NextestConfig::from_sources(
1102 graph.workspace().root(),
1103 &pcx,
1104 None,
1105 &tool_config_files,
1106 &btreeset! { ConfigExperimental::SetupScripts },
1107 )
1108 .expect("config is valid");
1109 let profile = nextest_config_result
1110 .profile("default")
1111 .expect("valid profile name")
1112 .apply_build_platforms(&build_platforms());
1113
1114 let host_binary_query =
1116 binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
1117 let query = TestQuery {
1118 binary_query: host_binary_query.to_query(),
1119 test_name: "script1",
1120 };
1121 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1122 assert_eq!(scripts.len(), 2, "two scripts should be enabled");
1123 assert_eq!(
1124 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1125 "foo",
1126 "first script should be foo"
1127 );
1128 assert_eq!(
1129 scripts.enabled_scripts.get_index(1).unwrap().0.as_str(),
1130 "bar",
1131 "second script should be bar"
1132 );
1133
1134 let target_binary_query = binary_query(
1135 &graph,
1136 package_id,
1137 "lib",
1138 "my-binary",
1139 BuildPlatform::Target,
1140 );
1141
1142 let query = TestQuery {
1144 binary_query: target_binary_query.to_query(),
1145 test_name: "script2",
1146 };
1147 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1148 assert_eq!(scripts.len(), 1, "one script should be enabled");
1149 assert_eq!(
1150 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1151 "baz",
1152 "first script should be baz"
1153 );
1154
1155 let query = TestQuery {
1157 binary_query: target_binary_query.to_query(),
1158 test_name: "script3",
1159 };
1160 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1161 assert_eq!(scripts.len(), 3, "three scripts should be enabled");
1162 assert_eq!(
1163 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1164 "@tool:my-tool:toolscript",
1165 "first script should be toolscript"
1166 );
1167 assert_eq!(
1168 scripts.enabled_scripts.get_index(1).unwrap().0.as_str(),
1169 "foo",
1170 "second script should be foo"
1171 );
1172 assert_eq!(
1173 scripts.enabled_scripts.get_index(2).unwrap().0.as_str(),
1174 "baz",
1175 "third script should be baz"
1176 );
1177 }
1178
1179 #[test_case(
1180 indoc! {r#"
1181 [scripts.setup.foo]
1182 command = ""
1183 "#},
1184 "invalid value: string \"\", expected a Unix shell command, a list of arguments, \
1185 or a table with command-line and relative-to"
1186
1187 ; "empty command"
1188 )]
1189 #[test_case(
1190 indoc! {r#"
1191 [scripts.setup.foo]
1192 command = []
1193 "#},
1194 "invalid length 0, expected a Unix shell command, a list of arguments, \
1195 or a table with command-line and relative-to"
1196
1197 ; "empty command list"
1198 )]
1199 #[test_case(
1200 indoc! {r#"
1201 [scripts.setup.foo]
1202 "#},
1203 "scripts.setup.foo: missing field `command`"
1204
1205 ; "missing command"
1206 )]
1207 #[test_case(
1208 indoc! {r#"
1209 [scripts.setup.foo]
1210 command = { command-line = "" }
1211 "#},
1212 "invalid value: string \"\", expected a non-empty command string"
1213
1214 ; "empty command-line in table"
1215 )]
1216 #[test_case(
1217 indoc! {r#"
1218 [scripts.setup.foo]
1219 command = { command-line = [] }
1220 "#},
1221 "invalid length 0, expected a string or array of strings"
1222
1223 ; "empty command-line array in table"
1224 )]
1225 #[test_case(
1226 indoc! {r#"
1227 [scripts.setup.foo]
1228 command = { relative-to = "target" }
1229 "#},
1230 "missing field `command-line`"
1231
1232 ; "missing command-line in table"
1233 )]
1234 #[test_case(
1235 indoc! {r#"
1236 [scripts.setup.foo]
1237 command = { command-line = "my-command", relative-to = "invalid" }
1238 "#},
1239 r#"unknown variant `invalid`, expected `none` or `target`"#
1240
1241 ; "invalid relative-to value"
1242 )]
1243 #[test_case(
1244 indoc! {r#"
1245 [scripts.setup.foo]
1246 command = { command-line = "my-command", unknown-field = "value" }
1247 "#},
1248 r#"unknown field `unknown-field`, expected `command-line` or `relative-to`"#
1249
1250 ; "unknown field in command table"
1251 )]
1252 #[test_case(
1253 indoc! {r#"
1254 [scripts.setup.foo]
1255 command = "my-command"
1256 slow-timeout = 34
1257 "#},
1258 r#"invalid type: integer `34`, expected a table ({ period = "60s", terminate-after = 2 }) or a string ("60s")"#
1259
1260 ; "slow timeout is not a duration"
1261 )]
1262 #[test_case(
1263 indoc! {r#"
1264 [scripts.setup.'@tool:foo']
1265 command = "my-command"
1266 "#},
1267 r#"invalid configuration script name: tool identifier not of the form "@tool:tool-name:identifier": `@tool:foo`"#
1268
1269 ; "invalid tool script name"
1270 )]
1271 #[test_case(
1272 indoc! {r#"
1273 [scripts.setup.'#foo']
1274 command = "my-command"
1275 "#},
1276 r"invalid configuration script name: invalid identifier `#foo`"
1277
1278 ; "invalid script name"
1279 )]
1280 #[test_case(
1281 indoc! {r#"
1282 [scripts.wrapper.foo]
1283 command = "my-command"
1284 target-runner = "not-a-valid-value"
1285 "#},
1286 r#"unknown variant `not-a-valid-value`, expected one of `ignore`, `overrides-wrapper`, `within-wrapper`, `around-wrapper`"#
1287
1288 ; "invalid target-runner value"
1289 )]
1290 #[test_case(
1291 indoc! {r#"
1292 [scripts.wrapper.foo]
1293 command = "my-command"
1294 target-runner = ["foo"]
1295 "#},
1296 r#"invalid type: sequence, expected a string"#
1297
1298 ; "target-runner is not a string"
1299 )]
1300 fn parse_scripts_invalid_deserialize(config_contents: &str, message: &str) {
1301 let workspace_dir = tempdir().unwrap();
1302
1303 let graph = temp_workspace(&workspace_dir, config_contents);
1304 let pcx = ParseContext::new(&graph);
1305
1306 let nextest_config_error = NextestConfig::from_sources(
1307 graph.workspace().root(),
1308 &pcx,
1309 None,
1310 &[][..],
1311 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1312 )
1313 .expect_err("config is invalid");
1314 let actual_message = DisplayErrorChain::new(nextest_config_error).to_string();
1315
1316 assert!(
1317 actual_message.contains(message),
1318 "nextest config error `{actual_message}` contains message `{message}`"
1319 );
1320 }
1321
1322 #[test_case(
1323 indoc! {r#"
1324 [scripts.setup.foo]
1325 command = "my-command"
1326
1327 [[profile.default.scripts]]
1328 setup = ["foo"]
1329 "#},
1330 "default",
1331 &[MietteJsonReport {
1332 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1333 labels: vec![],
1334 }]
1335
1336 ; "neither platform nor filter specified"
1337 )]
1338 #[test_case(
1339 indoc! {r#"
1340 [scripts.setup.foo]
1341 command = "my-command"
1342
1343 [[profile.default.scripts]]
1344 platform = {}
1345 setup = ["foo"]
1346 "#},
1347 "default",
1348 &[MietteJsonReport {
1349 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1350 labels: vec![],
1351 }]
1352
1353 ; "empty platform map"
1354 )]
1355 #[test_case(
1356 indoc! {r#"
1357 [scripts.setup.foo]
1358 command = "my-command"
1359
1360 [[profile.default.scripts]]
1361 platform = { host = 'cfg(target_os = "linux' }
1362 setup = ["foo"]
1363 "#},
1364 "default",
1365 &[MietteJsonReport {
1366 message: "error parsing cfg() expression".to_owned(),
1367 labels: vec![
1368 MietteJsonLabel { label: "expected one of `=`, `,`, `)` here".to_owned(), span: MietteJsonSpan { offset: 3, length: 1 } }
1369 ]
1370 }]
1371
1372 ; "invalid platform expression"
1373 )]
1374 #[test_case(
1375 indoc! {r#"
1376 [scripts.setup.foo]
1377 command = "my-command"
1378
1379 [[profile.ci.overrides]]
1380 filter = 'test(/foo)'
1381 setup = ["foo"]
1382 "#},
1383 "ci",
1384 &[MietteJsonReport {
1385 message: "expected close regex".to_owned(),
1386 labels: vec![
1387 MietteJsonLabel { label: "missing `/`".to_owned(), span: MietteJsonSpan { offset: 9, length: 0 } }
1388 ]
1389 }]
1390
1391 ; "invalid filterset"
1392 )]
1393 fn parse_scripts_invalid_compile(
1394 config_contents: &str,
1395 faulty_profile: &str,
1396 expected_reports: &[MietteJsonReport],
1397 ) {
1398 let workspace_dir = tempdir().unwrap();
1399
1400 let graph = temp_workspace(&workspace_dir, config_contents);
1401
1402 let pcx = ParseContext::new(&graph);
1403
1404 let error = NextestConfig::from_sources(
1405 graph.workspace().root(),
1406 &pcx,
1407 None,
1408 &[][..],
1409 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1410 )
1411 .expect_err("config is invalid");
1412 match error.kind() {
1413 ConfigParseErrorKind::CompileErrors(compile_errors) => {
1414 assert_eq!(
1415 compile_errors.len(),
1416 1,
1417 "exactly one override error must be produced"
1418 );
1419 let error = compile_errors.first().unwrap();
1420 assert_eq!(
1421 error.profile_name, faulty_profile,
1422 "compile error profile matches"
1423 );
1424 let handler = miette::JSONReportHandler::new();
1425 let reports = error
1426 .kind
1427 .reports()
1428 .map(|report| {
1429 let mut out = String::new();
1430 handler.render_report(&mut out, report.as_ref()).unwrap();
1431
1432 let json_report: MietteJsonReport = serde_json::from_str(&out)
1433 .unwrap_or_else(|err| {
1434 panic!(
1435 "failed to deserialize JSON message produced by miette: {err}"
1436 )
1437 });
1438 json_report
1439 })
1440 .collect::<Vec<_>>();
1441 assert_eq!(&reports, expected_reports, "reports match");
1442 }
1443 other => {
1444 panic!(
1445 "for config error {other:?}, expected ConfigParseErrorKind::CompiledDataParseError"
1446 );
1447 }
1448 }
1449 }
1450
1451 #[test_case(
1452 indoc! {r#"
1453 [scripts.setup.'@tool:foo:bar']
1454 command = "my-command"
1455
1456 [[profile.ci.overrides]]
1457 setup = ["@tool:foo:bar"]
1458 "#},
1459 &["@tool:foo:bar"]
1460
1461 ; "tool config in main program")]
1462 fn parse_scripts_invalid_defined(config_contents: &str, expected_invalid_scripts: &[&str]) {
1463 let workspace_dir = tempdir().unwrap();
1464
1465 let graph = temp_workspace(&workspace_dir, config_contents);
1466
1467 let pcx = ParseContext::new(&graph);
1468
1469 let error = NextestConfig::from_sources(
1470 graph.workspace().root(),
1471 &pcx,
1472 None,
1473 &[][..],
1474 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1475 )
1476 .expect_err("config is invalid");
1477 match error.kind() {
1478 ConfigParseErrorKind::InvalidConfigScriptsDefined(scripts) => {
1479 assert_eq!(
1480 scripts.len(),
1481 expected_invalid_scripts.len(),
1482 "correct number of scripts defined"
1483 );
1484 for (script, expected_script) in scripts.iter().zip(expected_invalid_scripts) {
1485 assert_eq!(script.as_str(), *expected_script, "script name matches");
1486 }
1487 }
1488 other => {
1489 panic!(
1490 "for config error {other:?}, expected ConfigParseErrorKind::InvalidConfigScriptsDefined"
1491 );
1492 }
1493 }
1494 }
1495
1496 #[test_case(
1497 indoc! {r#"
1498 [scripts.setup.'blarg']
1499 command = "my-command"
1500
1501 [[profile.ci.overrides]]
1502 setup = ["blarg"]
1503 "#},
1504 &["blarg"]
1505
1506 ; "non-tool config in tool")]
1507 fn parse_scripts_invalid_defined_by_tool(
1508 tool_config_contents: &str,
1509 expected_invalid_scripts: &[&str],
1510 ) {
1511 let workspace_dir = tempdir().unwrap();
1512 let graph = temp_workspace(&workspace_dir, "");
1513
1514 let tool_path = workspace_dir.child(".config/my-tool.toml");
1515 tool_path.write_str(tool_config_contents).unwrap();
1516 let tool_config_files = [ToolConfigFile {
1517 tool: "my-tool".to_owned(),
1518 config_file: tool_path.to_path_buf(),
1519 }];
1520
1521 let pcx = ParseContext::new(&graph);
1522
1523 let error = NextestConfig::from_sources(
1524 graph.workspace().root(),
1525 &pcx,
1526 None,
1527 &tool_config_files,
1528 &btreeset! { ConfigExperimental::SetupScripts },
1529 )
1530 .expect_err("config is invalid");
1531 match error.kind() {
1532 ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool(scripts) => {
1533 assert_eq!(
1534 scripts.len(),
1535 expected_invalid_scripts.len(),
1536 "exactly one script must be defined"
1537 );
1538 for (script, expected_script) in scripts.iter().zip(expected_invalid_scripts) {
1539 assert_eq!(script.as_str(), *expected_script, "script name matches");
1540 }
1541 }
1542 other => {
1543 panic!(
1544 "for config error {other:?}, expected ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool"
1545 );
1546 }
1547 }
1548 }
1549
1550 #[test_case(
1551 indoc! {r#"
1552 [scripts.setup.foo]
1553 command = 'echo foo'
1554
1555 [[profile.default.scripts]]
1556 platform = 'cfg(unix)'
1557 setup = ['bar']
1558
1559 [[profile.ci.scripts]]
1560 platform = 'cfg(unix)'
1561 setup = ['baz']
1562 "#},
1563 vec![
1564 ProfileUnknownScriptError {
1565 profile_name: "default".to_owned(),
1566 name: ScriptId::new("bar".into()).unwrap(),
1567 },
1568 ProfileUnknownScriptError {
1569 profile_name: "ci".to_owned(),
1570 name: ScriptId::new("baz".into()).unwrap(),
1571 },
1572 ],
1573 &["foo"]
1574
1575 ; "unknown scripts"
1576 )]
1577 fn parse_scripts_invalid_unknown(
1578 config_contents: &str,
1579 expected_errors: Vec<ProfileUnknownScriptError>,
1580 expected_known_scripts: &[&str],
1581 ) {
1582 let workspace_dir = tempdir().unwrap();
1583
1584 let graph = temp_workspace(&workspace_dir, config_contents);
1585
1586 let pcx = ParseContext::new(&graph);
1587
1588 let error = NextestConfig::from_sources(
1589 graph.workspace().root(),
1590 &pcx,
1591 None,
1592 &[][..],
1593 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1594 )
1595 .expect_err("config is invalid");
1596 match error.kind() {
1597 ConfigParseErrorKind::ProfileScriptErrors {
1598 errors,
1599 known_scripts,
1600 } => {
1601 let ProfileScriptErrors {
1602 unknown_scripts,
1603 wrong_script_types,
1604 list_scripts_using_run_filters,
1605 } = &**errors;
1606 assert_eq!(wrong_script_types.len(), 0, "no wrong script types");
1607 assert_eq!(
1608 list_scripts_using_run_filters.len(),
1609 0,
1610 "no scripts using run filters in list phase"
1611 );
1612 assert_eq!(
1613 unknown_scripts.len(),
1614 expected_errors.len(),
1615 "correct number of errors"
1616 );
1617 for (error, expected_error) in unknown_scripts.iter().zip(expected_errors) {
1618 assert_eq!(error, &expected_error, "error matches");
1619 }
1620 assert_eq!(
1621 known_scripts.len(),
1622 expected_known_scripts.len(),
1623 "correct number of known scripts"
1624 );
1625 for (script, expected_script) in known_scripts.iter().zip(expected_known_scripts) {
1626 assert_eq!(
1627 script.as_str(),
1628 *expected_script,
1629 "known script name matches"
1630 );
1631 }
1632 }
1633 other => {
1634 panic!(
1635 "for config error {other:?}, expected ConfigParseErrorKind::ProfileScriptErrors"
1636 );
1637 }
1638 }
1639 }
1640
1641 #[test_case(
1642 indoc! {r#"
1643 [scripts.setup.setup-script]
1644 command = 'echo setup'
1645
1646 [scripts.wrapper.wrapper-script]
1647 command = 'echo wrapper'
1648
1649 [[profile.default.scripts]]
1650 platform = 'cfg(unix)'
1651 setup = ['wrapper-script']
1652 list-wrapper = 'setup-script'
1653
1654 [[profile.ci.scripts]]
1655 platform = 'cfg(unix)'
1656 setup = 'wrapper-script'
1657 run-wrapper = 'setup-script'
1658 "#},
1659 vec![
1660 ProfileWrongConfigScriptTypeError {
1661 profile_name: "default".to_owned(),
1662 name: ScriptId::new("wrapper-script".into()).unwrap(),
1663 attempted: ProfileScriptType::Setup,
1664 actual: ScriptType::Wrapper,
1665 },
1666 ProfileWrongConfigScriptTypeError {
1667 profile_name: "default".to_owned(),
1668 name: ScriptId::new("setup-script".into()).unwrap(),
1669 attempted: ProfileScriptType::ListWrapper,
1670 actual: ScriptType::Setup,
1671 },
1672 ProfileWrongConfigScriptTypeError {
1673 profile_name: "ci".to_owned(),
1674 name: ScriptId::new("wrapper-script".into()).unwrap(),
1675 attempted: ProfileScriptType::Setup,
1676 actual: ScriptType::Wrapper,
1677 },
1678 ProfileWrongConfigScriptTypeError {
1679 profile_name: "ci".to_owned(),
1680 name: ScriptId::new("setup-script".into()).unwrap(),
1681 attempted: ProfileScriptType::RunWrapper,
1682 actual: ScriptType::Setup,
1683 },
1684 ],
1685 &["setup-script", "wrapper-script"]
1686
1687 ; "wrong script types"
1688 )]
1689 fn parse_scripts_invalid_wrong_type(
1690 config_contents: &str,
1691 expected_errors: Vec<ProfileWrongConfigScriptTypeError>,
1692 expected_known_scripts: &[&str],
1693 ) {
1694 let workspace_dir = tempdir().unwrap();
1695
1696 let graph = temp_workspace(&workspace_dir, config_contents);
1697
1698 let pcx = ParseContext::new(&graph);
1699
1700 let error = NextestConfig::from_sources(
1701 graph.workspace().root(),
1702 &pcx,
1703 None,
1704 &[][..],
1705 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1706 )
1707 .expect_err("config is invalid");
1708 match error.kind() {
1709 ConfigParseErrorKind::ProfileScriptErrors {
1710 errors,
1711 known_scripts,
1712 } => {
1713 let ProfileScriptErrors {
1714 unknown_scripts,
1715 wrong_script_types,
1716 list_scripts_using_run_filters,
1717 } = &**errors;
1718 assert_eq!(unknown_scripts.len(), 0, "no unknown scripts");
1719 assert_eq!(
1720 list_scripts_using_run_filters.len(),
1721 0,
1722 "no scripts using run filters in list phase"
1723 );
1724 assert_eq!(
1725 wrong_script_types.len(),
1726 expected_errors.len(),
1727 "correct number of errors"
1728 );
1729 for (error, expected_error) in wrong_script_types.iter().zip(expected_errors) {
1730 assert_eq!(error, &expected_error, "error matches");
1731 }
1732 assert_eq!(
1733 known_scripts.len(),
1734 expected_known_scripts.len(),
1735 "correct number of known scripts"
1736 );
1737 for (script, expected_script) in known_scripts.iter().zip(expected_known_scripts) {
1738 assert_eq!(
1739 script.as_str(),
1740 *expected_script,
1741 "known script name matches"
1742 );
1743 }
1744 }
1745 other => {
1746 panic!(
1747 "for config error {other:?}, expected ConfigParseErrorKind::ProfileScriptErrors"
1748 );
1749 }
1750 }
1751 }
1752
1753 #[test_case(
1754 indoc! {r#"
1755 [scripts.wrapper.list-script]
1756 command = 'echo list'
1757
1758 [[profile.default.scripts]]
1759 filter = 'test(hello)'
1760 list-wrapper = 'list-script'
1761
1762 [[profile.ci.scripts]]
1763 filter = 'test(world)'
1764 list-wrapper = 'list-script'
1765 "#},
1766 vec![
1767 ProfileListScriptUsesRunFiltersError {
1768 profile_name: "default".to_owned(),
1769 name: ScriptId::new("list-script".into()).unwrap(),
1770 script_type: ProfileScriptType::ListWrapper,
1771 filters: vec!["test(hello)".to_owned()].into_iter().collect(),
1772 },
1773 ProfileListScriptUsesRunFiltersError {
1774 profile_name: "ci".to_owned(),
1775 name: ScriptId::new("list-script".into()).unwrap(),
1776 script_type: ProfileScriptType::ListWrapper,
1777 filters: vec!["test(world)".to_owned()].into_iter().collect(),
1778 },
1779 ],
1780 &["list-script"]
1781
1782 ; "list scripts using run filters"
1783 )]
1784 fn parse_scripts_invalid_list_using_run_filters(
1785 config_contents: &str,
1786 expected_errors: Vec<ProfileListScriptUsesRunFiltersError>,
1787 expected_known_scripts: &[&str],
1788 ) {
1789 let workspace_dir = tempdir().unwrap();
1790
1791 let graph = temp_workspace(&workspace_dir, config_contents);
1792
1793 let pcx = ParseContext::new(&graph);
1794
1795 let error = NextestConfig::from_sources(
1796 graph.workspace().root(),
1797 &pcx,
1798 None,
1799 &[][..],
1800 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1801 )
1802 .expect_err("config is invalid");
1803 match error.kind() {
1804 ConfigParseErrorKind::ProfileScriptErrors {
1805 errors,
1806 known_scripts,
1807 } => {
1808 let ProfileScriptErrors {
1809 unknown_scripts,
1810 wrong_script_types,
1811 list_scripts_using_run_filters,
1812 } = &**errors;
1813 assert_eq!(unknown_scripts.len(), 0, "no unknown scripts");
1814 assert_eq!(wrong_script_types.len(), 0, "no wrong script types");
1815 assert_eq!(
1816 list_scripts_using_run_filters.len(),
1817 expected_errors.len(),
1818 "correct number of errors"
1819 );
1820 for (error, expected_error) in
1821 list_scripts_using_run_filters.iter().zip(expected_errors)
1822 {
1823 assert_eq!(error, &expected_error, "error matches");
1824 }
1825 assert_eq!(
1826 known_scripts.len(),
1827 expected_known_scripts.len(),
1828 "correct number of known scripts"
1829 );
1830 for (script, expected_script) in known_scripts.iter().zip(expected_known_scripts) {
1831 assert_eq!(
1832 script.as_str(),
1833 *expected_script,
1834 "known script name matches"
1835 );
1836 }
1837 }
1838 other => {
1839 panic!(
1840 "for config error {other:?}, expected ConfigParseErrorKind::ProfileScriptErrors"
1841 );
1842 }
1843 }
1844 }
1845
1846 #[test]
1847 fn test_parse_scripts_empty_sections() {
1848 let config_contents = indoc! {r#"
1849 [scripts.setup.foo]
1850 command = 'echo foo'
1851
1852 [[profile.default.scripts]]
1853 platform = 'cfg(unix)'
1854
1855 [[profile.ci.scripts]]
1856 platform = 'cfg(unix)'
1857 "#};
1858
1859 let workspace_dir = tempdir().unwrap();
1860
1861 let graph = temp_workspace(&workspace_dir, config_contents);
1862
1863 let pcx = ParseContext::new(&graph);
1864
1865 let result = NextestConfig::from_sources(
1867 graph.workspace().root(),
1868 &pcx,
1869 None,
1870 &[][..],
1871 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1872 );
1873
1874 match result {
1875 Ok(_config) => {
1876 }
1879 Err(e) => {
1880 panic!("Config should be valid but got error: {e:?}");
1881 }
1882 }
1883 }
1884}