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,
665
666 #[serde(
668 default,
669 deserialize_with = "crate::config::elements::deserialize_slow_timeout"
670 )]
671 pub slow_timeout: Option<SlowTimeout>,
672
673 #[serde(
675 default,
676 deserialize_with = "crate::config::elements::deserialize_leak_timeout"
677 )]
678 pub leak_timeout: Option<LeakTimeout>,
679
680 #[serde(default)]
682 pub capture_stdout: bool,
683
684 #[serde(default)]
686 pub capture_stderr: bool,
687
688 #[serde(default)]
690 pub junit: SetupScriptJunitConfig,
691}
692
693impl SetupScriptConfig {
694 #[inline]
696 pub fn no_capture(&self) -> bool {
697 !(self.capture_stdout && self.capture_stderr)
698 }
699}
700
701#[derive(Copy, Clone, Debug, Deserialize)]
703#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
704#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
705#[serde(rename_all = "kebab-case")]
706pub struct SetupScriptJunitConfig {
707 #[serde(default = "default_true")]
711 pub store_success_output: bool,
712
713 #[serde(default = "default_true")]
717 pub store_failure_output: bool,
718}
719
720impl Default for SetupScriptJunitConfig {
721 fn default() -> Self {
722 Self {
723 store_success_output: true,
724 store_failure_output: true,
725 }
726 }
727}
728
729#[derive(Clone, Debug, Deserialize)]
733#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
734#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
735#[serde(rename_all = "kebab-case")]
736pub struct WrapperScriptConfig {
737 pub command: ScriptCommand,
739
740 #[serde(default)]
743 pub target_runner: WrapperScriptTargetRunner,
744}
745
746#[derive(Clone, Debug, Default)]
748#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
749#[cfg_attr(feature = "config-schema", schemars(rename_all = "kebab-case"))]
750pub enum WrapperScriptTargetRunner {
751 #[default]
753 Ignore,
754
755 OverridesWrapper,
757
758 WithinWrapper,
761
762 AroundWrapper,
765}
766
767impl<'de> Deserialize<'de> for WrapperScriptTargetRunner {
768 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
769 where
770 D: serde::Deserializer<'de>,
771 {
772 let s = String::deserialize(deserializer)?;
773 match s.as_str() {
774 "ignore" => Ok(WrapperScriptTargetRunner::Ignore),
775 "overrides-wrapper" => Ok(WrapperScriptTargetRunner::OverridesWrapper),
776 "within-wrapper" => Ok(WrapperScriptTargetRunner::WithinWrapper),
777 "around-wrapper" => Ok(WrapperScriptTargetRunner::AroundWrapper),
778 _ => Err(serde::de::Error::unknown_variant(
779 &s,
780 &[
781 "ignore",
782 "overrides-wrapper",
783 "within-wrapper",
784 "around-wrapper",
785 ],
786 )),
787 }
788 }
789}
790
791fn default_true() -> bool {
792 true
793}
794
795fn deserialize_script_ids<'de, D>(deserializer: D) -> Result<Vec<ScriptId>, D::Error>
796where
797 D: serde::Deserializer<'de>,
798{
799 struct ScriptIdVisitor;
800
801 impl<'de> serde::de::Visitor<'de> for ScriptIdVisitor {
802 type Value = Vec<ScriptId>;
803
804 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
805 formatter.write_str("a script ID (string) or a list of script IDs")
806 }
807
808 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
809 where
810 E: serde::de::Error,
811 {
812 Ok(vec![ScriptId::new(value.into()).map_err(E::custom)?])
813 }
814
815 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
816 where
817 A: serde::de::SeqAccess<'de>,
818 {
819 let mut ids = Vec::new();
820 while let Some(value) = seq.next_element::<String>()? {
821 ids.push(ScriptId::new(value.into()).map_err(A::Error::custom)?);
822 }
823 Ok(ids)
824 }
825 }
826
827 deserializer.deserialize_any(ScriptIdVisitor)
828}
829
830#[derive(Clone, Debug)]
832pub struct ScriptCommand {
833 pub program: String,
835
836 pub args: Vec<String>,
838
839 pub env: ScriptCommandEnvMap,
841
842 pub relative_to: ScriptCommandRelativeTo,
847}
848
849impl ScriptCommand {
850 pub fn program(&self, workspace_root: &Utf8Path, target_dir: &Utf8Path) -> String {
852 match self.relative_to {
853 ScriptCommandRelativeTo::None => self.program.clone(),
854 ScriptCommandRelativeTo::WorkspaceRoot => {
855 let path = Utf8Path::new(&self.program);
857 if path.is_relative() {
858 workspace_root
859 .join(convert_rel_path_to_main_sep(path))
860 .to_string()
861 } else {
862 path.to_string()
863 }
864 }
865 ScriptCommandRelativeTo::Target => {
866 let path = Utf8Path::new(&self.program);
868 if path.is_relative() {
869 target_dir
870 .join(convert_rel_path_to_main_sep(path))
871 .to_string()
872 } else {
873 path.to_string()
874 }
875 }
876 }
877 }
878}
879
880impl<'de> Deserialize<'de> for ScriptCommand {
881 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
882 where
883 D: serde::Deserializer<'de>,
884 {
885 struct CommandVisitor;
886
887 impl<'de> serde::de::Visitor<'de> for CommandVisitor {
888 type Value = ScriptCommand;
889
890 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
891 formatter.write_str("a Unix shell command, a list of arguments, or a table with command-line, env, and relative-to")
892 }
893
894 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
895 where
896 E: serde::de::Error,
897 {
898 let mut args = shell_words::split(value).map_err(E::custom)?;
899 if args.is_empty() {
900 return Err(E::invalid_value(serde::de::Unexpected::Str(value), &self));
901 }
902 let program = args.remove(0);
903 Ok(ScriptCommand {
904 program,
905 args,
906 env: ScriptCommandEnvMap::default(),
907 relative_to: ScriptCommandRelativeTo::None,
908 })
909 }
910
911 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
912 where
913 A: serde::de::SeqAccess<'de>,
914 {
915 let Some(program) = seq.next_element::<String>()? else {
916 return Err(A::Error::invalid_length(0, &self));
917 };
918 let mut args = Vec::new();
919 while let Some(value) = seq.next_element::<String>()? {
920 args.push(value);
921 }
922 Ok(ScriptCommand {
923 program,
924 args,
925 env: ScriptCommandEnvMap::default(),
926 relative_to: ScriptCommandRelativeTo::None,
927 })
928 }
929
930 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
931 where
932 A: serde::de::MapAccess<'de>,
933 {
934 let mut command_line = None;
935 let mut relative_to = None;
936 let mut env = None;
937
938 while let Some(key) = map.next_key::<String>()? {
939 match key.as_str() {
940 "command-line" => {
941 if command_line.is_some() {
942 return Err(A::Error::duplicate_field("command-line"));
943 }
944 command_line = Some(map.next_value_seed(CommandInnerSeed)?);
945 }
946 "relative-to" => {
947 if relative_to.is_some() {
948 return Err(A::Error::duplicate_field("relative-to"));
949 }
950 relative_to = Some(map.next_value::<ScriptCommandRelativeTo>()?);
951 }
952 "env" => {
953 if env.is_some() {
954 return Err(A::Error::duplicate_field("env"));
955 }
956 env = Some(map.next_value::<ScriptCommandEnvMap>()?);
957 }
958 _ => {
959 return Err(A::Error::unknown_field(
960 &key,
961 &["command-line", "env", "relative-to"],
962 ));
963 }
964 }
965 }
966
967 let (program, arguments) =
968 command_line.ok_or_else(|| A::Error::missing_field("command-line"))?;
969 let env = env.unwrap_or_default();
970 let relative_to = relative_to.unwrap_or(ScriptCommandRelativeTo::None);
971
972 Ok(ScriptCommand {
973 program,
974 args: arguments,
975 env,
976 relative_to,
977 })
978 }
979 }
980
981 deserializer.deserialize_any(CommandVisitor)
982 }
983}
984
985#[cfg(feature = "config-schema")]
986impl schemars::JsonSchema for ScriptCommand {
987 fn schema_name() -> std::borrow::Cow<'static, str> {
988 "ScriptCommand".into()
989 }
990
991 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
992 fn non_empty_string_array_schema(
993 generator: &mut schemars::SchemaGenerator,
994 ) -> schemars::Schema {
995 schemars::json_schema!({
996 "type": "array",
997 "items": generator.subschema_for::<String>(),
998 "minItems": 1,
999 })
1000 }
1001
1002 schemars::json_schema!({
1003 "title": "ScriptCommand",
1004 "oneOf": [
1005 generator.subschema_for::<String>(),
1006 non_empty_string_array_schema(generator),
1007 {
1008 "type": "object",
1009 "properties": {
1010 "command-line": {
1011 "oneOf": [
1012 generator.subschema_for::<String>(),
1013 non_empty_string_array_schema(generator),
1014 ]
1015 },
1016 "env": generator.subschema_for::<std::collections::BTreeMap<String, String>>(),
1017 "relative-to": generator.subschema_for::<ScriptCommandRelativeTo>(),
1018 },
1019 "required": ["command-line"],
1020 "additionalProperties": false,
1021 }
1022 ]
1023 })
1024 }
1025}
1026
1027struct CommandInnerSeed;
1028
1029impl<'de> serde::de::DeserializeSeed<'de> for CommandInnerSeed {
1030 type Value = (String, Vec<String>);
1031
1032 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1033 where
1034 D: serde::Deserializer<'de>,
1035 {
1036 struct CommandInnerVisitor;
1037
1038 impl<'de> serde::de::Visitor<'de> for CommandInnerVisitor {
1039 type Value = (String, Vec<String>);
1040
1041 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1042 formatter.write_str("a string or array of strings")
1043 }
1044
1045 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1046 where
1047 E: serde::de::Error,
1048 {
1049 let mut args = shell_words::split(value).map_err(E::custom)?;
1050 if args.is_empty() {
1051 return Err(E::invalid_value(
1052 serde::de::Unexpected::Str(value),
1053 &"a non-empty command string",
1054 ));
1055 }
1056 let program = args.remove(0);
1057 Ok((program, args))
1058 }
1059
1060 fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
1061 where
1062 S: serde::de::SeqAccess<'de>,
1063 {
1064 let mut args = Vec::new();
1065 while let Some(value) = seq.next_element::<String>()? {
1066 args.push(value);
1067 }
1068 if args.is_empty() {
1069 return Err(S::Error::invalid_length(0, &self));
1070 }
1071 let program = args.remove(0);
1072 Ok((program, args))
1073 }
1074 }
1075
1076 deserializer.deserialize_any(CommandInnerVisitor)
1077 }
1078}
1079
1080#[derive(Clone, Copy, Debug)]
1085#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1086#[cfg_attr(feature = "config-schema", schemars(rename_all = "kebab-case"))]
1087pub enum ScriptCommandRelativeTo {
1088 None,
1090
1091 WorkspaceRoot,
1093
1094 Target,
1096 }
1098
1099impl<'de> Deserialize<'de> for ScriptCommandRelativeTo {
1100 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1101 where
1102 D: serde::Deserializer<'de>,
1103 {
1104 let s = String::deserialize(deserializer)?;
1105 match s.as_str() {
1106 "none" => Ok(ScriptCommandRelativeTo::None),
1107 "workspace-root" => Ok(ScriptCommandRelativeTo::WorkspaceRoot),
1108 "target" => Ok(ScriptCommandRelativeTo::Target),
1109 _ => Err(serde::de::Error::unknown_variant(&s, &["none", "target"])),
1110 }
1111 }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::*;
1117 use crate::{
1118 config::{
1119 core::{ConfigExperimental, NextestConfig, ToolConfigFile, ToolName},
1120 utils::test_helpers::*,
1121 },
1122 errors::{
1123 ConfigParseErrorKind, DisplayErrorChain, ProfileListScriptUsesRunFiltersError,
1124 ProfileScriptErrors, ProfileUnknownScriptError, ProfileWrongConfigScriptTypeError,
1125 },
1126 };
1127 use camino_tempfile::tempdir;
1128 use camino_tempfile_ext::prelude::*;
1129 use indoc::indoc;
1130 use maplit::btreeset;
1131 use nextest_metadata::TestCaseName;
1132 use test_case::test_case;
1133
1134 fn tool_name(s: &str) -> ToolName {
1135 ToolName::new(s.into()).unwrap()
1136 }
1137
1138 #[test]
1139 fn test_scripts_basic() {
1140 let config_contents = indoc! {r#"
1141 [[profile.default.scripts]]
1142 platform = { host = "x86_64-unknown-linux-gnu" }
1143 filter = "test(script1)"
1144 setup = ["foo", "bar"]
1145
1146 [[profile.default.scripts]]
1147 platform = { target = "aarch64-apple-darwin" }
1148 filter = "test(script2)"
1149 setup = "baz"
1150
1151 [[profile.default.scripts]]
1152 filter = "test(script3)"
1153 # No matter which order scripts are specified here, they must always be run in the
1154 # order defined below.
1155 setup = ["baz", "foo", "@tool:my-tool:toolscript"]
1156
1157 [[profile.default.scripts]]
1158 filter = "test(script4)"
1159 setup = "qux"
1160
1161 [scripts.setup.foo]
1162 command = "command foo"
1163
1164 [scripts.setup.bar]
1165 command = ["cargo", "run", "-p", "bar"]
1166 slow-timeout = { period = "60s", terminate-after = 2 }
1167
1168 [scripts.setup.baz]
1169 command = "baz"
1170 slow-timeout = "1s"
1171 leak-timeout = "1s"
1172 capture-stdout = true
1173 capture-stderr = true
1174
1175 [scripts.setup.qux]
1176 command = {
1177 command-line = "qux",
1178 env = {
1179 MODE = "qux_mode",
1180 },
1181 }
1182 "#
1183 };
1184
1185 let tool_config_contents = indoc! {r#"
1186 [scripts.setup.'@tool:my-tool:toolscript']
1187 command = "tool-command"
1188 "#
1189 };
1190
1191 let workspace_dir = tempdir().unwrap();
1192
1193 let graph = temp_workspace(&workspace_dir, config_contents);
1194 let tool_path = workspace_dir.child(".config/my-tool.toml");
1195 tool_path.write_str(tool_config_contents).unwrap();
1196
1197 let package_id = graph.workspace().iter().next().unwrap().id();
1198
1199 let pcx = ParseContext::new(&graph);
1200
1201 let tool_config_files = [ToolConfigFile {
1202 tool: tool_name("my-tool"),
1203 config_file: tool_path.to_path_buf(),
1204 }];
1205
1206 let nextest_config_error = NextestConfig::from_sources(
1208 graph.workspace().root(),
1209 &pcx,
1210 None,
1211 &tool_config_files,
1212 &Default::default(),
1213 )
1214 .unwrap_err();
1215 match nextest_config_error.kind() {
1216 ConfigParseErrorKind::ExperimentalFeaturesNotEnabled { missing_features } => {
1217 assert_eq!(
1218 *missing_features,
1219 btreeset! { ConfigExperimental::SetupScripts }
1220 );
1221 }
1222 other => panic!("unexpected error kind: {other:?}"),
1223 }
1224
1225 let nextest_config_result = NextestConfig::from_sources(
1227 graph.workspace().root(),
1228 &pcx,
1229 None,
1230 &tool_config_files,
1231 &btreeset! { ConfigExperimental::SetupScripts },
1232 )
1233 .expect("config is valid");
1234 let profile = nextest_config_result
1235 .profile("default")
1236 .expect("valid profile name")
1237 .apply_build_platforms(&build_platforms());
1238
1239 let host_binary_query =
1241 binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
1242 let test_name = TestCaseName::new("script1");
1243 let query = TestQuery {
1244 binary_query: host_binary_query.to_query(),
1245 test_name: &test_name,
1246 };
1247 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1248 assert_eq!(scripts.len(), 2, "two scripts should be enabled");
1249 assert_eq!(
1250 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1251 "foo",
1252 "first script should be foo"
1253 );
1254 assert_eq!(
1255 scripts.enabled_scripts.get_index(1).unwrap().0.as_str(),
1256 "bar",
1257 "second script should be bar"
1258 );
1259
1260 let target_binary_query = binary_query(
1261 &graph,
1262 package_id,
1263 "lib",
1264 "my-binary",
1265 BuildPlatform::Target,
1266 );
1267
1268 let test_name = TestCaseName::new("script2");
1270 let query = TestQuery {
1271 binary_query: target_binary_query.to_query(),
1272 test_name: &test_name,
1273 };
1274 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1275 assert_eq!(scripts.len(), 1, "one script should be enabled");
1276 assert_eq!(
1277 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1278 "baz",
1279 "first script should be baz"
1280 );
1281
1282 let test_name = TestCaseName::new("script3");
1284 let query = TestQuery {
1285 binary_query: target_binary_query.to_query(),
1286 test_name: &test_name,
1287 };
1288 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1289 assert_eq!(scripts.len(), 3, "three scripts should be enabled");
1290 assert_eq!(
1291 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1292 "@tool:my-tool:toolscript",
1293 "first script should be toolscript"
1294 );
1295 assert_eq!(
1296 scripts.enabled_scripts.get_index(1).unwrap().0.as_str(),
1297 "foo",
1298 "second script should be foo"
1299 );
1300 assert_eq!(
1301 scripts.enabled_scripts.get_index(2).unwrap().0.as_str(),
1302 "baz",
1303 "third script should be baz"
1304 );
1305
1306 let test_name = TestCaseName::new("script4");
1308 let query = TestQuery {
1309 binary_query: target_binary_query.to_query(),
1310 test_name: &test_name,
1311 };
1312 let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
1313 assert_eq!(scripts.len(), 1, "one script should be enabled");
1314 assert_eq!(
1315 scripts.enabled_scripts.get_index(0).unwrap().0.as_str(),
1316 "qux",
1317 "first script should be qux"
1318 );
1319 assert_eq!(
1320 scripts
1321 .enabled_scripts
1322 .get_index(0)
1323 .unwrap()
1324 .1
1325 .config
1326 .command
1327 .env
1328 .get("MODE"),
1329 Some("qux_mode"),
1330 "first script should be passed environment variable MODE with value qux_mode",
1331 );
1332 }
1333
1334 #[test_case(
1335 indoc! {r#"
1336 [scripts.setup.foo]
1337 command = ""
1338 "#},
1339 "invalid value: string \"\", expected a Unix shell command, a list of arguments, \
1340 or a table with command-line, env, and relative-to"
1341
1342 ; "empty command"
1343 )]
1344 #[test_case(
1345 indoc! {r#"
1346 [scripts.setup.foo]
1347 command = []
1348 "#},
1349 "invalid length 0, expected a Unix shell command, a list of arguments, \
1350 or a table with command-line, env, and relative-to"
1351
1352 ; "empty command list"
1353 )]
1354 #[test_case(
1355 indoc! {r#"
1356 [scripts.setup.foo]
1357 "#},
1358 r#"scripts.setup.foo: missing configuration field "scripts.setup.foo.command""#
1359
1360 ; "missing command"
1361 )]
1362 #[test_case(
1363 indoc! {r#"
1364 [scripts.setup.foo]
1365 command = { command-line = "" }
1366 "#},
1367 "invalid value: string \"\", expected a non-empty command string"
1368
1369 ; "empty command-line in table"
1370 )]
1371 #[test_case(
1372 indoc! {r#"
1373 [scripts.setup.foo]
1374 command = { command-line = [] }
1375 "#},
1376 "invalid length 0, expected a string or array of strings"
1377
1378 ; "empty command-line array in table"
1379 )]
1380 #[test_case(
1381 indoc! {r#"
1382 [scripts.setup.foo]
1383 command = {
1384 command_line = "hi",
1385 command_line = ["hi"],
1386 }
1387 "#},
1388 r#"duplicate key"#
1389
1390 ; "command line is duplicate"
1391 )]
1392 #[test_case(
1393 indoc! {r#"
1394 [scripts.setup.foo]
1395 command = { relative-to = "target" }
1396 "#},
1397 r#"missing configuration field "scripts.setup.foo.command.command-line""#
1398
1399 ; "missing command-line in table"
1400 )]
1401 #[test_case(
1402 indoc! {r#"
1403 [scripts.setup.foo]
1404 command = { command-line = "my-command", relative-to = "invalid" }
1405 "#},
1406 r#"unknown variant `invalid`, expected `none` or `target`"#
1407
1408 ; "invalid relative-to value"
1409 )]
1410 #[test_case(
1411 indoc! {r#"
1412 [scripts.setup.foo]
1413 command = {
1414 relative-to = "none",
1415 relative-to = "target",
1416 }
1417 "#},
1418 r#"duplicate key"#
1419
1420 ; "relative to is duplicate"
1421 )]
1422 #[test_case(
1423 indoc! {r#"
1424 [scripts.setup.foo]
1425 command = { command-line = "my-command", unknown-field = "value" }
1426 "#},
1427 r#"unknown field `unknown-field`, expected one of `command-line`, `env`, `relative-to`"#
1428
1429 ; "unknown field in command table"
1430 )]
1431 #[test_case(
1432 indoc! {r#"
1433 [scripts.setup.foo]
1434 command = "my-command"
1435 slow-timeout = 34
1436 "#},
1437 r#"invalid type: integer `34`, expected a table ({ period = "60s", terminate-after = 2 }) or a string ("60s")"#
1438
1439 ; "slow timeout is not a duration"
1440 )]
1441 #[test_case(
1442 indoc! {r#"
1443 [scripts.setup.'@tool:foo']
1444 command = "my-command"
1445 "#},
1446 r#"invalid configuration script name: tool identifier not of the form "@tool:tool-name:identifier": `@tool:foo`"#
1447
1448 ; "invalid tool script name"
1449 )]
1450 #[test_case(
1451 indoc! {r#"
1452 [scripts.setup.'#foo']
1453 command = "my-command"
1454 "#},
1455 r"invalid configuration script name: invalid identifier `#foo`"
1456
1457 ; "invalid script name"
1458 )]
1459 #[test_case(
1460 indoc! {r#"
1461 [scripts.wrapper.foo]
1462 command = "my-command"
1463 target-runner = "not-a-valid-value"
1464 "#},
1465 r#"unknown variant `not-a-valid-value`, expected one of `ignore`, `overrides-wrapper`, `within-wrapper`, `around-wrapper`"#
1466
1467 ; "invalid target-runner value"
1468 )]
1469 #[test_case(
1470 indoc! {r#"
1471 [scripts.wrapper.foo]
1472 command = "my-command"
1473 target-runner = ["foo"]
1474 "#},
1475 r#"invalid type: sequence, expected a string"#
1476
1477 ; "target-runner is not a string"
1478 )]
1479 #[test_case(
1480 indoc! {r#"
1481 [scripts.setup.foo]
1482 command = {
1483 env = {},
1484 env = {},
1485 }
1486 "#},
1487 r#"duplicate key"#
1488
1489 ; "env is duplicate"
1490 )]
1491 #[test_case(
1492 indoc! {r#"
1493 [scripts.setup.foo]
1494 command = {
1495 command-line = "my-command",
1496 env = "not a map"
1497 }
1498 "#},
1499 r#"scripts.setup.foo.command.env: invalid type: string "not a map", expected a map of environment variable names to values"#
1500
1501 ; "env is not a map"
1502 )]
1503 #[test_case(
1504 indoc! {r#"
1505 [scripts.setup.foo]
1506 command = {
1507 command-line = "my-command",
1508 env = {
1509 NEXTEST_RESERVED = "reserved",
1510 },
1511 }
1512 "#},
1513 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"#
1514
1515 ; "env containing key reserved for internal use"
1516 )]
1517 #[test_case(
1518 indoc! {r#"
1519 [scripts.setup.foo]
1520 command = {
1521 command-line = "my-command",
1522 env = {
1523 42 = "answer",
1524 },
1525 }
1526 "#},
1527 r#"scripts.setup.foo.command.env: invalid value: string "42", expected a key that starts with a letter or underscore"#
1528
1529 ; "env containing key first character a digit"
1530 )]
1531 #[test_case(
1532 indoc! {r#"
1533 [scripts.setup.foo]
1534 command = {
1535 command-line = "my-command",
1536 env = {
1537 " " = "some value",
1538 },
1539 }
1540 "#},
1541 r#"scripts.setup.foo.command.env: invalid value: string " ", expected a key that starts with a letter or underscore"#
1542
1543 ; "env containing key started with an unsupported characters"
1544 )]
1545 #[test_case(
1546 indoc! {r#"
1547 [scripts.setup.foo]
1548 command = {
1549 command-line = "my-command",
1550 env = {
1551 "test=test" = "some value",
1552 },
1553 }
1554 "#},
1555 r#"scripts.setup.foo.command.env: invalid value: string "test=test", expected a key that consists solely of letters, digits, and underscores"#
1556
1557 ; "env containing key with unsupported characters"
1558 )]
1559 fn parse_scripts_invalid_deserialize(config_contents: &str, message: &str) {
1560 let workspace_dir = tempdir().unwrap();
1561
1562 let graph = temp_workspace(&workspace_dir, config_contents);
1563 let pcx = ParseContext::new(&graph);
1564
1565 let nextest_config_error = NextestConfig::from_sources(
1566 graph.workspace().root(),
1567 &pcx,
1568 None,
1569 &[][..],
1570 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1571 )
1572 .expect_err("config is invalid");
1573 let actual_message = DisplayErrorChain::new(nextest_config_error).to_string();
1574
1575 assert!(
1576 actual_message.contains(message),
1577 "nextest config error `{actual_message}` contains message `{message}`"
1578 );
1579 }
1580
1581 #[test_case(
1582 indoc! {r#"
1583 [scripts.setup.foo]
1584 command = "my-command"
1585
1586 [[profile.default.scripts]]
1587 setup = ["foo"]
1588 "#},
1589 "default",
1590 &[MietteJsonReport {
1591 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1592 labels: vec![],
1593 }]
1594
1595 ; "neither platform nor filter specified"
1596 )]
1597 #[test_case(
1598 indoc! {r#"
1599 [scripts.setup.foo]
1600 command = "my-command"
1601
1602 [[profile.default.scripts]]
1603 platform = {}
1604 setup = ["foo"]
1605 "#},
1606 "default",
1607 &[MietteJsonReport {
1608 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1609 labels: vec![],
1610 }]
1611
1612 ; "empty platform map"
1613 )]
1614 #[test_case(
1615 indoc! {r#"
1616 [scripts.setup.foo]
1617 command = "my-command"
1618
1619 [[profile.default.scripts]]
1620 platform = { host = 'cfg(target_os = "linux' }
1621 setup = ["foo"]
1622 "#},
1623 "default",
1624 &[MietteJsonReport {
1625 message: "error parsing cfg() expression".to_owned(),
1626 labels: vec![
1627 MietteJsonLabel { label: "expected one of `=`, `,`, `)` here".to_owned(), span: MietteJsonSpan { offset: 3, length: 1 } }
1628 ]
1629 }]
1630
1631 ; "invalid platform expression"
1632 )]
1633 #[test_case(
1634 indoc! {r#"
1635 [scripts.setup.foo]
1636 command = "my-command"
1637
1638 [[profile.ci.overrides]]
1639 filter = 'test(/foo)'
1640 setup = ["foo"]
1641 "#},
1642 "ci",
1643 &[MietteJsonReport {
1644 message: "expected close regex".to_owned(),
1645 labels: vec![
1646 MietteJsonLabel { label: "missing `/`".to_owned(), span: MietteJsonSpan { offset: 9, length: 0 } }
1647 ]
1648 }]
1649
1650 ; "invalid filterset"
1651 )]
1652 fn parse_scripts_invalid_compile(
1653 config_contents: &str,
1654 faulty_profile: &str,
1655 expected_reports: &[MietteJsonReport],
1656 ) {
1657 let workspace_dir = tempdir().unwrap();
1658
1659 let graph = temp_workspace(&workspace_dir, config_contents);
1660
1661 let pcx = ParseContext::new(&graph);
1662
1663 let error = NextestConfig::from_sources(
1664 graph.workspace().root(),
1665 &pcx,
1666 None,
1667 &[][..],
1668 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1669 )
1670 .expect_err("config is invalid");
1671 match error.kind() {
1672 ConfigParseErrorKind::CompileErrors(compile_errors) => {
1673 assert_eq!(
1674 compile_errors.len(),
1675 1,
1676 "exactly one override error must be produced"
1677 );
1678 let error = compile_errors.first().unwrap();
1679 assert_eq!(
1680 error.profile_name, faulty_profile,
1681 "compile error profile matches"
1682 );
1683 let handler = miette::JSONReportHandler::new();
1684 let reports = error
1685 .kind
1686 .reports()
1687 .map(|report| {
1688 let mut out = String::new();
1689 handler.render_report(&mut out, report.as_ref()).unwrap();
1690
1691 let json_report: MietteJsonReport = serde_json::from_str(&out)
1692 .unwrap_or_else(|err| {
1693 panic!(
1694 "failed to deserialize JSON message produced by miette: {err}"
1695 )
1696 });
1697 json_report
1698 })
1699 .collect::<Vec<_>>();
1700 assert_eq!(&reports, expected_reports, "reports match");
1701 }
1702 other => {
1703 panic!(
1704 "for config error {other:?}, expected ConfigParseErrorKind::CompiledDataParseError"
1705 );
1706 }
1707 }
1708 }
1709
1710 #[test_case(
1711 indoc! {r#"
1712 [scripts.setup.'@tool:foo:bar']
1713 command = "my-command"
1714
1715 [[profile.ci.overrides]]
1716 setup = ["@tool:foo:bar"]
1717 "#},
1718 &["@tool:foo:bar"]
1719
1720 ; "tool config in main program")]
1721 fn parse_scripts_invalid_defined(config_contents: &str, expected_invalid_scripts: &[&str]) {
1722 let workspace_dir = tempdir().unwrap();
1723
1724 let graph = temp_workspace(&workspace_dir, config_contents);
1725
1726 let pcx = ParseContext::new(&graph);
1727
1728 let error = NextestConfig::from_sources(
1729 graph.workspace().root(),
1730 &pcx,
1731 None,
1732 &[][..],
1733 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1734 )
1735 .expect_err("config is invalid");
1736 match error.kind() {
1737 ConfigParseErrorKind::InvalidConfigScriptsDefined(scripts) => {
1738 assert_eq!(
1739 scripts.len(),
1740 expected_invalid_scripts.len(),
1741 "correct number of scripts defined"
1742 );
1743 for (script, expected_script) in scripts.iter().zip(expected_invalid_scripts) {
1744 assert_eq!(script.as_str(), *expected_script, "script name matches");
1745 }
1746 }
1747 other => {
1748 panic!(
1749 "for config error {other:?}, expected ConfigParseErrorKind::InvalidConfigScriptsDefined"
1750 );
1751 }
1752 }
1753 }
1754
1755 #[test_case(
1756 indoc! {r#"
1757 [scripts.setup.'blarg']
1758 command = "my-command"
1759
1760 [[profile.ci.overrides]]
1761 setup = ["blarg"]
1762 "#},
1763 &["blarg"]
1764
1765 ; "non-tool config in tool")]
1766 fn parse_scripts_invalid_defined_by_tool(
1767 tool_config_contents: &str,
1768 expected_invalid_scripts: &[&str],
1769 ) {
1770 let workspace_dir = tempdir().unwrap();
1771 let graph = temp_workspace(&workspace_dir, "");
1772
1773 let tool_path = workspace_dir.child(".config/my-tool.toml");
1774 tool_path.write_str(tool_config_contents).unwrap();
1775 let tool_config_files = [ToolConfigFile {
1776 tool: tool_name("my-tool"),
1777 config_file: tool_path.to_path_buf(),
1778 }];
1779
1780 let pcx = ParseContext::new(&graph);
1781
1782 let error = NextestConfig::from_sources(
1783 graph.workspace().root(),
1784 &pcx,
1785 None,
1786 &tool_config_files,
1787 &btreeset! { ConfigExperimental::SetupScripts },
1788 )
1789 .expect_err("config is invalid");
1790 match error.kind() {
1791 ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool(scripts) => {
1792 assert_eq!(
1793 scripts.len(),
1794 expected_invalid_scripts.len(),
1795 "exactly one script must be defined"
1796 );
1797 for (script, expected_script) in scripts.iter().zip(expected_invalid_scripts) {
1798 assert_eq!(script.as_str(), *expected_script, "script name matches");
1799 }
1800 }
1801 other => {
1802 panic!(
1803 "for config error {other:?}, expected ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool"
1804 );
1805 }
1806 }
1807 }
1808
1809 #[test_case(
1810 indoc! {r#"
1811 [scripts.setup.foo]
1812 command = 'echo foo'
1813
1814 [[profile.default.scripts]]
1815 platform = 'cfg(unix)'
1816 setup = ['bar']
1817
1818 [[profile.ci.scripts]]
1819 platform = 'cfg(unix)'
1820 setup = ['baz']
1821 "#},
1822 vec![
1823 ProfileUnknownScriptError {
1824 profile_name: "default".to_owned(),
1825 name: ScriptId::new("bar".into()).unwrap(),
1826 },
1827 ProfileUnknownScriptError {
1828 profile_name: "ci".to_owned(),
1829 name: ScriptId::new("baz".into()).unwrap(),
1830 },
1831 ],
1832 &["foo"]
1833
1834 ; "unknown scripts"
1835 )]
1836 fn parse_scripts_invalid_unknown(
1837 config_contents: &str,
1838 expected_errors: Vec<ProfileUnknownScriptError>,
1839 expected_known_scripts: &[&str],
1840 ) {
1841 let workspace_dir = tempdir().unwrap();
1842
1843 let graph = temp_workspace(&workspace_dir, config_contents);
1844
1845 let pcx = ParseContext::new(&graph);
1846
1847 let error = NextestConfig::from_sources(
1848 graph.workspace().root(),
1849 &pcx,
1850 None,
1851 &[][..],
1852 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1853 )
1854 .expect_err("config is invalid");
1855 match error.kind() {
1856 ConfigParseErrorKind::ProfileScriptErrors {
1857 errors,
1858 known_scripts,
1859 } => {
1860 let ProfileScriptErrors {
1861 unknown_scripts,
1862 wrong_script_types,
1863 list_scripts_using_run_filters,
1864 } = &**errors;
1865 assert_eq!(wrong_script_types.len(), 0, "no wrong script types");
1866 assert_eq!(
1867 list_scripts_using_run_filters.len(),
1868 0,
1869 "no scripts using run filters in list phase"
1870 );
1871 assert_eq!(
1872 unknown_scripts.len(),
1873 expected_errors.len(),
1874 "correct number of errors"
1875 );
1876 for (error, expected_error) in unknown_scripts.iter().zip(expected_errors) {
1877 assert_eq!(error, &expected_error, "error matches");
1878 }
1879 assert_eq!(
1880 known_scripts.len(),
1881 expected_known_scripts.len(),
1882 "correct number of known scripts"
1883 );
1884 for (script, expected_script) in known_scripts.iter().zip(expected_known_scripts) {
1885 assert_eq!(
1886 script.as_str(),
1887 *expected_script,
1888 "known script name matches"
1889 );
1890 }
1891 }
1892 other => {
1893 panic!(
1894 "for config error {other:?}, expected ConfigParseErrorKind::ProfileScriptErrors"
1895 );
1896 }
1897 }
1898 }
1899
1900 #[test_case(
1901 indoc! {r#"
1902 [scripts.setup.setup-script]
1903 command = 'echo setup'
1904
1905 [scripts.wrapper.wrapper-script]
1906 command = 'echo wrapper'
1907
1908 [[profile.default.scripts]]
1909 platform = 'cfg(unix)'
1910 setup = ['wrapper-script']
1911 list-wrapper = 'setup-script'
1912
1913 [[profile.ci.scripts]]
1914 platform = 'cfg(unix)'
1915 setup = 'wrapper-script'
1916 run-wrapper = 'setup-script'
1917 "#},
1918 vec![
1919 ProfileWrongConfigScriptTypeError {
1920 profile_name: "default".to_owned(),
1921 name: ScriptId::new("wrapper-script".into()).unwrap(),
1922 attempted: ProfileScriptType::Setup,
1923 actual: ScriptType::Wrapper,
1924 },
1925 ProfileWrongConfigScriptTypeError {
1926 profile_name: "default".to_owned(),
1927 name: ScriptId::new("setup-script".into()).unwrap(),
1928 attempted: ProfileScriptType::ListWrapper,
1929 actual: ScriptType::Setup,
1930 },
1931 ProfileWrongConfigScriptTypeError {
1932 profile_name: "ci".to_owned(),
1933 name: ScriptId::new("wrapper-script".into()).unwrap(),
1934 attempted: ProfileScriptType::Setup,
1935 actual: ScriptType::Wrapper,
1936 },
1937 ProfileWrongConfigScriptTypeError {
1938 profile_name: "ci".to_owned(),
1939 name: ScriptId::new("setup-script".into()).unwrap(),
1940 attempted: ProfileScriptType::RunWrapper,
1941 actual: ScriptType::Setup,
1942 },
1943 ],
1944 &["setup-script", "wrapper-script"]
1945
1946 ; "wrong script types"
1947 )]
1948 fn parse_scripts_invalid_wrong_type(
1949 config_contents: &str,
1950 expected_errors: Vec<ProfileWrongConfigScriptTypeError>,
1951 expected_known_scripts: &[&str],
1952 ) {
1953 let workspace_dir = tempdir().unwrap();
1954
1955 let graph = temp_workspace(&workspace_dir, config_contents);
1956
1957 let pcx = ParseContext::new(&graph);
1958
1959 let error = NextestConfig::from_sources(
1960 graph.workspace().root(),
1961 &pcx,
1962 None,
1963 &[][..],
1964 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
1965 )
1966 .expect_err("config is invalid");
1967 match error.kind() {
1968 ConfigParseErrorKind::ProfileScriptErrors {
1969 errors,
1970 known_scripts,
1971 } => {
1972 let ProfileScriptErrors {
1973 unknown_scripts,
1974 wrong_script_types,
1975 list_scripts_using_run_filters,
1976 } = &**errors;
1977 assert_eq!(unknown_scripts.len(), 0, "no unknown scripts");
1978 assert_eq!(
1979 list_scripts_using_run_filters.len(),
1980 0,
1981 "no scripts using run filters in list phase"
1982 );
1983 assert_eq!(
1984 wrong_script_types.len(),
1985 expected_errors.len(),
1986 "correct number of errors"
1987 );
1988 for (error, expected_error) in wrong_script_types.iter().zip(expected_errors) {
1989 assert_eq!(error, &expected_error, "error matches");
1990 }
1991 assert_eq!(
1992 known_scripts.len(),
1993 expected_known_scripts.len(),
1994 "correct number of known scripts"
1995 );
1996 for (script, expected_script) in known_scripts.iter().zip(expected_known_scripts) {
1997 assert_eq!(
1998 script.as_str(),
1999 *expected_script,
2000 "known script name matches"
2001 );
2002 }
2003 }
2004 other => {
2005 panic!(
2006 "for config error {other:?}, expected ConfigParseErrorKind::ProfileScriptErrors"
2007 );
2008 }
2009 }
2010 }
2011
2012 #[test_case(
2013 indoc! {r#"
2014 [scripts.wrapper.list-script]
2015 command = 'echo list'
2016
2017 [[profile.default.scripts]]
2018 filter = 'test(hello)'
2019 list-wrapper = 'list-script'
2020
2021 [[profile.ci.scripts]]
2022 filter = 'test(world)'
2023 list-wrapper = 'list-script'
2024 "#},
2025 vec![
2026 ProfileListScriptUsesRunFiltersError {
2027 profile_name: "default".to_owned(),
2028 name: ScriptId::new("list-script".into()).unwrap(),
2029 script_type: ProfileScriptType::ListWrapper,
2030 filters: vec!["test(hello)".to_owned()].into_iter().collect(),
2031 },
2032 ProfileListScriptUsesRunFiltersError {
2033 profile_name: "ci".to_owned(),
2034 name: ScriptId::new("list-script".into()).unwrap(),
2035 script_type: ProfileScriptType::ListWrapper,
2036 filters: vec!["test(world)".to_owned()].into_iter().collect(),
2037 },
2038 ],
2039 &["list-script"]
2040
2041 ; "list scripts using run filters"
2042 )]
2043 fn parse_scripts_invalid_list_using_run_filters(
2044 config_contents: &str,
2045 expected_errors: Vec<ProfileListScriptUsesRunFiltersError>,
2046 expected_known_scripts: &[&str],
2047 ) {
2048 let workspace_dir = tempdir().unwrap();
2049
2050 let graph = temp_workspace(&workspace_dir, config_contents);
2051
2052 let pcx = ParseContext::new(&graph);
2053
2054 let error = NextestConfig::from_sources(
2055 graph.workspace().root(),
2056 &pcx,
2057 None,
2058 &[][..],
2059 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
2060 )
2061 .expect_err("config is invalid");
2062 match error.kind() {
2063 ConfigParseErrorKind::ProfileScriptErrors {
2064 errors,
2065 known_scripts,
2066 } => {
2067 let ProfileScriptErrors {
2068 unknown_scripts,
2069 wrong_script_types,
2070 list_scripts_using_run_filters,
2071 } = &**errors;
2072 assert_eq!(unknown_scripts.len(), 0, "no unknown scripts");
2073 assert_eq!(wrong_script_types.len(), 0, "no wrong script types");
2074 assert_eq!(
2075 list_scripts_using_run_filters.len(),
2076 expected_errors.len(),
2077 "correct number of errors"
2078 );
2079 for (error, expected_error) in
2080 list_scripts_using_run_filters.iter().zip(expected_errors)
2081 {
2082 assert_eq!(error, &expected_error, "error matches");
2083 }
2084 assert_eq!(
2085 known_scripts.len(),
2086 expected_known_scripts.len(),
2087 "correct number of known scripts"
2088 );
2089 for (script, expected_script) in known_scripts.iter().zip(expected_known_scripts) {
2090 assert_eq!(
2091 script.as_str(),
2092 *expected_script,
2093 "known script name matches"
2094 );
2095 }
2096 }
2097 other => {
2098 panic!(
2099 "for config error {other:?}, expected ConfigParseErrorKind::ProfileScriptErrors"
2100 );
2101 }
2102 }
2103 }
2104
2105 #[test]
2106 fn test_parse_scripts_empty_sections() {
2107 let config_contents = indoc! {r#"
2108 [scripts.setup.foo]
2109 command = 'echo foo'
2110
2111 [[profile.default.scripts]]
2112 platform = 'cfg(unix)'
2113
2114 [[profile.ci.scripts]]
2115 platform = 'cfg(unix)'
2116 "#};
2117
2118 let workspace_dir = tempdir().unwrap();
2119
2120 let graph = temp_workspace(&workspace_dir, config_contents);
2121
2122 let pcx = ParseContext::new(&graph);
2123
2124 let result = NextestConfig::from_sources(
2126 graph.workspace().root(),
2127 &pcx,
2128 None,
2129 &[][..],
2130 &btreeset! { ConfigExperimental::SetupScripts, ConfigExperimental::WrapperScripts },
2131 );
2132
2133 match result {
2134 Ok(_config) => {
2135 }
2138 Err(e) => {
2139 panic!("Config should be valid but got error: {e:?}");
2140 }
2141 }
2142 }
2143}