1use super::{DisplayFilterMatcher, TestListDisplayFilter};
5use crate::{
6 cargo_config::EnvironmentMap,
7 config::{
8 core::EvaluatableProfile,
9 overrides::{ListSettings, TestSettings},
10 scripts::{WrapperScriptConfig, WrapperScriptTargetRunner},
11 },
12 double_spawn::DoubleSpawnInfo,
13 errors::{CreateTestListError, FromMessagesError, WriteTestListError},
14 helpers::{convert_build_platform, dylib_path, dylib_path_envvar, write_test_name},
15 indenter::indented,
16 list::{BinaryList, OutputFormat, RustBuildMeta, Styles, TestListState},
17 reuse_build::PathMapper,
18 target_runner::{PlatformRunner, TargetRunner},
19 test_command::{LocalExecuteContext, TestCommand, TestCommandPhase},
20 test_filter::{BinaryMismatchReason, FilterBinaryMatch, FilterBound, TestFilterBuilder},
21 write_str::WriteStr,
22};
23use camino::{Utf8Path, Utf8PathBuf};
24use debug_ignore::DebugIgnore;
25use futures::prelude::*;
26use guppy::{
27 PackageId,
28 graph::{PackageGraph, PackageMetadata},
29};
30use nextest_filtering::{BinaryQuery, EvalContext, TestQuery};
31use nextest_metadata::{
32 BuildPlatform, FilterMatch, MismatchReason, RustBinaryId, RustNonTestBinaryKind,
33 RustTestBinaryKind, RustTestBinarySummary, RustTestCaseSummary, RustTestSuiteStatusSummary,
34 RustTestSuiteSummary, TestListSummary,
35};
36use owo_colors::OwoColorize;
37use std::{
38 borrow::Cow,
39 collections::{BTreeMap, BTreeSet},
40 ffi::{OsStr, OsString},
41 fmt, io,
42 path::PathBuf,
43 sync::{Arc, OnceLock},
44};
45use tokio::runtime::Runtime;
46use tracing::debug;
47
48#[derive(Clone, Debug)]
53pub struct RustTestArtifact<'g> {
54 pub binary_id: RustBinaryId,
56
57 pub package: PackageMetadata<'g>,
60
61 pub binary_path: Utf8PathBuf,
63
64 pub binary_name: String,
66
67 pub kind: RustTestBinaryKind,
69
70 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
72
73 pub cwd: Utf8PathBuf,
75
76 pub build_platform: BuildPlatform,
78}
79
80impl<'g> RustTestArtifact<'g> {
81 pub fn from_binary_list(
83 graph: &'g PackageGraph,
84 binary_list: Arc<BinaryList>,
85 rust_build_meta: &RustBuildMeta<TestListState>,
86 path_mapper: &PathMapper,
87 platform_filter: Option<BuildPlatform>,
88 ) -> Result<Vec<Self>, FromMessagesError> {
89 let mut binaries = vec![];
90
91 for binary in &binary_list.rust_binaries {
92 if platform_filter.is_some() && platform_filter != Some(binary.build_platform) {
93 continue;
94 }
95
96 let package_id = PackageId::new(binary.package_id.clone());
98 let package = graph
99 .metadata(&package_id)
100 .map_err(FromMessagesError::PackageGraph)?;
101
102 let cwd = package
104 .manifest_path()
105 .parent()
106 .unwrap_or_else(|| {
107 panic!(
108 "manifest path {} doesn't have a parent",
109 package.manifest_path()
110 )
111 })
112 .to_path_buf();
113
114 let binary_path = path_mapper.map_binary(binary.path.clone());
115 let cwd = path_mapper.map_cwd(cwd);
116
117 let non_test_binaries = if binary.kind == RustTestBinaryKind::TEST
119 || binary.kind == RustTestBinaryKind::BENCH
120 {
121 match rust_build_meta.non_test_binaries.get(package_id.repr()) {
124 Some(binaries) => binaries
125 .iter()
126 .filter(|binary| {
127 binary.kind == RustNonTestBinaryKind::BIN_EXE
129 })
130 .map(|binary| {
131 let abs_path = rust_build_meta.target_directory.join(&binary.path);
133 (binary.name.clone(), abs_path)
134 })
135 .collect(),
136 None => BTreeSet::new(),
137 }
138 } else {
139 BTreeSet::new()
140 };
141
142 binaries.push(RustTestArtifact {
143 binary_id: binary.id.clone(),
144 package,
145 binary_path,
146 binary_name: binary.name.clone(),
147 kind: binary.kind.clone(),
148 cwd,
149 non_test_binaries,
150 build_platform: binary.build_platform,
151 })
152 }
153
154 Ok(binaries)
155 }
156
157 pub fn to_binary_query(&self) -> BinaryQuery<'_> {
159 BinaryQuery {
160 package_id: self.package.id(),
161 binary_id: &self.binary_id,
162 kind: &self.kind,
163 binary_name: &self.binary_name,
164 platform: convert_build_platform(self.build_platform),
165 }
166 }
167
168 fn into_test_suite(self, status: RustTestSuiteStatus) -> (RustBinaryId, RustTestSuite<'g>) {
172 let Self {
173 binary_id,
174 package,
175 binary_path,
176 binary_name,
177 kind,
178 non_test_binaries,
179 cwd,
180 build_platform,
181 } = self;
182 (
183 binary_id.clone(),
184 RustTestSuite {
185 binary_id,
186 binary_path,
187 package,
188 binary_name,
189 kind,
190 non_test_binaries,
191 cwd,
192 build_platform,
193 status,
194 },
195 )
196 }
197}
198
199#[derive(Clone, Debug, Eq, PartialEq)]
201pub struct SkipCounts {
202 pub skipped_tests: usize,
204
205 pub skipped_tests_default_filter: usize,
207
208 pub skipped_binaries: usize,
210
211 pub skipped_binaries_default_filter: usize,
213}
214
215#[derive(Clone, Debug)]
217pub struct TestList<'g> {
218 test_count: usize,
219 rust_build_meta: RustBuildMeta<TestListState>,
220 rust_suites: BTreeMap<RustBinaryId, RustTestSuite<'g>>,
221 workspace_root: Utf8PathBuf,
222 env: EnvironmentMap,
223 updated_dylib_path: OsString,
224 skip_counts: OnceLock<SkipCounts>,
226}
227
228impl<'g> TestList<'g> {
229 #[expect(clippy::too_many_arguments)]
231 pub fn new<I>(
232 ctx: &TestExecuteContext<'_>,
233 test_artifacts: I,
234 rust_build_meta: RustBuildMeta<TestListState>,
235 filter: &TestFilterBuilder,
236 workspace_root: Utf8PathBuf,
237 env: EnvironmentMap,
238 profile: &impl ListProfile,
239 bound: FilterBound,
240 list_threads: usize,
241 ) -> Result<Self, CreateTestListError>
242 where
243 I: IntoIterator<Item = RustTestArtifact<'g>>,
244 I::IntoIter: Send,
245 {
246 let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
247 debug!(
248 "updated {}: {}",
249 dylib_path_envvar(),
250 updated_dylib_path.to_string_lossy(),
251 );
252 let lctx = LocalExecuteContext {
253 phase: TestCommandPhase::List,
254 workspace_root: &workspace_root,
257 rust_build_meta: &rust_build_meta,
258 double_spawn: ctx.double_spawn,
259 dylib_path: &updated_dylib_path,
260 profile_name: ctx.profile_name,
261 env: &env,
262 };
263
264 let ecx = profile.filterset_ecx();
265
266 let runtime = Runtime::new().map_err(CreateTestListError::TokioRuntimeCreate)?;
267
268 let stream = futures::stream::iter(test_artifacts).map(|test_binary| {
269 async {
270 let binary_query = test_binary.to_binary_query();
271 let binary_match = filter.filter_binary_match(&test_binary, &ecx, bound);
272 match binary_match {
273 FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
274 debug!(
275 "executing test binary to obtain test list \
276 (match result is {binary_match:?}): {}",
277 test_binary.binary_id,
278 );
279 let list_settings = profile.list_settings_for(&binary_query);
281 let (non_ignored, ignored) = test_binary
282 .exec(&lctx, &list_settings, ctx.target_runner)
283 .await?;
284 let (bin, info) = Self::process_output(
285 test_binary,
286 filter,
287 &ecx,
288 bound,
289 non_ignored.as_str(),
290 ignored.as_str(),
291 )?;
292 Ok::<_, CreateTestListError>((bin, info))
293 }
294 FilterBinaryMatch::Mismatch { reason } => {
295 debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
296 Ok(Self::process_skipped(test_binary, reason))
297 }
298 }
299 }
300 });
301 let fut = stream.buffer_unordered(list_threads).try_collect();
302
303 let rust_suites: BTreeMap<_, _> = runtime.block_on(fut)?;
304
305 runtime.shutdown_background();
308
309 let test_count = rust_suites
310 .values()
311 .map(|suite| suite.status.test_count())
312 .sum();
313
314 Ok(Self {
315 rust_suites,
316 workspace_root,
317 env,
318 rust_build_meta,
319 updated_dylib_path,
320 test_count,
321 skip_counts: OnceLock::new(),
322 })
323 }
324
325 #[cfg(test)]
327 fn new_with_outputs(
328 test_bin_outputs: impl IntoIterator<
329 Item = (RustTestArtifact<'g>, impl AsRef<str>, impl AsRef<str>),
330 >,
331 workspace_root: Utf8PathBuf,
332 rust_build_meta: RustBuildMeta<TestListState>,
333 filter: &TestFilterBuilder,
334 env: EnvironmentMap,
335 ecx: &EvalContext<'_>,
336 bound: FilterBound,
337 ) -> Result<Self, CreateTestListError> {
338 let mut test_count = 0;
339
340 let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
341
342 let rust_suites = test_bin_outputs
343 .into_iter()
344 .map(|(test_binary, non_ignored, ignored)| {
345 let binary_match = filter.filter_binary_match(&test_binary, ecx, bound);
346 match binary_match {
347 FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
348 debug!(
349 "processing output for binary \
350 (match result is {binary_match:?}): {}",
351 test_binary.binary_id,
352 );
353 let (bin, info) = Self::process_output(
354 test_binary,
355 filter,
356 ecx,
357 bound,
358 non_ignored.as_ref(),
359 ignored.as_ref(),
360 )?;
361 test_count += info.status.test_count();
362 Ok((bin, info))
363 }
364 FilterBinaryMatch::Mismatch { reason } => {
365 debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
366 Ok(Self::process_skipped(test_binary, reason))
367 }
368 }
369 })
370 .collect::<Result<BTreeMap<_, _>, _>>()?;
371
372 Ok(Self {
373 rust_suites,
374 workspace_root,
375 env,
376 rust_build_meta,
377 updated_dylib_path,
378 test_count,
379 skip_counts: OnceLock::new(),
380 })
381 }
382
383 pub fn test_count(&self) -> usize {
385 self.test_count
386 }
387
388 pub fn rust_build_meta(&self) -> &RustBuildMeta<TestListState> {
390 &self.rust_build_meta
391 }
392
393 pub fn skip_counts(&self) -> &SkipCounts {
395 self.skip_counts.get_or_init(|| {
396 let mut skipped_tests_default_filter = 0;
397 let skipped_tests = self
398 .iter_tests()
399 .filter(|instance| match instance.test_info.filter_match {
400 FilterMatch::Mismatch {
401 reason: MismatchReason::DefaultFilter,
402 } => {
403 skipped_tests_default_filter += 1;
404 true
405 }
406 FilterMatch::Mismatch { .. } => true,
407 FilterMatch::Matches => false,
408 })
409 .count();
410
411 let mut skipped_binaries_default_filter = 0;
412 let skipped_binaries = self
413 .rust_suites
414 .values()
415 .filter(|suite| match suite.status {
416 RustTestSuiteStatus::Skipped {
417 reason: BinaryMismatchReason::DefaultSet,
418 } => {
419 skipped_binaries_default_filter += 1;
420 true
421 }
422 RustTestSuiteStatus::Skipped { .. } => true,
423 RustTestSuiteStatus::Listed { .. } => false,
424 })
425 .count();
426
427 SkipCounts {
428 skipped_tests,
429 skipped_tests_default_filter,
430 skipped_binaries,
431 skipped_binaries_default_filter,
432 }
433 })
434 }
435
436 pub fn run_count(&self) -> usize {
440 self.test_count - self.skip_counts().skipped_tests
441 }
442
443 pub fn binary_count(&self) -> usize {
445 self.rust_suites.len()
446 }
447
448 pub fn listed_binary_count(&self) -> usize {
450 self.binary_count() - self.skip_counts().skipped_binaries
451 }
452
453 pub fn workspace_root(&self) -> &Utf8Path {
455 &self.workspace_root
456 }
457
458 pub fn cargo_env(&self) -> &EnvironmentMap {
460 &self.env
461 }
462
463 pub fn updated_dylib_path(&self) -> &OsStr {
465 &self.updated_dylib_path
466 }
467
468 pub fn to_summary(&self) -> TestListSummary {
470 let rust_suites = self
471 .rust_suites
472 .values()
473 .map(|test_suite| {
474 let (status, test_cases) = test_suite.status.to_summary();
475 let testsuite = RustTestSuiteSummary {
476 package_name: test_suite.package.name().to_owned(),
477 binary: RustTestBinarySummary {
478 binary_name: test_suite.binary_name.clone(),
479 package_id: test_suite.package.id().repr().to_owned(),
480 kind: test_suite.kind.clone(),
481 binary_path: test_suite.binary_path.clone(),
482 binary_id: test_suite.binary_id.clone(),
483 build_platform: test_suite.build_platform,
484 },
485 cwd: test_suite.cwd.clone(),
486 status,
487 test_cases,
488 };
489 (test_suite.binary_id.clone(), testsuite)
490 })
491 .collect();
492 let mut summary = TestListSummary::new(self.rust_build_meta.to_summary());
493 summary.test_count = self.test_count;
494 summary.rust_suites = rust_suites;
495 summary
496 }
497
498 pub fn write(
500 &self,
501 output_format: OutputFormat,
502 writer: &mut dyn WriteStr,
503 colorize: bool,
504 ) -> Result<(), WriteTestListError> {
505 match output_format {
506 OutputFormat::Human { verbose } => self
507 .write_human(writer, verbose, colorize)
508 .map_err(WriteTestListError::Io),
509 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
510 }
511 }
512
513 pub fn iter(&self) -> impl Iterator<Item = &RustTestSuite<'_>> + '_ {
515 self.rust_suites.values()
516 }
517
518 pub fn iter_tests(&self) -> impl Iterator<Item = TestInstance<'_>> + '_ {
520 self.rust_suites.values().flat_map(|test_suite| {
521 test_suite
522 .status
523 .test_cases()
524 .map(move |(name, test_info)| TestInstance::new(name, test_suite, test_info))
525 })
526 }
527
528 pub fn to_priority_queue(
530 &'g self,
531 profile: &'g EvaluatableProfile<'g>,
532 ) -> TestPriorityQueue<'g> {
533 TestPriorityQueue::new(self, profile)
534 }
535
536 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
538 let mut s = String::with_capacity(1024);
539 self.write(output_format, &mut s, false)?;
540 Ok(s)
541 }
542
543 #[cfg(test)]
549 pub(crate) fn empty() -> Self {
550 Self {
551 test_count: 0,
552 workspace_root: Utf8PathBuf::new(),
553 rust_build_meta: RustBuildMeta::empty(),
554 env: EnvironmentMap::empty(),
555 updated_dylib_path: OsString::new(),
556 rust_suites: BTreeMap::new(),
557 skip_counts: OnceLock::new(),
558 }
559 }
560
561 pub(crate) fn create_dylib_path(
562 rust_build_meta: &RustBuildMeta<TestListState>,
563 ) -> Result<OsString, CreateTestListError> {
564 let dylib_path = dylib_path();
565 let dylib_path_is_empty = dylib_path.is_empty();
566 let new_paths = rust_build_meta.dylib_paths();
567
568 let mut updated_dylib_path: Vec<PathBuf> =
569 Vec::with_capacity(dylib_path.len() + new_paths.len());
570 updated_dylib_path.extend(
571 new_paths
572 .iter()
573 .map(|path| path.clone().into_std_path_buf()),
574 );
575 updated_dylib_path.extend(dylib_path);
576
577 if cfg!(target_os = "macos") && dylib_path_is_empty {
584 if let Some(home) = home::home_dir() {
585 updated_dylib_path.push(home.join("lib"));
586 }
587 updated_dylib_path.push("/usr/local/lib".into());
588 updated_dylib_path.push("/usr/lib".into());
589 }
590
591 std::env::join_paths(updated_dylib_path)
592 .map_err(move |error| CreateTestListError::dylib_join_paths(new_paths, error))
593 }
594
595 fn process_output(
596 test_binary: RustTestArtifact<'g>,
597 filter: &TestFilterBuilder,
598 ecx: &EvalContext<'_>,
599 bound: FilterBound,
600 non_ignored: impl AsRef<str>,
601 ignored: impl AsRef<str>,
602 ) -> Result<(RustBinaryId, RustTestSuite<'g>), CreateTestListError> {
603 let mut test_cases = BTreeMap::new();
604
605 let mut non_ignored_filter = filter.build();
608 for test_name in Self::parse(&test_binary.binary_id, non_ignored.as_ref())? {
609 test_cases.insert(
610 test_name.into(),
611 RustTestCaseSummary {
612 ignored: false,
613 filter_match: non_ignored_filter.filter_match(
614 &test_binary,
615 test_name,
616 ecx,
617 bound,
618 false,
619 ),
620 },
621 );
622 }
623
624 let mut ignored_filter = filter.build();
625 for test_name in Self::parse(&test_binary.binary_id, ignored.as_ref())? {
626 test_cases.insert(
631 test_name.into(),
632 RustTestCaseSummary {
633 ignored: true,
634 filter_match: ignored_filter.filter_match(
635 &test_binary,
636 test_name,
637 ecx,
638 bound,
639 true,
640 ),
641 },
642 );
643 }
644
645 Ok(test_binary.into_test_suite(RustTestSuiteStatus::Listed {
646 test_cases: test_cases.into(),
647 }))
648 }
649
650 fn process_skipped(
651 test_binary: RustTestArtifact<'g>,
652 reason: BinaryMismatchReason,
653 ) -> (RustBinaryId, RustTestSuite<'g>) {
654 test_binary.into_test_suite(RustTestSuiteStatus::Skipped { reason })
655 }
656
657 fn parse<'a>(
659 binary_id: &'a RustBinaryId,
660 list_output: &'a str,
661 ) -> Result<Vec<&'a str>, CreateTestListError> {
662 let mut list = Self::parse_impl(binary_id, list_output).collect::<Result<Vec<_>, _>>()?;
663 list.sort_unstable();
664 Ok(list)
665 }
666
667 fn parse_impl<'a>(
668 binary_id: &'a RustBinaryId,
669 list_output: &'a str,
670 ) -> impl Iterator<Item = Result<&'a str, CreateTestListError>> + 'a + use<'a> {
671 list_output.lines().map(move |line| {
677 line.strip_suffix(": test")
678 .or_else(|| line.strip_suffix(": benchmark"))
679 .ok_or_else(|| {
680 CreateTestListError::parse_line(
681 binary_id.clone(),
682 format!(
683 "line '{line}' did not end with the string ': test' or ': benchmark'"
684 ),
685 list_output,
686 )
687 })
688 })
689 }
690
691 pub fn write_human(
693 &self,
694 writer: &mut dyn WriteStr,
695 verbose: bool,
696 colorize: bool,
697 ) -> io::Result<()> {
698 self.write_human_impl(None, writer, verbose, colorize)
699 }
700
701 pub(crate) fn write_human_with_filter(
703 &self,
704 filter: &TestListDisplayFilter<'_>,
705 writer: &mut dyn WriteStr,
706 verbose: bool,
707 colorize: bool,
708 ) -> io::Result<()> {
709 self.write_human_impl(Some(filter), writer, verbose, colorize)
710 }
711
712 fn write_human_impl(
713 &self,
714 filter: Option<&TestListDisplayFilter<'_>>,
715 mut writer: &mut dyn WriteStr,
716 verbose: bool,
717 colorize: bool,
718 ) -> io::Result<()> {
719 let mut styles = Styles::default();
720 if colorize {
721 styles.colorize();
722 }
723
724 for info in self.rust_suites.values() {
725 let matcher = match filter {
726 Some(filter) => match filter.matcher_for(&info.binary_id) {
727 Some(matcher) => matcher,
728 None => continue,
729 },
730 None => DisplayFilterMatcher::All,
731 };
732
733 if !verbose
736 && info
737 .status
738 .test_cases()
739 .all(|(_, test_case)| !test_case.filter_match.is_match())
740 {
741 continue;
742 }
743
744 writeln!(writer, "{}:", info.binary_id.style(styles.binary_id))?;
745 if verbose {
746 writeln!(
747 writer,
748 " {} {}",
749 "bin:".style(styles.field),
750 info.binary_path
751 )?;
752 writeln!(writer, " {} {}", "cwd:".style(styles.field), info.cwd)?;
753 writeln!(
754 writer,
755 " {} {}",
756 "build platform:".style(styles.field),
757 info.build_platform,
758 )?;
759 }
760
761 let mut indented = indented(writer).with_str(" ");
762
763 match &info.status {
764 RustTestSuiteStatus::Listed { test_cases } => {
765 let matching_tests: Vec<_> = test_cases
766 .iter()
767 .filter(|(name, _)| matcher.is_match(name))
768 .collect();
769 if matching_tests.is_empty() {
770 writeln!(indented, "(no tests)")?;
771 } else {
772 for (name, info) in matching_tests {
773 match (verbose, info.filter_match.is_match()) {
774 (_, true) => {
775 write_test_name(name, &styles, &mut indented)?;
776 writeln!(indented)?;
777 }
778 (true, false) => {
779 write_test_name(name, &styles, &mut indented)?;
780 writeln!(indented, " (skipped)")?;
781 }
782 (false, false) => {
783 }
785 }
786 }
787 }
788 }
789 RustTestSuiteStatus::Skipped { reason } => {
790 writeln!(indented, "(test binary {reason}, skipped)")?;
791 }
792 }
793
794 writer = indented.into_inner();
795 }
796 Ok(())
797 }
798}
799
800pub trait ListProfile {
802 fn filterset_ecx(&self) -> EvalContext<'_>;
804
805 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_>;
807}
808
809impl<'g> ListProfile for EvaluatableProfile<'g> {
810 fn filterset_ecx(&self) -> EvalContext<'_> {
811 self.filterset_ecx()
812 }
813
814 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
815 self.list_settings_for(query)
816 }
817}
818
819pub struct TestPriorityQueue<'a> {
821 tests: Vec<TestInstanceWithSettings<'a>>,
822}
823
824impl<'a> TestPriorityQueue<'a> {
825 fn new(test_list: &'a TestList<'a>, profile: &'a EvaluatableProfile<'a>) -> Self {
826 let mut tests = test_list
827 .iter_tests()
828 .map(|instance| {
829 let settings = profile.settings_for(&instance.to_test_query());
830 TestInstanceWithSettings { instance, settings }
831 })
832 .collect::<Vec<_>>();
833 tests.sort_by_key(|test| test.settings.priority());
836
837 Self { tests }
838 }
839}
840
841impl<'a> IntoIterator for TestPriorityQueue<'a> {
842 type Item = TestInstanceWithSettings<'a>;
843 type IntoIter = std::vec::IntoIter<Self::Item>;
844
845 fn into_iter(self) -> Self::IntoIter {
846 self.tests.into_iter()
847 }
848}
849
850#[derive(Debug)]
854pub struct TestInstanceWithSettings<'a> {
855 pub instance: TestInstance<'a>,
857
858 pub settings: TestSettings<'a>,
860}
861
862#[derive(Clone, Debug, Eq, PartialEq)]
866pub struct RustTestSuite<'g> {
867 pub binary_id: RustBinaryId,
869
870 pub binary_path: Utf8PathBuf,
872
873 pub package: PackageMetadata<'g>,
875
876 pub binary_name: String,
878
879 pub kind: RustTestBinaryKind,
881
882 pub cwd: Utf8PathBuf,
885
886 pub build_platform: BuildPlatform,
888
889 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
891
892 pub status: RustTestSuiteStatus,
894}
895
896impl RustTestArtifact<'_> {
897 async fn exec(
899 &self,
900 lctx: &LocalExecuteContext<'_>,
901 list_settings: &ListSettings<'_>,
902 target_runner: &TargetRunner,
903 ) -> Result<(String, String), CreateTestListError> {
904 if !self.cwd.is_dir() {
907 return Err(CreateTestListError::CwdIsNotDir {
908 binary_id: self.binary_id.clone(),
909 cwd: self.cwd.clone(),
910 });
911 }
912 let platform_runner = target_runner.for_build_platform(self.build_platform);
913
914 let non_ignored = self.exec_single(false, lctx, list_settings, platform_runner);
915 let ignored = self.exec_single(true, lctx, list_settings, platform_runner);
916
917 let (non_ignored_out, ignored_out) = futures::future::join(non_ignored, ignored).await;
918 Ok((non_ignored_out?, ignored_out?))
919 }
920
921 async fn exec_single(
922 &self,
923 ignored: bool,
924 lctx: &LocalExecuteContext<'_>,
925 list_settings: &ListSettings<'_>,
926 runner: Option<&PlatformRunner>,
927 ) -> Result<String, CreateTestListError> {
928 let mut cli = TestCommandCli::default();
929 cli.apply_wrappers(
930 list_settings.list_wrapper(),
931 runner,
932 lctx.workspace_root,
933 &lctx.rust_build_meta.target_directory,
934 );
935 cli.push(self.binary_path.as_str());
936
937 cli.extend(["--list", "--format", "terse"]);
938 if ignored {
939 cli.push("--ignored");
940 }
941
942 let cmd = TestCommand::new(
943 lctx,
944 cli.program
945 .clone()
946 .expect("at least one argument passed in")
947 .into_owned(),
948 &cli.args,
949 &self.cwd,
950 &self.package,
951 &self.non_test_binaries,
952 );
953
954 let output =
955 cmd.wait_with_output()
956 .await
957 .map_err(|error| CreateTestListError::CommandExecFail {
958 binary_id: self.binary_id.clone(),
959 command: cli.to_owned_cli(),
960 error,
961 })?;
962
963 if output.status.success() {
964 String::from_utf8(output.stdout).map_err(|err| CreateTestListError::CommandNonUtf8 {
965 binary_id: self.binary_id.clone(),
966 command: cli.to_owned_cli(),
967 stdout: err.into_bytes(),
968 stderr: output.stderr,
969 })
970 } else {
971 Err(CreateTestListError::CommandFail {
972 binary_id: self.binary_id.clone(),
973 command: cli.to_owned_cli(),
974 exit_status: output.status,
975 stdout: output.stdout,
976 stderr: output.stderr,
977 })
978 }
979 }
980}
981
982#[derive(Clone, Debug, Eq, PartialEq)]
986pub enum RustTestSuiteStatus {
987 Listed {
989 test_cases: DebugIgnore<BTreeMap<String, RustTestCaseSummary>>,
991 },
992
993 Skipped {
995 reason: BinaryMismatchReason,
997 },
998}
999
1000static EMPTY_TEST_CASE_MAP: BTreeMap<String, RustTestCaseSummary> = BTreeMap::new();
1001
1002impl RustTestSuiteStatus {
1003 pub fn test_count(&self) -> usize {
1005 match self {
1006 RustTestSuiteStatus::Listed { test_cases } => test_cases.len(),
1007 RustTestSuiteStatus::Skipped { .. } => 0,
1008 }
1009 }
1010
1011 pub fn test_cases(&self) -> impl Iterator<Item = (&str, &RustTestCaseSummary)> + '_ {
1013 match self {
1014 RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
1015 RustTestSuiteStatus::Skipped { .. } => {
1016 EMPTY_TEST_CASE_MAP.iter()
1018 }
1019 }
1020 .map(|(name, case)| (name.as_str(), case))
1021 }
1022
1023 pub fn to_summary(
1025 &self,
1026 ) -> (
1027 RustTestSuiteStatusSummary,
1028 BTreeMap<String, RustTestCaseSummary>,
1029 ) {
1030 match self {
1031 Self::Listed { test_cases } => {
1032 (RustTestSuiteStatusSummary::LISTED, test_cases.clone().0)
1033 }
1034 Self::Skipped {
1035 reason: BinaryMismatchReason::Expression,
1036 } => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
1037 Self::Skipped {
1038 reason: BinaryMismatchReason::DefaultSet,
1039 } => (
1040 RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER,
1041 BTreeMap::new(),
1042 ),
1043 }
1044 }
1045}
1046
1047#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1049pub struct TestInstance<'a> {
1050 pub name: &'a str,
1052
1053 pub suite_info: &'a RustTestSuite<'a>,
1055
1056 pub test_info: &'a RustTestCaseSummary,
1058}
1059
1060impl<'a> TestInstance<'a> {
1061 pub(crate) fn new(
1063 name: &'a (impl AsRef<str> + ?Sized),
1064 suite_info: &'a RustTestSuite,
1065 test_info: &'a RustTestCaseSummary,
1066 ) -> Self {
1067 Self {
1068 name: name.as_ref(),
1069 suite_info,
1070 test_info,
1071 }
1072 }
1073
1074 #[inline]
1077 pub fn id(&self) -> TestInstanceId<'a> {
1078 TestInstanceId {
1079 binary_id: &self.suite_info.binary_id,
1080 test_name: self.name,
1081 }
1082 }
1083
1084 pub fn to_test_query(&self) -> TestQuery<'a> {
1086 TestQuery {
1087 binary_query: BinaryQuery {
1088 package_id: self.suite_info.package.id(),
1089 binary_id: &self.suite_info.binary_id,
1090 kind: &self.suite_info.kind,
1091 binary_name: &self.suite_info.binary_name,
1092 platform: convert_build_platform(self.suite_info.build_platform),
1093 },
1094 test_name: self.name,
1095 }
1096 }
1097
1098 pub(crate) fn make_command(
1100 &self,
1101 ctx: &TestExecuteContext<'_>,
1102 test_list: &TestList<'_>,
1103 wrapper_script: Option<&'a WrapperScriptConfig>,
1104 extra_args: &[String],
1105 ) -> TestCommand {
1106 let platform_runner = ctx
1109 .target_runner
1110 .for_build_platform(self.suite_info.build_platform);
1111
1112 let mut cli = TestCommandCli::default();
1113 cli.apply_wrappers(
1114 wrapper_script,
1115 platform_runner,
1116 test_list.workspace_root(),
1117 &test_list.rust_build_meta().target_directory,
1118 );
1119 cli.push(self.suite_info.binary_path.as_str());
1120
1121 cli.extend(["--exact", self.name, "--nocapture"]);
1122 if self.test_info.ignored {
1123 cli.push("--ignored");
1124 }
1125 cli.extend(extra_args.iter().map(String::as_str));
1126
1127 let lctx = LocalExecuteContext {
1128 phase: TestCommandPhase::Run,
1129 workspace_root: test_list.workspace_root(),
1130 rust_build_meta: &test_list.rust_build_meta,
1131 double_spawn: ctx.double_spawn,
1132 dylib_path: test_list.updated_dylib_path(),
1133 profile_name: ctx.profile_name,
1134 env: &test_list.env,
1135 };
1136
1137 TestCommand::new(
1138 &lctx,
1139 cli.program
1140 .expect("at least one argument is guaranteed")
1141 .into_owned(),
1142 &cli.args,
1143 &self.suite_info.cwd,
1144 &self.suite_info.package,
1145 &self.suite_info.non_test_binaries,
1146 )
1147 }
1148}
1149
1150#[derive(Clone, Debug, Default)]
1151struct TestCommandCli<'a> {
1152 program: Option<Cow<'a, str>>,
1153 args: Vec<Cow<'a, str>>,
1154}
1155
1156impl<'a> TestCommandCli<'a> {
1157 fn apply_wrappers(
1158 &mut self,
1159 wrapper_script: Option<&'a WrapperScriptConfig>,
1160 platform_runner: Option<&'a PlatformRunner>,
1161 workspace_root: &Utf8Path,
1162 target_dir: &Utf8Path,
1163 ) {
1164 if let Some(wrapper) = wrapper_script {
1166 match wrapper.target_runner {
1167 WrapperScriptTargetRunner::Ignore => {
1168 self.push(wrapper.command.program(workspace_root, target_dir));
1170 self.extend(wrapper.command.args.iter().map(String::as_str));
1171 }
1172 WrapperScriptTargetRunner::AroundWrapper => {
1173 if let Some(runner) = platform_runner {
1175 self.push(runner.binary());
1176 self.extend(runner.args());
1177 }
1178 self.push(wrapper.command.program(workspace_root, target_dir));
1179 self.extend(wrapper.command.args.iter().map(String::as_str));
1180 }
1181 WrapperScriptTargetRunner::WithinWrapper => {
1182 self.push(wrapper.command.program(workspace_root, target_dir));
1184 self.extend(wrapper.command.args.iter().map(String::as_str));
1185 if let Some(runner) = platform_runner {
1186 self.push(runner.binary());
1187 self.extend(runner.args());
1188 }
1189 }
1190 WrapperScriptTargetRunner::OverridesWrapper => {
1191 if let Some(runner) = platform_runner {
1193 self.push(runner.binary());
1194 self.extend(runner.args());
1195 }
1196 }
1197 }
1198 } else {
1199 if let Some(runner) = platform_runner {
1201 self.push(runner.binary());
1202 self.extend(runner.args());
1203 }
1204 }
1205 }
1206
1207 fn push(&mut self, arg: impl Into<Cow<'a, str>>) {
1208 if self.program.is_none() {
1209 self.program = Some(arg.into());
1210 } else {
1211 self.args.push(arg.into());
1212 }
1213 }
1214
1215 fn extend(&mut self, args: impl IntoIterator<Item = &'a str>) {
1216 for arg in args {
1217 self.push(arg);
1218 }
1219 }
1220
1221 fn to_owned_cli(&self) -> Vec<String> {
1222 let mut owned_cli = Vec::new();
1223 if let Some(program) = &self.program {
1224 owned_cli.push(program.to_string());
1225 }
1226 owned_cli.extend(self.args.iter().map(|arg| arg.clone().into_owned()));
1227 owned_cli
1228 }
1229}
1230
1231#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
1235pub struct TestInstanceId<'a> {
1236 pub binary_id: &'a RustBinaryId,
1238
1239 pub test_name: &'a str,
1241}
1242
1243impl fmt::Display for TestInstanceId<'_> {
1244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1245 write!(f, "{} {}", self.binary_id, self.test_name)
1246 }
1247}
1248
1249#[derive(Clone, Debug)]
1251pub struct TestExecuteContext<'a> {
1252 pub profile_name: &'a str,
1254
1255 pub double_spawn: &'a DoubleSpawnInfo,
1257
1258 pub target_runner: &'a TargetRunner,
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264 use super::*;
1265 use crate::{
1266 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
1267 config::scripts::{ScriptCommand, ScriptCommandRelativeTo},
1268 list::SerializableFormat,
1269 platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
1270 target_runner::PlatformRunnerSource,
1271 test_filter::{RunIgnored, TestFilterPatterns},
1272 };
1273 use guppy::CargoMetadata;
1274 use indoc::indoc;
1275 use maplit::btreemap;
1276 use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, ParseContext};
1277 use nextest_metadata::{FilterMatch, MismatchReason, PlatformLibdirUnavailable};
1278 use pretty_assertions::assert_eq;
1279 use std::sync::LazyLock;
1280 use target_spec::Platform;
1281
1282 #[test]
1283 fn test_parse_test_list() {
1284 let non_ignored_output = indoc! {"
1286 tests::foo::test_bar: test
1287 tests::baz::test_quux: test
1288 benches::bench_foo: benchmark
1289 "};
1290 let ignored_output = indoc! {"
1291 tests::ignored::test_bar: test
1292 tests::baz::test_ignored: test
1293 benches::ignored_bench_foo: benchmark
1294 "};
1295
1296 let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
1297
1298 let test_filter = TestFilterBuilder::new(
1299 RunIgnored::Default,
1300 None,
1301 TestFilterPatterns::default(),
1302 vec![
1304 Filterset::parse("platform(target)".to_owned(), &cx, FiltersetKind::Test).unwrap(),
1305 ],
1306 )
1307 .unwrap();
1308 let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
1309 let fake_binary_name = "fake-binary".to_owned();
1310 let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
1311
1312 let test_binary = RustTestArtifact {
1313 binary_path: "/fake/binary".into(),
1314 cwd: fake_cwd.clone(),
1315 package: package_metadata(),
1316 binary_name: fake_binary_name.clone(),
1317 binary_id: fake_binary_id.clone(),
1318 kind: RustTestBinaryKind::LIB,
1319 non_test_binaries: BTreeSet::new(),
1320 build_platform: BuildPlatform::Target,
1321 };
1322
1323 let skipped_binary_name = "skipped-binary".to_owned();
1324 let skipped_binary_id = RustBinaryId::new("fake-package::skipped-binary");
1325 let skipped_binary = RustTestArtifact {
1326 binary_path: "/fake/skipped-binary".into(),
1327 cwd: fake_cwd.clone(),
1328 package: package_metadata(),
1329 binary_name: skipped_binary_name.clone(),
1330 binary_id: skipped_binary_id.clone(),
1331 kind: RustTestBinaryKind::PROC_MACRO,
1332 non_test_binaries: BTreeSet::new(),
1333 build_platform: BuildPlatform::Host,
1334 };
1335
1336 let fake_triple = TargetTriple {
1337 platform: Platform::new(
1338 "aarch64-unknown-linux-gnu",
1339 target_spec::TargetFeatures::Unknown,
1340 )
1341 .unwrap(),
1342 source: TargetTripleSource::CliOption,
1343 location: TargetDefinitionLocation::Builtin,
1344 };
1345 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
1346 let build_platforms = BuildPlatforms {
1347 host: HostPlatform {
1348 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
1349 libdir: PlatformLibdir::Available(fake_host_libdir.into()),
1350 },
1351 target: Some(TargetPlatform {
1352 triple: fake_triple,
1353 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
1355 }),
1356 };
1357
1358 let fake_env = EnvironmentMap::empty();
1359 let rust_build_meta =
1360 RustBuildMeta::new("/fake", build_platforms).map_paths(&PathMapper::noop());
1361 let ecx = EvalContext {
1362 default_filter: &CompiledExpr::ALL,
1363 };
1364 let test_list = TestList::new_with_outputs(
1365 [
1366 (test_binary, &non_ignored_output, &ignored_output),
1367 (
1368 skipped_binary,
1369 &"should-not-show-up-stdout",
1370 &"should-not-show-up-stderr",
1371 ),
1372 ],
1373 Utf8PathBuf::from("/fake/path"),
1374 rust_build_meta,
1375 &test_filter,
1376 fake_env,
1377 &ecx,
1378 FilterBound::All,
1379 )
1380 .expect("valid output");
1381 assert_eq!(
1382 test_list.rust_suites,
1383 btreemap! {
1384 fake_binary_id.clone() => RustTestSuite {
1385 status: RustTestSuiteStatus::Listed {
1386 test_cases: btreemap! {
1387 "tests::foo::test_bar".to_owned() => RustTestCaseSummary {
1388 ignored: false,
1389 filter_match: FilterMatch::Matches,
1390 },
1391 "tests::baz::test_quux".to_owned() => RustTestCaseSummary {
1392 ignored: false,
1393 filter_match: FilterMatch::Matches,
1394 },
1395 "benches::bench_foo".to_owned() => RustTestCaseSummary {
1396 ignored: false,
1397 filter_match: FilterMatch::Matches,
1398 },
1399 "tests::ignored::test_bar".to_owned() => RustTestCaseSummary {
1400 ignored: true,
1401 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1402 },
1403 "tests::baz::test_ignored".to_owned() => RustTestCaseSummary {
1404 ignored: true,
1405 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1406 },
1407 "benches::ignored_bench_foo".to_owned() => RustTestCaseSummary {
1408 ignored: true,
1409 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1410 },
1411 }.into(),
1412 },
1413 cwd: fake_cwd.clone(),
1414 build_platform: BuildPlatform::Target,
1415 package: package_metadata(),
1416 binary_name: fake_binary_name,
1417 binary_id: fake_binary_id,
1418 binary_path: "/fake/binary".into(),
1419 kind: RustTestBinaryKind::LIB,
1420 non_test_binaries: BTreeSet::new(),
1421 },
1422 skipped_binary_id.clone() => RustTestSuite {
1423 status: RustTestSuiteStatus::Skipped {
1424 reason: BinaryMismatchReason::Expression,
1425 },
1426 cwd: fake_cwd,
1427 build_platform: BuildPlatform::Host,
1428 package: package_metadata(),
1429 binary_name: skipped_binary_name,
1430 binary_id: skipped_binary_id,
1431 binary_path: "/fake/skipped-binary".into(),
1432 kind: RustTestBinaryKind::PROC_MACRO,
1433 non_test_binaries: BTreeSet::new(),
1434 },
1435 }
1436 );
1437
1438 static EXPECTED_HUMAN: &str = indoc! {"
1440 fake-package::fake-binary:
1441 benches::bench_foo
1442 tests::baz::test_quux
1443 tests::foo::test_bar
1444 "};
1445 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {"
1446 fake-package::fake-binary:
1447 bin: /fake/binary
1448 cwd: /fake/cwd
1449 build platform: target
1450 benches::bench_foo
1451 benches::ignored_bench_foo (skipped)
1452 tests::baz::test_ignored (skipped)
1453 tests::baz::test_quux
1454 tests::foo::test_bar
1455 tests::ignored::test_bar (skipped)
1456 fake-package::skipped-binary:
1457 bin: /fake/skipped-binary
1458 cwd: /fake/cwd
1459 build platform: host
1460 (test binary didn't match filtersets, skipped)
1461 "};
1462 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
1463 {
1464 "rust-build-meta": {
1465 "target-directory": "/fake",
1466 "base-output-directories": [],
1467 "non-test-binaries": {},
1468 "build-script-out-dirs": {},
1469 "linked-paths": [],
1470 "platforms": {
1471 "host": {
1472 "platform": {
1473 "triple": "x86_64-unknown-linux-gnu",
1474 "target-features": "unknown"
1475 },
1476 "libdir": {
1477 "status": "available",
1478 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
1479 }
1480 },
1481 "targets": [
1482 {
1483 "platform": {
1484 "triple": "aarch64-unknown-linux-gnu",
1485 "target-features": "unknown"
1486 },
1487 "libdir": {
1488 "status": "unavailable",
1489 "reason": "test"
1490 }
1491 }
1492 ]
1493 },
1494 "target-platforms": [
1495 {
1496 "triple": "aarch64-unknown-linux-gnu",
1497 "target-features": "unknown"
1498 }
1499 ],
1500 "target-platform": "aarch64-unknown-linux-gnu"
1501 },
1502 "test-count": 6,
1503 "rust-suites": {
1504 "fake-package::fake-binary": {
1505 "package-name": "metadata-helper",
1506 "binary-id": "fake-package::fake-binary",
1507 "binary-name": "fake-binary",
1508 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
1509 "kind": "lib",
1510 "binary-path": "/fake/binary",
1511 "build-platform": "target",
1512 "cwd": "/fake/cwd",
1513 "status": "listed",
1514 "testcases": {
1515 "benches::bench_foo": {
1516 "ignored": false,
1517 "filter-match": {
1518 "status": "matches"
1519 }
1520 },
1521 "benches::ignored_bench_foo": {
1522 "ignored": true,
1523 "filter-match": {
1524 "status": "mismatch",
1525 "reason": "ignored"
1526 }
1527 },
1528 "tests::baz::test_ignored": {
1529 "ignored": true,
1530 "filter-match": {
1531 "status": "mismatch",
1532 "reason": "ignored"
1533 }
1534 },
1535 "tests::baz::test_quux": {
1536 "ignored": false,
1537 "filter-match": {
1538 "status": "matches"
1539 }
1540 },
1541 "tests::foo::test_bar": {
1542 "ignored": false,
1543 "filter-match": {
1544 "status": "matches"
1545 }
1546 },
1547 "tests::ignored::test_bar": {
1548 "ignored": true,
1549 "filter-match": {
1550 "status": "mismatch",
1551 "reason": "ignored"
1552 }
1553 }
1554 }
1555 },
1556 "fake-package::skipped-binary": {
1557 "package-name": "metadata-helper",
1558 "binary-id": "fake-package::skipped-binary",
1559 "binary-name": "skipped-binary",
1560 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
1561 "kind": "proc-macro",
1562 "binary-path": "/fake/skipped-binary",
1563 "build-platform": "host",
1564 "cwd": "/fake/cwd",
1565 "status": "skipped",
1566 "testcases": {}
1567 }
1568 }
1569 }"#};
1570
1571 assert_eq!(
1572 test_list
1573 .to_string(OutputFormat::Human { verbose: false })
1574 .expect("human succeeded"),
1575 EXPECTED_HUMAN
1576 );
1577 assert_eq!(
1578 test_list
1579 .to_string(OutputFormat::Human { verbose: true })
1580 .expect("human succeeded"),
1581 EXPECTED_HUMAN_VERBOSE
1582 );
1583 println!(
1584 "{}",
1585 test_list
1586 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1587 .expect("json-pretty succeeded")
1588 );
1589 assert_eq!(
1590 test_list
1591 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1592 .expect("json-pretty succeeded"),
1593 EXPECTED_JSON_PRETTY
1594 );
1595 }
1596
1597 #[test]
1598 fn apply_wrappers_examples() {
1599 cfg_if::cfg_if! {
1600 if #[cfg(windows)]
1601 {
1602 let workspace_root = Utf8Path::new("D:\\workspace\\root");
1603 let target_dir = Utf8Path::new("C:\\foo\\bar");
1604 } else {
1605 let workspace_root = Utf8Path::new("/workspace/root");
1606 let target_dir = Utf8Path::new("/foo/bar");
1607 }
1608 };
1609
1610 {
1612 let mut cli_no_wrappers = TestCommandCli::default();
1613 cli_no_wrappers.apply_wrappers(None, None, workspace_root, target_dir);
1614 cli_no_wrappers.extend(["binary", "arg"]);
1615 assert_eq!(cli_no_wrappers.to_owned_cli(), vec!["binary", "arg"]);
1616 }
1617
1618 {
1620 let runner = PlatformRunner::debug_new(
1621 "runner".into(),
1622 Vec::new(),
1623 PlatformRunnerSource::Env("fake".to_owned()),
1624 );
1625 let mut cli_runner_only = TestCommandCli::default();
1626 cli_runner_only.apply_wrappers(None, Some(&runner), workspace_root, target_dir);
1627 cli_runner_only.extend(["binary", "arg"]);
1628 assert_eq!(
1629 cli_runner_only.to_owned_cli(),
1630 vec!["runner", "binary", "arg"],
1631 );
1632 }
1633
1634 {
1636 let runner = PlatformRunner::debug_new(
1637 "runner".into(),
1638 Vec::new(),
1639 PlatformRunnerSource::Env("fake".to_owned()),
1640 );
1641 let wrapper_ignore = WrapperScriptConfig {
1642 command: ScriptCommand {
1643 program: "wrapper".into(),
1644 args: Vec::new(),
1645 relative_to: ScriptCommandRelativeTo::None,
1646 },
1647 target_runner: WrapperScriptTargetRunner::Ignore,
1648 };
1649 let mut cli_wrapper_ignore = TestCommandCli::default();
1650 cli_wrapper_ignore.apply_wrappers(
1651 Some(&wrapper_ignore),
1652 Some(&runner),
1653 workspace_root,
1654 target_dir,
1655 );
1656 cli_wrapper_ignore.extend(["binary", "arg"]);
1657 assert_eq!(
1658 cli_wrapper_ignore.to_owned_cli(),
1659 vec!["wrapper", "binary", "arg"],
1660 );
1661 }
1662
1663 {
1665 let runner = PlatformRunner::debug_new(
1666 "runner".into(),
1667 Vec::new(),
1668 PlatformRunnerSource::Env("fake".to_owned()),
1669 );
1670 let wrapper_around = WrapperScriptConfig {
1671 command: ScriptCommand {
1672 program: "wrapper".into(),
1673 args: Vec::new(),
1674 relative_to: ScriptCommandRelativeTo::None,
1675 },
1676 target_runner: WrapperScriptTargetRunner::AroundWrapper,
1677 };
1678 let mut cli_wrapper_around = TestCommandCli::default();
1679 cli_wrapper_around.apply_wrappers(
1680 Some(&wrapper_around),
1681 Some(&runner),
1682 workspace_root,
1683 target_dir,
1684 );
1685 cli_wrapper_around.extend(["binary", "arg"]);
1686 assert_eq!(
1687 cli_wrapper_around.to_owned_cli(),
1688 vec!["runner", "wrapper", "binary", "arg"],
1689 );
1690 }
1691
1692 {
1694 let runner = PlatformRunner::debug_new(
1695 "runner".into(),
1696 Vec::new(),
1697 PlatformRunnerSource::Env("fake".to_owned()),
1698 );
1699 let wrapper_within = WrapperScriptConfig {
1700 command: ScriptCommand {
1701 program: "wrapper".into(),
1702 args: Vec::new(),
1703 relative_to: ScriptCommandRelativeTo::None,
1704 },
1705 target_runner: WrapperScriptTargetRunner::WithinWrapper,
1706 };
1707 let mut cli_wrapper_within = TestCommandCli::default();
1708 cli_wrapper_within.apply_wrappers(
1709 Some(&wrapper_within),
1710 Some(&runner),
1711 workspace_root,
1712 target_dir,
1713 );
1714 cli_wrapper_within.extend(["binary", "arg"]);
1715 assert_eq!(
1716 cli_wrapper_within.to_owned_cli(),
1717 vec!["wrapper", "runner", "binary", "arg"],
1718 );
1719 }
1720
1721 {
1723 let runner = PlatformRunner::debug_new(
1724 "runner".into(),
1725 Vec::new(),
1726 PlatformRunnerSource::Env("fake".to_owned()),
1727 );
1728 let wrapper_overrides = WrapperScriptConfig {
1729 command: ScriptCommand {
1730 program: "wrapper".into(),
1731 args: Vec::new(),
1732 relative_to: ScriptCommandRelativeTo::None,
1733 },
1734 target_runner: WrapperScriptTargetRunner::OverridesWrapper,
1735 };
1736 let mut cli_wrapper_overrides = TestCommandCli::default();
1737 cli_wrapper_overrides.apply_wrappers(
1738 Some(&wrapper_overrides),
1739 Some(&runner),
1740 workspace_root,
1741 target_dir,
1742 );
1743 cli_wrapper_overrides.extend(["binary", "arg"]);
1744 assert_eq!(
1745 cli_wrapper_overrides.to_owned_cli(),
1746 vec!["runner", "binary", "arg"],
1747 );
1748 }
1749
1750 {
1752 let wrapper_with_args = WrapperScriptConfig {
1753 command: ScriptCommand {
1754 program: "wrapper".into(),
1755 args: vec!["--flag".to_string(), "value".to_string()],
1756 relative_to: ScriptCommandRelativeTo::None,
1757 },
1758 target_runner: WrapperScriptTargetRunner::Ignore,
1759 };
1760 let mut cli_wrapper_args = TestCommandCli::default();
1761 cli_wrapper_args.apply_wrappers(
1762 Some(&wrapper_with_args),
1763 None,
1764 workspace_root,
1765 target_dir,
1766 );
1767 cli_wrapper_args.extend(["binary", "arg"]);
1768 assert_eq!(
1769 cli_wrapper_args.to_owned_cli(),
1770 vec!["wrapper", "--flag", "value", "binary", "arg"],
1771 );
1772 }
1773
1774 {
1776 let runner_with_args = PlatformRunner::debug_new(
1777 "runner".into(),
1778 vec!["--runner-flag".into(), "value".into()],
1779 PlatformRunnerSource::Env("fake".to_owned()),
1780 );
1781 let mut cli_runner_args = TestCommandCli::default();
1782 cli_runner_args.apply_wrappers(
1783 None,
1784 Some(&runner_with_args),
1785 workspace_root,
1786 target_dir,
1787 );
1788 cli_runner_args.extend(["binary", "arg"]);
1789 assert_eq!(
1790 cli_runner_args.to_owned_cli(),
1791 vec!["runner", "--runner-flag", "value", "binary", "arg"],
1792 );
1793 }
1794
1795 {
1797 let wrapper_relative_to_workspace_root = WrapperScriptConfig {
1798 command: ScriptCommand {
1799 program: "abc/def/my-wrapper".into(),
1800 args: vec!["--verbose".to_string()],
1801 relative_to: ScriptCommandRelativeTo::WorkspaceRoot,
1802 },
1803 target_runner: WrapperScriptTargetRunner::Ignore,
1804 };
1805 let mut cli_wrapper_relative = TestCommandCli::default();
1806 cli_wrapper_relative.apply_wrappers(
1807 Some(&wrapper_relative_to_workspace_root),
1808 None,
1809 workspace_root,
1810 target_dir,
1811 );
1812 cli_wrapper_relative.extend(["binary", "arg"]);
1813
1814 cfg_if::cfg_if! {
1815 if #[cfg(windows)] {
1816 let wrapper_path = "D:\\workspace\\root\\abc\\def\\my-wrapper";
1817 } else {
1818 let wrapper_path = "/workspace/root/abc/def/my-wrapper";
1819 }
1820 }
1821 assert_eq!(
1822 cli_wrapper_relative.to_owned_cli(),
1823 vec![wrapper_path, "--verbose", "binary", "arg"],
1824 );
1825 }
1826
1827 {
1829 let wrapper_relative_to_target = WrapperScriptConfig {
1830 command: ScriptCommand {
1831 program: "abc/def/my-wrapper".into(),
1832 args: vec!["--verbose".to_string()],
1833 relative_to: ScriptCommandRelativeTo::Target,
1834 },
1835 target_runner: WrapperScriptTargetRunner::Ignore,
1836 };
1837 let mut cli_wrapper_relative = TestCommandCli::default();
1838 cli_wrapper_relative.apply_wrappers(
1839 Some(&wrapper_relative_to_target),
1840 None,
1841 workspace_root,
1842 target_dir,
1843 );
1844 cli_wrapper_relative.extend(["binary", "arg"]);
1845 cfg_if::cfg_if! {
1846 if #[cfg(windows)] {
1847 let wrapper_path = "C:\\foo\\bar\\abc\\def\\my-wrapper";
1848 } else {
1849 let wrapper_path = "/foo/bar/abc/def/my-wrapper";
1850 }
1851 }
1852 assert_eq!(
1853 cli_wrapper_relative.to_owned_cli(),
1854 vec![wrapper_path, "--verbose", "binary", "arg"],
1855 );
1856 }
1857 }
1858
1859 static PACKAGE_GRAPH_FIXTURE: LazyLock<PackageGraph> = LazyLock::new(|| {
1860 static FIXTURE_JSON: &str = include_str!("../../../fixtures/cargo-metadata.json");
1861 let metadata = CargoMetadata::parse_json(FIXTURE_JSON).expect("fixture is valid JSON");
1862 metadata
1863 .build_graph()
1864 .expect("fixture is valid PackageGraph")
1865 });
1866
1867 static PACKAGE_METADATA_ID: &str = "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)";
1868 fn package_metadata() -> PackageMetadata<'static> {
1869 PACKAGE_GRAPH_FIXTURE
1870 .metadata(&PackageId::new(PACKAGE_METADATA_ID))
1871 .expect("package ID is valid")
1872 }
1873}