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