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