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