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, serde::Serialize)]
539#[serde(transparent)]
540pub struct ScriptId(pub ConfigIdentifier);
541
542impl ScriptId {
543 pub fn new(identifier: SmolStr) -> Result<Self, InvalidConfigScriptName> {
545 let identifier = ConfigIdentifier::new(identifier).map_err(InvalidConfigScriptName)?;
546 Ok(Self(identifier))
547 }
548
549 pub fn as_identifier(&self) -> &ConfigIdentifier {
551 &self.0
552 }
553
554 pub fn unique_id(&self, run_id: ReportUuid, stress_index: Option<u32>) -> String {
556 let mut out = String::new();
557 swrite!(out, "{run_id}:{self}");
558 if let Some(stress_index) = stress_index {
559 swrite!(out, "@stress-{}", stress_index);
560 }
561 out
562 }
563
564 #[cfg(test)]
565 pub(super) fn as_str(&self) -> &str {
566 self.0.as_str()
567 }
568}
569
570impl<'de> Deserialize<'de> for ScriptId {
571 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
572 where
573 D: serde::Deserializer<'de>,
574 {
575 let identifier = SmolStr::deserialize(deserializer)?;
577 Self::new(identifier).map_err(serde::de::Error::custom)
578 }
579}
580
581impl fmt::Display for ScriptId {
582 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
583 write!(f, "{}", self.0)
584 }
585}
586
587#[derive(Clone, Debug)]
588pub(in crate::config) struct ProfileScriptData {
589 host_spec: MaybeTargetSpec,
590 target_spec: MaybeTargetSpec,
591 expr: Option<Filterset>,
592}
593
594impl ProfileScriptData {
595 pub(in crate::config) fn expr(&self) -> Option<&Filterset> {
596 self.expr.as_ref()
597 }
598}
599
600#[derive(Clone, Debug, Deserialize)]
602#[serde(rename_all = "kebab-case")]
603pub(in crate::config) struct DeserializedProfileScriptConfig {
604 #[serde(default)]
606 pub(in crate::config) platform: PlatformStrings,
607
608 #[serde(default)]
610 filter: Option<String>,
611
612 #[serde(default, deserialize_with = "deserialize_script_ids")]
614 setup: Vec<ScriptId>,
615
616 #[serde(default)]
618 list_wrapper: Option<ScriptId>,
619
620 #[serde(default)]
622 run_wrapper: Option<ScriptId>,
623}
624
625#[derive(Clone, Debug, Deserialize)]
629#[serde(rename_all = "kebab-case")]
630pub struct SetupScriptConfig {
631 pub command: ScriptCommand,
634
635 #[serde(
637 default,
638 deserialize_with = "crate::config::elements::deserialize_slow_timeout"
639 )]
640 pub slow_timeout: Option<SlowTimeout>,
641
642 #[serde(
644 default,
645 deserialize_with = "crate::config::elements::deserialize_leak_timeout"
646 )]
647 pub leak_timeout: Option<LeakTimeout>,
648
649 #[serde(default)]
651 pub capture_stdout: bool,
652
653 #[serde(default)]
655 pub capture_stderr: bool,
656
657 #[serde(default)]
659 pub junit: SetupScriptJunitConfig,
660}
661
662impl SetupScriptConfig {
663 #[inline]
665 pub fn no_capture(&self) -> bool {
666 !(self.capture_stdout && self.capture_stderr)
667 }
668}
669
670#[derive(Copy, Clone, Debug, Deserialize)]
672#[serde(rename_all = "kebab-case")]
673pub struct SetupScriptJunitConfig {
674 #[serde(default = "default_true")]
678 pub store_success_output: bool,
679
680 #[serde(default = "default_true")]
684 pub store_failure_output: bool,
685}
686
687impl Default for SetupScriptJunitConfig {
688 fn default() -> Self {
689 Self {
690 store_success_output: true,
691 store_failure_output: true,
692 }
693 }
694}
695
696#[derive(Clone, Debug, Deserialize)]
700#[serde(rename_all = "kebab-case")]
701pub struct WrapperScriptConfig {
702 pub command: ScriptCommand,
704
705 #[serde(default)]
708 pub target_runner: WrapperScriptTargetRunner,
709}
710
711#[derive(Clone, Debug, Default)]
713pub enum WrapperScriptTargetRunner {
714 #[default]
716 Ignore,
717
718 OverridesWrapper,
720
721 WithinWrapper,
724
725 AroundWrapper,
728}
729
730impl<'de> Deserialize<'de> for WrapperScriptTargetRunner {
731 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
732 where
733 D: serde::Deserializer<'de>,
734 {
735 let s = String::deserialize(deserializer)?;
736 match s.as_str() {
737 "ignore" => Ok(WrapperScriptTargetRunner::Ignore),
738 "overrides-wrapper" => Ok(WrapperScriptTargetRunner::OverridesWrapper),
739 "within-wrapper" => Ok(WrapperScriptTargetRunner::WithinWrapper),
740 "around-wrapper" => Ok(WrapperScriptTargetRunner::AroundWrapper),
741 _ => Err(serde::de::Error::unknown_variant(
742 &s,
743 &[
744 "ignore",
745 "overrides-wrapper",
746 "within-wrapper",
747 "around-wrapper",
748 ],
749 )),
750 }
751 }
752}
753
754fn default_true() -> bool {
755 true
756}
757
758fn deserialize_script_ids<'de, D>(deserializer: D) -> Result<Vec<ScriptId>, D::Error>
759where
760 D: serde::Deserializer<'de>,
761{
762 struct ScriptIdVisitor;
763
764 impl<'de> serde::de::Visitor<'de> for ScriptIdVisitor {
765 type Value = Vec<ScriptId>;
766
767 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
768 formatter.write_str("a script ID (string) or a list of script IDs")
769 }
770
771 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
772 where
773 E: serde::de::Error,
774 {
775 Ok(vec![ScriptId::new(value.into()).map_err(E::custom)?])
776 }
777
778 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
779 where
780 A: serde::de::SeqAccess<'de>,
781 {
782 let mut ids = Vec::new();
783 while let Some(value) = seq.next_element::<String>()? {
784 ids.push(ScriptId::new(value.into()).map_err(A::Error::custom)?);
785 }
786 Ok(ids)
787 }
788 }
789
790 deserializer.deserialize_any(ScriptIdVisitor)
791}
792
793#[derive(Clone, Debug)]
795pub struct ScriptCommand {
796 pub program: String,
798
799 pub args: Vec<String>,
801
802 pub relative_to: ScriptCommandRelativeTo,
807}
808
809impl ScriptCommand {
810 pub fn program(&self, workspace_root: &Utf8Path, target_dir: &Utf8Path) -> String {
812 match self.relative_to {
813 ScriptCommandRelativeTo::None => self.program.clone(),
814 ScriptCommandRelativeTo::WorkspaceRoot => {
815 let path = Utf8Path::new(&self.program);
817 if path.is_relative() {
818 workspace_root
819 .join(convert_rel_path_to_main_sep(path))
820 .to_string()
821 } else {
822 path.to_string()
823 }
824 }
825 ScriptCommandRelativeTo::Target => {
826 let path = Utf8Path::new(&self.program);
828 if path.is_relative() {
829 target_dir
830 .join(convert_rel_path_to_main_sep(path))
831 .to_string()
832 } else {
833 path.to_string()
834 }
835 }
836 }
837 }
838}
839
840impl<'de> Deserialize<'de> for ScriptCommand {
841 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
842 where
843 D: serde::Deserializer<'de>,
844 {
845 struct CommandVisitor;
846
847 impl<'de> serde::de::Visitor<'de> for CommandVisitor {
848 type Value = ScriptCommand;
849
850 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
851 formatter.write_str("a Unix shell command, a list of arguments, or a table with command-line and relative-to")
852 }
853
854 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
855 where
856 E: serde::de::Error,
857 {
858 let mut args = shell_words::split(value).map_err(E::custom)?;
859 if args.is_empty() {
860 return Err(E::invalid_value(serde::de::Unexpected::Str(value), &self));
861 }
862 let program = args.remove(0);
863 Ok(ScriptCommand {
864 program,
865 args,
866 relative_to: ScriptCommandRelativeTo::None,
867 })
868 }
869
870 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
871 where
872 A: serde::de::SeqAccess<'de>,
873 {
874 let Some(program) = seq.next_element::<String>()? else {
875 return Err(A::Error::invalid_length(0, &self));
876 };
877 let mut args = Vec::new();
878 while let Some(value) = seq.next_element::<String>()? {
879 args.push(value);
880 }
881 Ok(ScriptCommand {
882 program,
883 args,
884 relative_to: ScriptCommandRelativeTo::None,
885 })
886 }
887
888 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
889 where
890 A: serde::de::MapAccess<'de>,
891 {
892 let mut command_line = None;
893 let mut relative_to = None;
894
895 while let Some(key) = map.next_key::<String>()? {
896 match key.as_str() {
897 "command-line" => {
898 if command_line.is_some() {
899 return Err(A::Error::duplicate_field("command-line"));
900 }
901 command_line = Some(map.next_value_seed(CommandInnerSeed)?);
902 }
903 "relative-to" => {
904 if relative_to.is_some() {
905 return Err(A::Error::duplicate_field("relative-to"));
906 }
907 relative_to = Some(map.next_value::<ScriptCommandRelativeTo>()?);
908 }
909 _ => {
910 return Err(A::Error::unknown_field(
911 &key,
912 &["command-line", "relative-to"],
913 ));
914 }
915 }
916 }
917
918 let (program, arguments) =
919 command_line.ok_or_else(|| A::Error::missing_field("command-line"))?;
920 let relative_to = relative_to.unwrap_or(ScriptCommandRelativeTo::None);
921
922 Ok(ScriptCommand {
923 program,
924 args: arguments,
925 relative_to,
926 })
927 }
928 }
929
930 deserializer.deserialize_any(CommandVisitor)
931 }
932}
933
934struct CommandInnerSeed;
935
936impl<'de> serde::de::DeserializeSeed<'de> for CommandInnerSeed {
937 type Value = (String, Vec<String>);
938
939 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
940 where
941 D: serde::Deserializer<'de>,
942 {
943 struct CommandInnerVisitor;
944
945 impl<'de> serde::de::Visitor<'de> for CommandInnerVisitor {
946 type Value = (String, Vec<String>);
947
948 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
949 formatter.write_str("a string or array of strings")
950 }
951
952 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
953 where
954 E: serde::de::Error,
955 {
956 let mut args = shell_words::split(value).map_err(E::custom)?;
957 if args.is_empty() {
958 return Err(E::invalid_value(
959 serde::de::Unexpected::Str(value),
960 &"a non-empty command string",
961 ));
962 }
963 let program = args.remove(0);
964 Ok((program, args))
965 }
966
967 fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
968 where
969 S: serde::de::SeqAccess<'de>,
970 {
971 let mut args = Vec::new();
972 while let Some(value) = seq.next_element::<String>()? {
973 args.push(value);
974 }
975 if args.is_empty() {
976 return Err(S::Error::invalid_length(0, &self));
977 }
978 let program = args.remove(0);
979 Ok((program, args))
980 }
981 }
982
983 deserializer.deserialize_any(CommandInnerVisitor)
984 }
985}
986
987#[derive(Clone, Copy, Debug)]
992pub enum ScriptCommandRelativeTo {
993 None,
995
996 WorkspaceRoot,
998
999 Target,
1001 }
1003
1004impl<'de> Deserialize<'de> for ScriptCommandRelativeTo {
1005 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1006 where
1007 D: serde::Deserializer<'de>,
1008 {
1009 let s = String::deserialize(deserializer)?;
1010 match s.as_str() {
1011 "none" => Ok(ScriptCommandRelativeTo::None),
1012 "workspace-root" => Ok(ScriptCommandRelativeTo::WorkspaceRoot),
1013 "target" => Ok(ScriptCommandRelativeTo::Target),
1014 _ => Err(serde::de::Error::unknown_variant(&s, &["none", "target"])),
1015 }
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022 use crate::{
1023 config::{
1024 core::{ConfigExperimental, NextestConfig, ToolConfigFile, ToolName},
1025 utils::test_helpers::*,
1026 },
1027 errors::{
1028 ConfigParseErrorKind, DisplayErrorChain, ProfileListScriptUsesRunFiltersError,
1029 ProfileScriptErrors, ProfileUnknownScriptError, ProfileWrongConfigScriptTypeError,
1030 },
1031 };
1032 use camino_tempfile::tempdir;
1033 use camino_tempfile_ext::prelude::*;
1034 use indoc::indoc;
1035 use maplit::btreeset;
1036 use nextest_metadata::TestCaseName;
1037 use test_case::test_case;
1038
1039 fn tool_name(s: &str) -> ToolName {
1040 ToolName::new(s.into()).unwrap()
1041 }
1042
1043 #[test]
1044 fn test_scripts_basic() {
1045 let config_contents = indoc! {r#"
1046 [[profile.default.scripts]]
1047 platform = { host = "x86_64-unknown-linux-gnu" }
1048 filter = "test(script1)"
1049 setup = ["foo", "bar"]
1050
1051 [[profile.default.scripts]]
1052 platform = { target = "aarch64-apple-darwin" }
1053 filter = "test(script2)"
1054 setup = "baz"
1055
1056 [[profile.default.scripts]]
1057 filter = "test(script3)"
1058 # No matter which order scripts are specified here, they must always be run in the
1059 # order defined below.
1060 setup = ["baz", "foo", "@tool:my-tool:toolscript"]
1061
1062 [scripts.setup.foo]
1063 command = "command foo"
1064
1065 [scripts.setup.bar]
1066 command = ["cargo", "run", "-p", "bar"]
1067 slow-timeout = { period = "60s", terminate-after = 2 }
1068
1069 [scripts.setup.baz]
1070 command = "baz"
1071 slow-timeout = "1s"
1072 leak-timeout = "1s"
1073 capture-stdout = true
1074 capture-stderr = true
1075 "#
1076 };
1077
1078 let tool_config_contents = indoc! {r#"
1079 [scripts.setup.'@tool:my-tool:toolscript']
1080 command = "tool-command"
1081 "#
1082 };
1083
1084 let workspace_dir = tempdir().unwrap();
1085
1086 let graph = temp_workspace(&workspace_dir, config_contents);
1087 let tool_path = workspace_dir.child(".config/my-tool.toml");
1088 tool_path.write_str(tool_config_contents).unwrap();
1089
1090 let package_id = graph.workspace().iter().next().unwrap().id();
1091
1092 let pcx = ParseContext::new(&graph);
1093
1094 let tool_config_files = [ToolConfigFile {
1095 tool: tool_name("my-tool"),
1096 config_file: tool_path.to_path_buf(),
1097 }];
1098
1099 let nextest_config_error = NextestConfig::from_sources(
1101 graph.workspace().root(),
1102 &pcx,
1103 None,
1104 &tool_config_files,
1105 &Default::default(),
1106 )
1107 .unwrap_err();
1108 match nextest_config_error.kind() {
1109 ConfigParseErrorKind::ExperimentalFeaturesNotEnabled { missing_features } => {
1110 assert_eq!(
1111 *missing_features,
1112 btreeset! { ConfigExperimental::SetupScripts }
1113 );
1114 }
1115 other => panic!("unexpected error kind: {other:?}"),
1116 }
1117
1118 let nextest_config_result = NextestConfig::from_sources(
1120 graph.workspace().root(),
1121 &pcx,
1122 None,
1123 &tool_config_files,
1124 &btreeset! { ConfigExperimental::SetupScripts },
1125 )
1126 .expect("config is valid");
1127 let profile = nextest_config_result
1128 .profile("default")
1129 .expect("valid profile name")
1130 .apply_build_platforms(&build_platforms());
1131
1132 let host_binary_query =
1134 binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
1135 let test_name = TestCaseName::new("script1");
1136 let query = TestQuery {
1137 binary_query: host_binary_query.to_query(),
1138 test_name: &test_name,
1139 };
1140 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1141 assert_eq!(scripts.len(), 2, "two scripts should be enabled");
1142 assert_eq!(
1143 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1144 "foo",
1145 "first script should be foo"
1146 );
1147 assert_eq!(
1148 scripts.enabled_scripts.get_index(1).unwrap().0.as_str(),
1149 "bar",
1150 "second script should be bar"
1151 );
1152
1153 let target_binary_query = binary_query(
1154 &graph,
1155 package_id,
1156 "lib",
1157 "my-binary",
1158 BuildPlatform::Target,
1159 );
1160
1161 let test_name = TestCaseName::new("script2");
1163 let query = TestQuery {
1164 binary_query: target_binary_query.to_query(),
1165 test_name: &test_name,
1166 };
1167 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1168 assert_eq!(scripts.len(), 1, "one script should be enabled");
1169 assert_eq!(
1170 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1171 "baz",
1172 "first script should be baz"
1173 );
1174
1175 let test_name = TestCaseName::new("script3");
1177 let query = TestQuery {
1178 binary_query: target_binary_query.to_query(),
1179 test_name: &test_name,
1180 };
1181 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1182 assert_eq!(scripts.len(), 3, "three scripts should be enabled");
1183 assert_eq!(
1184 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1185 "@tool:my-tool:toolscript",
1186 "first script should be toolscript"
1187 );
1188 assert_eq!(
1189 scripts.enabled_scripts.get_index(1).unwrap().0.as_str(),
1190 "foo",
1191 "second script should be foo"
1192 );
1193 assert_eq!(
1194 scripts.enabled_scripts.get_index(2).unwrap().0.as_str(),
1195 "baz",
1196 "third script should be baz"
1197 );
1198 }
1199
1200 #[test_case(
1201 indoc! {r#"
1202 [scripts.setup.foo]
1203 command = ""
1204 "#},
1205 "invalid value: string \"\", expected a Unix shell command, a list of arguments, \
1206 or a table with command-line and relative-to"
1207
1208 ; "empty command"
1209 )]
1210 #[test_case(
1211 indoc! {r#"
1212 [scripts.setup.foo]
1213 command = []
1214 "#},
1215 "invalid length 0, expected a Unix shell command, a list of arguments, \
1216 or a table with command-line and relative-to"
1217
1218 ; "empty command list"
1219 )]
1220 #[test_case(
1221 indoc! {r#"
1222 [scripts.setup.foo]
1223 "#},
1224 r#"scripts.setup.foo: missing configuration field "scripts.setup.foo.command""#
1225
1226 ; "missing command"
1227 )]
1228 #[test_case(
1229 indoc! {r#"
1230 [scripts.setup.foo]
1231 command = { command-line = "" }
1232 "#},
1233 "invalid value: string \"\", expected a non-empty command string"
1234
1235 ; "empty command-line in table"
1236 )]
1237 #[test_case(
1238 indoc! {r#"
1239 [scripts.setup.foo]
1240 command = { command-line = [] }
1241 "#},
1242 "invalid length 0, expected a string or array of strings"
1243
1244 ; "empty command-line array in table"
1245 )]
1246 #[test_case(
1247 indoc! {r#"
1248 [scripts.setup.foo]
1249 command = { relative-to = "target" }
1250 "#},
1251 r#"missing configuration field "scripts.setup.foo.command.command-line""#
1252
1253 ; "missing command-line in table"
1254 )]
1255 #[test_case(
1256 indoc! {r#"
1257 [scripts.setup.foo]
1258 command = { command-line = "my-command", relative-to = "invalid" }
1259 "#},
1260 r#"unknown variant `invalid`, expected `none` or `target`"#
1261
1262 ; "invalid relative-to value"
1263 )]
1264 #[test_case(
1265 indoc! {r#"
1266 [scripts.setup.foo]
1267 command = { command-line = "my-command", unknown-field = "value" }
1268 "#},
1269 r#"unknown field `unknown-field`, expected `command-line` or `relative-to`"#
1270
1271 ; "unknown field in command table"
1272 )]
1273 #[test_case(
1274 indoc! {r#"
1275 [scripts.setup.foo]
1276 command = "my-command"
1277 slow-timeout = 34
1278 "#},
1279 r#"invalid type: integer `34`, expected a table ({ period = "60s", terminate-after = 2 }) or a string ("60s")"#
1280
1281 ; "slow timeout is not a duration"
1282 )]
1283 #[test_case(
1284 indoc! {r#"
1285 [scripts.setup.'@tool:foo']
1286 command = "my-command"
1287 "#},
1288 r#"invalid configuration script name: tool identifier not of the form "@tool:tool-name:identifier": `@tool:foo`"#
1289
1290 ; "invalid tool script name"
1291 )]
1292 #[test_case(
1293 indoc! {r#"
1294 [scripts.setup.'#foo']
1295 command = "my-command"
1296 "#},
1297 r"invalid configuration script name: invalid identifier `#foo`"
1298
1299 ; "invalid script name"
1300 )]
1301 #[test_case(
1302 indoc! {r#"
1303 [scripts.wrapper.foo]
1304 command = "my-command"
1305 target-runner = "not-a-valid-value"
1306 "#},
1307 r#"unknown variant `not-a-valid-value`, expected one of `ignore`, `overrides-wrapper`, `within-wrapper`, `around-wrapper`"#
1308
1309 ; "invalid target-runner value"
1310 )]
1311 #[test_case(
1312 indoc! {r#"
1313 [scripts.wrapper.foo]
1314 command = "my-command"
1315 target-runner = ["foo"]
1316 "#},
1317 r#"invalid type: sequence, expected a string"#
1318
1319 ; "target-runner is not a string"
1320 )]
1321 fn parse_scripts_invalid_deserialize(config_contents: &str, message: &str) {
1322 let workspace_dir = tempdir().unwrap();
1323
1324 let graph = temp_workspace(&workspace_dir, config_contents);
1325 let pcx = ParseContext::new(&graph);
1326
1327 let nextest_config_error = NextestConfig::from_sources(
1328 graph.workspace().root(),
1329 &pcx,
1330 None,
1331 &[][..],
1332 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1333 )
1334 .expect_err("config is invalid");
1335 let actual_message = DisplayErrorChain::new(nextest_config_error).to_string();
1336
1337 assert!(
1338 actual_message.contains(message),
1339 "nextest config error `{actual_message}` contains message `{message}`"
1340 );
1341 }
1342
1343 #[test_case(
1344 indoc! {r#"
1345 [scripts.setup.foo]
1346 command = "my-command"
1347
1348 [[profile.default.scripts]]
1349 setup = ["foo"]
1350 "#},
1351 "default",
1352 &[MietteJsonReport {
1353 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1354 labels: vec![],
1355 }]
1356
1357 ; "neither platform nor filter specified"
1358 )]
1359 #[test_case(
1360 indoc! {r#"
1361 [scripts.setup.foo]
1362 command = "my-command"
1363
1364 [[profile.default.scripts]]
1365 platform = {}
1366 setup = ["foo"]
1367 "#},
1368 "default",
1369 &[MietteJsonReport {
1370 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1371 labels: vec![],
1372 }]
1373
1374 ; "empty platform map"
1375 )]
1376 #[test_case(
1377 indoc! {r#"
1378 [scripts.setup.foo]
1379 command = "my-command"
1380
1381 [[profile.default.scripts]]
1382 platform = { host = 'cfg(target_os = "linux' }
1383 setup = ["foo"]
1384 "#},
1385 "default",
1386 &[MietteJsonReport {
1387 message: "error parsing cfg() expression".to_owned(),
1388 labels: vec![
1389 MietteJsonLabel { label: "expected one of `=`, `,`, `)` here".to_owned(), span: MietteJsonSpan { offset: 3, length: 1 } }
1390 ]
1391 }]
1392
1393 ; "invalid platform expression"
1394 )]
1395 #[test_case(
1396 indoc! {r#"
1397 [scripts.setup.foo]
1398 command = "my-command"
1399
1400 [[profile.ci.overrides]]
1401 filter = 'test(/foo)'
1402 setup = ["foo"]
1403 "#},
1404 "ci",
1405 &[MietteJsonReport {
1406 message: "expected close regex".to_owned(),
1407 labels: vec![
1408 MietteJsonLabel { label: "missing `/`".to_owned(), span: MietteJsonSpan { offset: 9, length: 0 } }
1409 ]
1410 }]
1411
1412 ; "invalid filterset"
1413 )]
1414 fn parse_scripts_invalid_compile(
1415 config_contents: &str,
1416 faulty_profile: &str,
1417 expected_reports: &[MietteJsonReport],
1418 ) {
1419 let workspace_dir = tempdir().unwrap();
1420
1421 let graph = temp_workspace(&workspace_dir, config_contents);
1422
1423 let pcx = ParseContext::new(&graph);
1424
1425 let error = NextestConfig::from_sources(
1426 graph.workspace().root(),
1427 &pcx,
1428 None,
1429 &[][..],
1430 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1431 )
1432 .expect_err("config is invalid");
1433 match error.kind() {
1434 ConfigParseErrorKind::CompileErrors(compile_errors) => {
1435 assert_eq!(
1436 compile_errors.len(),
1437 1,
1438 "exactly one override error must be produced"
1439 );
1440 let error = compile_errors.first().unwrap();
1441 assert_eq!(
1442 error.profile_name, faulty_profile,
1443 "compile error profile matches"
1444 );
1445 let handler = miette::JSONReportHandler::new();
1446 let reports = error
1447 .kind
1448 .reports()
1449 .map(|report| {
1450 let mut out = String::new();
1451 handler.render_report(&mut out, report.as_ref()).unwrap();
1452
1453 let json_report: MietteJsonReport = serde_json::from_str(&out)
1454 .unwrap_or_else(|err| {
1455 panic!(
1456 "failed to deserialize JSON message produced by miette: {err}"
1457 )
1458 });
1459 json_report
1460 })
1461 .collect::<Vec<_>>();
1462 assert_eq!(&reports, expected_reports, "reports match");
1463 }
1464 other => {
1465 panic!(
1466 "for config error {other:?}, expected ConfigParseErrorKind::CompiledDataParseError"
1467 );
1468 }
1469 }
1470 }
1471
1472 #[test_case(
1473 indoc! {r#"
1474 [scripts.setup.'@tool:foo:bar']
1475 command = "my-command"
1476
1477 [[profile.ci.overrides]]
1478 setup = ["@tool:foo:bar"]
1479 "#},
1480 &["@tool:foo:bar"]
1481
1482 ; "tool config in main program")]
1483 fn parse_scripts_invalid_defined(config_contents: &str, expected_invalid_scripts: &[&str]) {
1484 let workspace_dir = tempdir().unwrap();
1485
1486 let graph = temp_workspace(&workspace_dir, config_contents);
1487
1488 let pcx = ParseContext::new(&graph);
1489
1490 let error = NextestConfig::from_sources(
1491 graph.workspace().root(),
1492 &pcx,
1493 None,
1494 &[][..],
1495 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1496 )
1497 .expect_err("config is invalid");
1498 match error.kind() {
1499 ConfigParseErrorKind::InvalidConfigScriptsDefined(scripts) => {
1500 assert_eq!(
1501 scripts.len(),
1502 expected_invalid_scripts.len(),
1503 "correct number of scripts 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::InvalidConfigScriptsDefined"
1512 );
1513 }
1514 }
1515 }
1516
1517 #[test_case(
1518 indoc! {r#"
1519 [scripts.setup.'blarg']
1520 command = "my-command"
1521
1522 [[profile.ci.overrides]]
1523 setup = ["blarg"]
1524 "#},
1525 &["blarg"]
1526
1527 ; "non-tool config in tool")]
1528 fn parse_scripts_invalid_defined_by_tool(
1529 tool_config_contents: &str,
1530 expected_invalid_scripts: &[&str],
1531 ) {
1532 let workspace_dir = tempdir().unwrap();
1533 let graph = temp_workspace(&workspace_dir, "");
1534
1535 let tool_path = workspace_dir.child(".config/my-tool.toml");
1536 tool_path.write_str(tool_config_contents).unwrap();
1537 let tool_config_files = [ToolConfigFile {
1538 tool: tool_name("my-tool"),
1539 config_file: tool_path.to_path_buf(),
1540 }];
1541
1542 let pcx = ParseContext::new(&graph);
1543
1544 let error = NextestConfig::from_sources(
1545 graph.workspace().root(),
1546 &pcx,
1547 None,
1548 &tool_config_files,
1549 &btreeset! { ConfigExperimental::SetupScripts },
1550 )
1551 .expect_err("config is invalid");
1552 match error.kind() {
1553 ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool(scripts) => {
1554 assert_eq!(
1555 scripts.len(),
1556 expected_invalid_scripts.len(),
1557 "exactly one script must be defined"
1558 );
1559 for (script, expected_script) in scripts.iter().zip(expected_invalid_scripts) {
1560 assert_eq!(script.as_str(), *expected_script, "script name matches");
1561 }
1562 }
1563 other => {
1564 panic!(
1565 "for config error {other:?}, expected ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool"
1566 );
1567 }
1568 }
1569 }
1570
1571 #[test_case(
1572 indoc! {r#"
1573 [scripts.setup.foo]
1574 command = 'echo foo'
1575
1576 [[profile.default.scripts]]
1577 platform = 'cfg(unix)'
1578 setup = ['bar']
1579
1580 [[profile.ci.scripts]]
1581 platform = 'cfg(unix)'
1582 setup = ['baz']
1583 "#},
1584 vec![
1585 ProfileUnknownScriptError {
1586 profile_name: "default".to_owned(),
1587 name: ScriptId::new("bar".into()).unwrap(),
1588 },
1589 ProfileUnknownScriptError {
1590 profile_name: "ci".to_owned(),
1591 name: ScriptId::new("baz".into()).unwrap(),
1592 },
1593 ],
1594 &["foo"]
1595
1596 ; "unknown scripts"
1597 )]
1598 fn parse_scripts_invalid_unknown(
1599 config_contents: &str,
1600 expected_errors: Vec<ProfileUnknownScriptError>,
1601 expected_known_scripts: &[&str],
1602 ) {
1603 let workspace_dir = tempdir().unwrap();
1604
1605 let graph = temp_workspace(&workspace_dir, config_contents);
1606
1607 let pcx = ParseContext::new(&graph);
1608
1609 let error = NextestConfig::from_sources(
1610 graph.workspace().root(),
1611 &pcx,
1612 None,
1613 &[][..],
1614 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1615 )
1616 .expect_err("config is invalid");
1617 match error.kind() {
1618 ConfigParseErrorKind::ProfileScriptErrors {
1619 errors,
1620 known_scripts,
1621 } => {
1622 let ProfileScriptErrors {
1623 unknown_scripts,
1624 wrong_script_types,
1625 list_scripts_using_run_filters,
1626 } = &**errors;
1627 assert_eq!(wrong_script_types.len(), 0, "no wrong script types");
1628 assert_eq!(
1629 list_scripts_using_run_filters.len(),
1630 0,
1631 "no scripts using run filters in list phase"
1632 );
1633 assert_eq!(
1634 unknown_scripts.len(),
1635 expected_errors.len(),
1636 "correct number of errors"
1637 );
1638 for (error, expected_error) in unknown_scripts.iter().zip(expected_errors) {
1639 assert_eq!(error, &expected_error, "error matches");
1640 }
1641 assert_eq!(
1642 known_scripts.len(),
1643 expected_known_scripts.len(),
1644 "correct number of known scripts"
1645 );
1646 for (script, expected_script) in known_scripts.iter().zip(expected_known_scripts) {
1647 assert_eq!(
1648 script.as_str(),
1649 *expected_script,
1650 "known script name matches"
1651 );
1652 }
1653 }
1654 other => {
1655 panic!(
1656 "for config error {other:?}, expected ConfigParseErrorKind::ProfileScriptErrors"
1657 );
1658 }
1659 }
1660 }
1661
1662 #[test_case(
1663 indoc! {r#"
1664 [scripts.setup.setup-script]
1665 command = 'echo setup'
1666
1667 [scripts.wrapper.wrapper-script]
1668 command = 'echo wrapper'
1669
1670 [[profile.default.scripts]]
1671 platform = 'cfg(unix)'
1672 setup = ['wrapper-script']
1673 list-wrapper = 'setup-script'
1674
1675 [[profile.ci.scripts]]
1676 platform = 'cfg(unix)'
1677 setup = 'wrapper-script'
1678 run-wrapper = 'setup-script'
1679 "#},
1680 vec![
1681 ProfileWrongConfigScriptTypeError {
1682 profile_name: "default".to_owned(),
1683 name: ScriptId::new("wrapper-script".into()).unwrap(),
1684 attempted: ProfileScriptType::Setup,
1685 actual: ScriptType::Wrapper,
1686 },
1687 ProfileWrongConfigScriptTypeError {
1688 profile_name: "default".to_owned(),
1689 name: ScriptId::new("setup-script".into()).unwrap(),
1690 attempted: ProfileScriptType::ListWrapper,
1691 actual: ScriptType::Setup,
1692 },
1693 ProfileWrongConfigScriptTypeError {
1694 profile_name: "ci".to_owned(),
1695 name: ScriptId::new("wrapper-script".into()).unwrap(),
1696 attempted: ProfileScriptType::Setup,
1697 actual: ScriptType::Wrapper,
1698 },
1699 ProfileWrongConfigScriptTypeError {
1700 profile_name: "ci".to_owned(),
1701 name: ScriptId::new("setup-script".into()).unwrap(),
1702 attempted: ProfileScriptType::RunWrapper,
1703 actual: ScriptType::Setup,
1704 },
1705 ],
1706 &["setup-script", "wrapper-script"]
1707
1708 ; "wrong script types"
1709 )]
1710 fn parse_scripts_invalid_wrong_type(
1711 config_contents: &str,
1712 expected_errors: Vec<ProfileWrongConfigScriptTypeError>,
1713 expected_known_scripts: &[&str],
1714 ) {
1715 let workspace_dir = tempdir().unwrap();
1716
1717 let graph = temp_workspace(&workspace_dir, config_contents);
1718
1719 let pcx = ParseContext::new(&graph);
1720
1721 let error = NextestConfig::from_sources(
1722 graph.workspace().root(),
1723 &pcx,
1724 None,
1725 &[][..],
1726 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1727 )
1728 .expect_err("config is invalid");
1729 match error.kind() {
1730 ConfigParseErrorKind::ProfileScriptErrors {
1731 errors,
1732 known_scripts,
1733 } => {
1734 let ProfileScriptErrors {
1735 unknown_scripts,
1736 wrong_script_types,
1737 list_scripts_using_run_filters,
1738 } = &**errors;
1739 assert_eq!(unknown_scripts.len(), 0, "no unknown scripts");
1740 assert_eq!(
1741 list_scripts_using_run_filters.len(),
1742 0,
1743 "no scripts using run filters in list phase"
1744 );
1745 assert_eq!(
1746 wrong_script_types.len(),
1747 expected_errors.len(),
1748 "correct number of errors"
1749 );
1750 for (error, expected_error) in wrong_script_types.iter().zip(expected_errors) {
1751 assert_eq!(error, &expected_error, "error matches");
1752 }
1753 assert_eq!(
1754 known_scripts.len(),
1755 expected_known_scripts.len(),
1756 "correct number of known scripts"
1757 );
1758 for (script, expected_script) in known_scripts.iter().zip(expected_known_scripts) {
1759 assert_eq!(
1760 script.as_str(),
1761 *expected_script,
1762 "known script name matches"
1763 );
1764 }
1765 }
1766 other => {
1767 panic!(
1768 "for config error {other:?}, expected ConfigParseErrorKind::ProfileScriptErrors"
1769 );
1770 }
1771 }
1772 }
1773
1774 #[test_case(
1775 indoc! {r#"
1776 [scripts.wrapper.list-script]
1777 command = 'echo list'
1778
1779 [[profile.default.scripts]]
1780 filter = 'test(hello)'
1781 list-wrapper = 'list-script'
1782
1783 [[profile.ci.scripts]]
1784 filter = 'test(world)'
1785 list-wrapper = 'list-script'
1786 "#},
1787 vec![
1788 ProfileListScriptUsesRunFiltersError {
1789 profile_name: "default".to_owned(),
1790 name: ScriptId::new("list-script".into()).unwrap(),
1791 script_type: ProfileScriptType::ListWrapper,
1792 filters: vec!["test(hello)".to_owned()].into_iter().collect(),
1793 },
1794 ProfileListScriptUsesRunFiltersError {
1795 profile_name: "ci".to_owned(),
1796 name: ScriptId::new("list-script".into()).unwrap(),
1797 script_type: ProfileScriptType::ListWrapper,
1798 filters: vec!["test(world)".to_owned()].into_iter().collect(),
1799 },
1800 ],
1801 &["list-script"]
1802
1803 ; "list scripts using run filters"
1804 )]
1805 fn parse_scripts_invalid_list_using_run_filters(
1806 config_contents: &str,
1807 expected_errors: Vec<ProfileListScriptUsesRunFiltersError>,
1808 expected_known_scripts: &[&str],
1809 ) {
1810 let workspace_dir = tempdir().unwrap();
1811
1812 let graph = temp_workspace(&workspace_dir, config_contents);
1813
1814 let pcx = ParseContext::new(&graph);
1815
1816 let error = NextestConfig::from_sources(
1817 graph.workspace().root(),
1818 &pcx,
1819 None,
1820 &[][..],
1821 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1822 )
1823 .expect_err("config is invalid");
1824 match error.kind() {
1825 ConfigParseErrorKind::ProfileScriptErrors {
1826 errors,
1827 known_scripts,
1828 } => {
1829 let ProfileScriptErrors {
1830 unknown_scripts,
1831 wrong_script_types,
1832 list_scripts_using_run_filters,
1833 } = &**errors;
1834 assert_eq!(unknown_scripts.len(), 0, "no unknown scripts");
1835 assert_eq!(wrong_script_types.len(), 0, "no wrong script types");
1836 assert_eq!(
1837 list_scripts_using_run_filters.len(),
1838 expected_errors.len(),
1839 "correct number of errors"
1840 );
1841 for (error, expected_error) in
1842 list_scripts_using_run_filters.iter().zip(expected_errors)
1843 {
1844 assert_eq!(error, &expected_error, "error matches");
1845 }
1846 assert_eq!(
1847 known_scripts.len(),
1848 expected_known_scripts.len(),
1849 "correct number of known scripts"
1850 );
1851 for (script, expected_script) in known_scripts.iter().zip(expected_known_scripts) {
1852 assert_eq!(
1853 script.as_str(),
1854 *expected_script,
1855 "known script name matches"
1856 );
1857 }
1858 }
1859 other => {
1860 panic!(
1861 "for config error {other:?}, expected ConfigParseErrorKind::ProfileScriptErrors"
1862 );
1863 }
1864 }
1865 }
1866
1867 #[test]
1868 fn test_parse_scripts_empty_sections() {
1869 let config_contents = indoc! {r#"
1870 [scripts.setup.foo]
1871 command = 'echo foo'
1872
1873 [[profile.default.scripts]]
1874 platform = 'cfg(unix)'
1875
1876 [[profile.ci.scripts]]
1877 platform = 'cfg(unix)'
1878 "#};
1879
1880 let workspace_dir = tempdir().unwrap();
1881
1882 let graph = temp_workspace(&workspace_dir, config_contents);
1883
1884 let pcx = ParseContext::new(&graph);
1885
1886 let result = NextestConfig::from_sources(
1888 graph.workspace().root(),
1889 &pcx,
1890 None,
1891 &[][..],
1892 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1893 );
1894
1895 match result {
1896 Ok(_config) => {
1897 }
1900 Err(e) => {
1901 panic!("Config should be valid but got error: {e:?}");
1902 }
1903 }
1904 }
1905}