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