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