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