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