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