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