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 |(name, test_info)| TestInstance::new(name, test_suite, test_info))
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 = BTreeMap::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(
612 test_name.into(),
613 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(
633 test_name.into(),
634 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(|(_, test_case)| !test_case.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(|(name, _)| matcher.is_match(name))
770 .collect();
771 if matching_tests.is_empty() {
772 writeln!(indented, "(no tests)")?;
773 } else {
774 for (name, info) in matching_tests {
775 match (verbose, info.filter_match.is_match()) {
776 (_, true) => {
777 write_test_name(name, &styles, &mut indented)?;
778 writeln!(indented)?;
779 }
780 (true, false) => {
781 write_test_name(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<BTreeMap<String, RustTestCaseSummary>>,
1006 },
1007
1008 Skipped {
1010 reason: BinaryMismatchReason,
1012 },
1013}
1014
1015static EMPTY_TEST_CASE_MAP: BTreeMap<String, RustTestCaseSummary> = BTreeMap::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 = (&str, &RustTestCaseSummary)> + '_ {
1028 match self {
1029 RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
1030 RustTestSuiteStatus::Skipped { .. } => {
1031 EMPTY_TEST_CASE_MAP.iter()
1033 }
1034 }
1035 .map(|(name, case)| (name.as_str(), case))
1036 }
1037
1038 pub fn to_summary(
1040 &self,
1041 ) -> (
1042 RustTestSuiteStatusSummary,
1043 BTreeMap<String, RustTestCaseSummary>,
1044 ) {
1045 match self {
1046 Self::Listed { test_cases } => {
1047 (RustTestSuiteStatusSummary::LISTED, test_cases.clone().0)
1048 }
1049 Self::Skipped {
1050 reason: BinaryMismatchReason::Expression,
1051 } => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
1052 Self::Skipped {
1053 reason: BinaryMismatchReason::DefaultSet,
1054 } => (
1055 RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER,
1056 BTreeMap::new(),
1057 ),
1058 }
1059 }
1060}
1061
1062#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1064pub struct TestInstance<'a> {
1065 pub name: &'a str,
1067
1068 pub suite_info: &'a RustTestSuite<'a>,
1070
1071 pub test_info: &'a RustTestCaseSummary,
1073}
1074
1075impl<'a> TestInstance<'a> {
1076 pub(crate) fn new(
1078 name: &'a (impl AsRef<str> + ?Sized),
1079 suite_info: &'a RustTestSuite,
1080 test_info: &'a RustTestCaseSummary,
1081 ) -> Self {
1082 Self {
1083 name: name.as_ref(),
1084 suite_info,
1085 test_info,
1086 }
1087 }
1088
1089 #[inline]
1092 pub fn id(&self) -> TestInstanceId<'a> {
1093 TestInstanceId {
1094 binary_id: &self.suite_info.binary_id,
1095 test_name: self.name,
1096 }
1097 }
1098
1099 pub fn to_test_query(&self) -> TestQuery<'a> {
1101 TestQuery {
1102 binary_query: BinaryQuery {
1103 package_id: self.suite_info.package.id(),
1104 binary_id: &self.suite_info.binary_id,
1105 kind: &self.suite_info.kind,
1106 binary_name: &self.suite_info.binary_name,
1107 platform: convert_build_platform(self.suite_info.build_platform),
1108 },
1109 test_name: self.name,
1110 }
1111 }
1112
1113 pub(crate) fn make_command(
1115 &self,
1116 ctx: &TestExecuteContext<'_>,
1117 test_list: &TestList<'_>,
1118 wrapper_script: Option<&'a WrapperScriptConfig>,
1119 extra_args: &[String],
1120 ) -> TestCommand {
1121 let platform_runner = ctx
1124 .target_runner
1125 .for_build_platform(self.suite_info.build_platform);
1126
1127 let mut cli = TestCommandCli::default();
1128 cli.apply_wrappers(
1129 wrapper_script,
1130 platform_runner,
1131 test_list.workspace_root(),
1132 &test_list.rust_build_meta().target_directory,
1133 );
1134 cli.push(self.suite_info.binary_path.as_str());
1135
1136 cli.extend(["--exact", self.name, "--nocapture"]);
1137 if self.test_info.ignored {
1138 cli.push("--ignored");
1139 }
1140 cli.extend(extra_args.iter().map(String::as_str));
1141
1142 let lctx = LocalExecuteContext {
1143 phase: TestCommandPhase::Run,
1144 workspace_root: test_list.workspace_root(),
1145 rust_build_meta: &test_list.rust_build_meta,
1146 double_spawn: ctx.double_spawn,
1147 dylib_path: test_list.updated_dylib_path(),
1148 profile_name: ctx.profile_name,
1149 env: &test_list.env,
1150 };
1151
1152 TestCommand::new(
1153 &lctx,
1154 cli.program
1155 .expect("at least one argument is guaranteed")
1156 .into_owned(),
1157 &cli.args,
1158 &self.suite_info.cwd,
1159 &self.suite_info.package,
1160 &self.suite_info.non_test_binaries,
1161 )
1162 }
1163}
1164
1165#[derive(Clone, Debug, Default)]
1166struct TestCommandCli<'a> {
1167 program: Option<Cow<'a, str>>,
1168 args: Vec<Cow<'a, str>>,
1169}
1170
1171impl<'a> TestCommandCli<'a> {
1172 fn apply_wrappers(
1173 &mut self,
1174 wrapper_script: Option<&'a WrapperScriptConfig>,
1175 platform_runner: Option<&'a PlatformRunner>,
1176 workspace_root: &Utf8Path,
1177 target_dir: &Utf8Path,
1178 ) {
1179 if let Some(wrapper) = wrapper_script {
1181 match wrapper.target_runner {
1182 WrapperScriptTargetRunner::Ignore => {
1183 self.push(wrapper.command.program(workspace_root, target_dir));
1185 self.extend(wrapper.command.args.iter().map(String::as_str));
1186 }
1187 WrapperScriptTargetRunner::AroundWrapper => {
1188 if let Some(runner) = platform_runner {
1190 self.push(runner.binary());
1191 self.extend(runner.args());
1192 }
1193 self.push(wrapper.command.program(workspace_root, target_dir));
1194 self.extend(wrapper.command.args.iter().map(String::as_str));
1195 }
1196 WrapperScriptTargetRunner::WithinWrapper => {
1197 self.push(wrapper.command.program(workspace_root, target_dir));
1199 self.extend(wrapper.command.args.iter().map(String::as_str));
1200 if let Some(runner) = platform_runner {
1201 self.push(runner.binary());
1202 self.extend(runner.args());
1203 }
1204 }
1205 WrapperScriptTargetRunner::OverridesWrapper => {
1206 if let Some(runner) = platform_runner {
1208 self.push(runner.binary());
1209 self.extend(runner.args());
1210 }
1211 }
1212 }
1213 } else {
1214 if let Some(runner) = platform_runner {
1216 self.push(runner.binary());
1217 self.extend(runner.args());
1218 }
1219 }
1220 }
1221
1222 fn push(&mut self, arg: impl Into<Cow<'a, str>>) {
1223 if self.program.is_none() {
1224 self.program = Some(arg.into());
1225 } else {
1226 self.args.push(arg.into());
1227 }
1228 }
1229
1230 fn extend(&mut self, args: impl IntoIterator<Item = &'a str>) {
1231 for arg in args {
1232 self.push(arg);
1233 }
1234 }
1235
1236 fn to_owned_cli(&self) -> Vec<String> {
1237 let mut owned_cli = Vec::new();
1238 if let Some(program) = &self.program {
1239 owned_cli.push(program.to_string());
1240 }
1241 owned_cli.extend(self.args.iter().map(|arg| arg.clone().into_owned()));
1242 owned_cli
1243 }
1244}
1245
1246#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
1250pub struct TestInstanceId<'a> {
1251 pub binary_id: &'a RustBinaryId,
1253
1254 pub test_name: &'a str,
1256}
1257
1258impl TestInstanceId<'_> {
1259 pub fn attempt_id(
1263 &self,
1264 run_id: ReportUuid,
1265 stress_index: Option<u32>,
1266 attempt: u32,
1267 ) -> String {
1268 let mut out = String::new();
1269 swrite!(out, "{run_id}:{}", self.binary_id);
1270 if let Some(stress_index) = stress_index {
1271 swrite!(out, "@stress-{}", stress_index);
1272 }
1273 swrite!(out, "${}", self.test_name);
1274 if attempt > 1 {
1275 swrite!(out, "#{attempt}");
1276 }
1277
1278 out
1279 }
1280}
1281
1282impl fmt::Display for TestInstanceId<'_> {
1283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1284 write!(f, "{} {}", self.binary_id, self.test_name)
1285 }
1286}
1287
1288#[derive(Clone, Debug)]
1290pub struct TestExecuteContext<'a> {
1291 pub profile_name: &'a str,
1293
1294 pub double_spawn: &'a DoubleSpawnInfo,
1296
1297 pub target_runner: &'a TargetRunner,
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303 use super::*;
1304 use crate::{
1305 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
1306 config::scripts::{ScriptCommand, ScriptCommandRelativeTo},
1307 list::SerializableFormat,
1308 platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
1309 target_runner::PlatformRunnerSource,
1310 test_filter::{RunIgnored, TestFilterPatterns},
1311 };
1312 use guppy::CargoMetadata;
1313 use iddqd::id_ord_map;
1314 use indoc::indoc;
1315 use maplit::btreemap;
1316 use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, ParseContext};
1317 use nextest_metadata::{FilterMatch, MismatchReason, PlatformLibdirUnavailable};
1318 use pretty_assertions::assert_eq;
1319 use std::sync::LazyLock;
1320 use target_spec::Platform;
1321
1322 #[test]
1323 fn test_parse_test_list() {
1324 let non_ignored_output = indoc! {"
1326 tests::foo::test_bar: test
1327 tests::baz::test_quux: test
1328 benches::bench_foo: benchmark
1329 "};
1330 let ignored_output = indoc! {"
1331 tests::ignored::test_bar: test
1332 tests::baz::test_ignored: test
1333 benches::ignored_bench_foo: benchmark
1334 "};
1335
1336 let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
1337
1338 let test_filter = TestFilterBuilder::new(
1339 RunIgnored::Default,
1340 None,
1341 TestFilterPatterns::default(),
1342 vec![
1344 Filterset::parse("platform(target)".to_owned(), &cx, FiltersetKind::Test).unwrap(),
1345 ],
1346 )
1347 .unwrap();
1348 let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
1349 let fake_binary_name = "fake-binary".to_owned();
1350 let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
1351
1352 let test_binary = RustTestArtifact {
1353 binary_path: "/fake/binary".into(),
1354 cwd: fake_cwd.clone(),
1355 package: package_metadata(),
1356 binary_name: fake_binary_name.clone(),
1357 binary_id: fake_binary_id.clone(),
1358 kind: RustTestBinaryKind::LIB,
1359 non_test_binaries: BTreeSet::new(),
1360 build_platform: BuildPlatform::Target,
1361 };
1362
1363 let skipped_binary_name = "skipped-binary".to_owned();
1364 let skipped_binary_id = RustBinaryId::new("fake-package::skipped-binary");
1365 let skipped_binary = RustTestArtifact {
1366 binary_path: "/fake/skipped-binary".into(),
1367 cwd: fake_cwd.clone(),
1368 package: package_metadata(),
1369 binary_name: skipped_binary_name.clone(),
1370 binary_id: skipped_binary_id.clone(),
1371 kind: RustTestBinaryKind::PROC_MACRO,
1372 non_test_binaries: BTreeSet::new(),
1373 build_platform: BuildPlatform::Host,
1374 };
1375
1376 let fake_triple = TargetTriple {
1377 platform: Platform::new(
1378 "aarch64-unknown-linux-gnu",
1379 target_spec::TargetFeatures::Unknown,
1380 )
1381 .unwrap(),
1382 source: TargetTripleSource::CliOption,
1383 location: TargetDefinitionLocation::Builtin,
1384 };
1385 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
1386 let build_platforms = BuildPlatforms {
1387 host: HostPlatform {
1388 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
1389 libdir: PlatformLibdir::Available(fake_host_libdir.into()),
1390 },
1391 target: Some(TargetPlatform {
1392 triple: fake_triple,
1393 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
1395 }),
1396 };
1397
1398 let fake_env = EnvironmentMap::empty();
1399 let rust_build_meta =
1400 RustBuildMeta::new("/fake", build_platforms).map_paths(&PathMapper::noop());
1401 let ecx = EvalContext {
1402 default_filter: &CompiledExpr::ALL,
1403 };
1404 let test_list = TestList::new_with_outputs(
1405 [
1406 (test_binary, &non_ignored_output, &ignored_output),
1407 (
1408 skipped_binary,
1409 &"should-not-show-up-stdout",
1410 &"should-not-show-up-stderr",
1411 ),
1412 ],
1413 Utf8PathBuf::from("/fake/path"),
1414 rust_build_meta,
1415 &test_filter,
1416 fake_env,
1417 &ecx,
1418 FilterBound::All,
1419 )
1420 .expect("valid output");
1421 assert_eq!(
1422 test_list.rust_suites,
1423 id_ord_map! {
1424 RustTestSuite {
1425 status: RustTestSuiteStatus::Listed {
1426 test_cases: btreemap! {
1427 "tests::foo::test_bar".to_owned() => RustTestCaseSummary {
1428 ignored: false,
1429 filter_match: FilterMatch::Matches,
1430 },
1431 "tests::baz::test_quux".to_owned() => RustTestCaseSummary {
1432 ignored: false,
1433 filter_match: FilterMatch::Matches,
1434 },
1435 "benches::bench_foo".to_owned() => RustTestCaseSummary {
1436 ignored: false,
1437 filter_match: FilterMatch::Matches,
1438 },
1439 "tests::ignored::test_bar".to_owned() => RustTestCaseSummary {
1440 ignored: true,
1441 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1442 },
1443 "tests::baz::test_ignored".to_owned() => RustTestCaseSummary {
1444 ignored: true,
1445 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1446 },
1447 "benches::ignored_bench_foo".to_owned() => RustTestCaseSummary {
1448 ignored: true,
1449 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1450 },
1451 }.into(),
1452 },
1453 cwd: fake_cwd.clone(),
1454 build_platform: BuildPlatform::Target,
1455 package: package_metadata(),
1456 binary_name: fake_binary_name,
1457 binary_id: fake_binary_id,
1458 binary_path: "/fake/binary".into(),
1459 kind: RustTestBinaryKind::LIB,
1460 non_test_binaries: BTreeSet::new(),
1461 },
1462 RustTestSuite {
1463 status: RustTestSuiteStatus::Skipped {
1464 reason: BinaryMismatchReason::Expression,
1465 },
1466 cwd: fake_cwd,
1467 build_platform: BuildPlatform::Host,
1468 package: package_metadata(),
1469 binary_name: skipped_binary_name,
1470 binary_id: skipped_binary_id,
1471 binary_path: "/fake/skipped-binary".into(),
1472 kind: RustTestBinaryKind::PROC_MACRO,
1473 non_test_binaries: BTreeSet::new(),
1474 },
1475 }
1476 );
1477
1478 static EXPECTED_HUMAN: &str = indoc! {"
1480 fake-package::fake-binary:
1481 benches::bench_foo
1482 tests::baz::test_quux
1483 tests::foo::test_bar
1484 "};
1485 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {"
1486 fake-package::fake-binary:
1487 bin: /fake/binary
1488 cwd: /fake/cwd
1489 build platform: target
1490 benches::bench_foo
1491 benches::ignored_bench_foo (skipped)
1492 tests::baz::test_ignored (skipped)
1493 tests::baz::test_quux
1494 tests::foo::test_bar
1495 tests::ignored::test_bar (skipped)
1496 fake-package::skipped-binary:
1497 bin: /fake/skipped-binary
1498 cwd: /fake/cwd
1499 build platform: host
1500 (test binary didn't match filtersets, skipped)
1501 "};
1502 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
1503 {
1504 "rust-build-meta": {
1505 "target-directory": "/fake",
1506 "base-output-directories": [],
1507 "non-test-binaries": {},
1508 "build-script-out-dirs": {},
1509 "linked-paths": [],
1510 "platforms": {
1511 "host": {
1512 "platform": {
1513 "triple": "x86_64-unknown-linux-gnu",
1514 "target-features": "unknown"
1515 },
1516 "libdir": {
1517 "status": "available",
1518 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
1519 }
1520 },
1521 "targets": [
1522 {
1523 "platform": {
1524 "triple": "aarch64-unknown-linux-gnu",
1525 "target-features": "unknown"
1526 },
1527 "libdir": {
1528 "status": "unavailable",
1529 "reason": "test"
1530 }
1531 }
1532 ]
1533 },
1534 "target-platforms": [
1535 {
1536 "triple": "aarch64-unknown-linux-gnu",
1537 "target-features": "unknown"
1538 }
1539 ],
1540 "target-platform": "aarch64-unknown-linux-gnu"
1541 },
1542 "test-count": 6,
1543 "rust-suites": {
1544 "fake-package::fake-binary": {
1545 "package-name": "metadata-helper",
1546 "binary-id": "fake-package::fake-binary",
1547 "binary-name": "fake-binary",
1548 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
1549 "kind": "lib",
1550 "binary-path": "/fake/binary",
1551 "build-platform": "target",
1552 "cwd": "/fake/cwd",
1553 "status": "listed",
1554 "testcases": {
1555 "benches::bench_foo": {
1556 "ignored": false,
1557 "filter-match": {
1558 "status": "matches"
1559 }
1560 },
1561 "benches::ignored_bench_foo": {
1562 "ignored": true,
1563 "filter-match": {
1564 "status": "mismatch",
1565 "reason": "ignored"
1566 }
1567 },
1568 "tests::baz::test_ignored": {
1569 "ignored": true,
1570 "filter-match": {
1571 "status": "mismatch",
1572 "reason": "ignored"
1573 }
1574 },
1575 "tests::baz::test_quux": {
1576 "ignored": false,
1577 "filter-match": {
1578 "status": "matches"
1579 }
1580 },
1581 "tests::foo::test_bar": {
1582 "ignored": false,
1583 "filter-match": {
1584 "status": "matches"
1585 }
1586 },
1587 "tests::ignored::test_bar": {
1588 "ignored": true,
1589 "filter-match": {
1590 "status": "mismatch",
1591 "reason": "ignored"
1592 }
1593 }
1594 }
1595 },
1596 "fake-package::skipped-binary": {
1597 "package-name": "metadata-helper",
1598 "binary-id": "fake-package::skipped-binary",
1599 "binary-name": "skipped-binary",
1600 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
1601 "kind": "proc-macro",
1602 "binary-path": "/fake/skipped-binary",
1603 "build-platform": "host",
1604 "cwd": "/fake/cwd",
1605 "status": "skipped",
1606 "testcases": {}
1607 }
1608 }
1609 }"#};
1610
1611 assert_eq!(
1612 test_list
1613 .to_string(OutputFormat::Human { verbose: false })
1614 .expect("human succeeded"),
1615 EXPECTED_HUMAN
1616 );
1617 assert_eq!(
1618 test_list
1619 .to_string(OutputFormat::Human { verbose: true })
1620 .expect("human succeeded"),
1621 EXPECTED_HUMAN_VERBOSE
1622 );
1623 println!(
1624 "{}",
1625 test_list
1626 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1627 .expect("json-pretty succeeded")
1628 );
1629 assert_eq!(
1630 test_list
1631 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1632 .expect("json-pretty succeeded"),
1633 EXPECTED_JSON_PRETTY
1634 );
1635 }
1636
1637 #[test]
1638 fn apply_wrappers_examples() {
1639 cfg_if::cfg_if! {
1640 if #[cfg(windows)]
1641 {
1642 let workspace_root = Utf8Path::new("D:\\workspace\\root");
1643 let target_dir = Utf8Path::new("C:\\foo\\bar");
1644 } else {
1645 let workspace_root = Utf8Path::new("/workspace/root");
1646 let target_dir = Utf8Path::new("/foo/bar");
1647 }
1648 };
1649
1650 {
1652 let mut cli_no_wrappers = TestCommandCli::default();
1653 cli_no_wrappers.apply_wrappers(None, None, workspace_root, target_dir);
1654 cli_no_wrappers.extend(["binary", "arg"]);
1655 assert_eq!(cli_no_wrappers.to_owned_cli(), vec!["binary", "arg"]);
1656 }
1657
1658 {
1660 let runner = PlatformRunner::debug_new(
1661 "runner".into(),
1662 Vec::new(),
1663 PlatformRunnerSource::Env("fake".to_owned()),
1664 );
1665 let mut cli_runner_only = TestCommandCli::default();
1666 cli_runner_only.apply_wrappers(None, Some(&runner), workspace_root, target_dir);
1667 cli_runner_only.extend(["binary", "arg"]);
1668 assert_eq!(
1669 cli_runner_only.to_owned_cli(),
1670 vec!["runner", "binary", "arg"],
1671 );
1672 }
1673
1674 {
1676 let runner = PlatformRunner::debug_new(
1677 "runner".into(),
1678 Vec::new(),
1679 PlatformRunnerSource::Env("fake".to_owned()),
1680 );
1681 let wrapper_ignore = WrapperScriptConfig {
1682 command: ScriptCommand {
1683 program: "wrapper".into(),
1684 args: Vec::new(),
1685 relative_to: ScriptCommandRelativeTo::None,
1686 },
1687 target_runner: WrapperScriptTargetRunner::Ignore,
1688 };
1689 let mut cli_wrapper_ignore = TestCommandCli::default();
1690 cli_wrapper_ignore.apply_wrappers(
1691 Some(&wrapper_ignore),
1692 Some(&runner),
1693 workspace_root,
1694 target_dir,
1695 );
1696 cli_wrapper_ignore.extend(["binary", "arg"]);
1697 assert_eq!(
1698 cli_wrapper_ignore.to_owned_cli(),
1699 vec!["wrapper", "binary", "arg"],
1700 );
1701 }
1702
1703 {
1705 let runner = PlatformRunner::debug_new(
1706 "runner".into(),
1707 Vec::new(),
1708 PlatformRunnerSource::Env("fake".to_owned()),
1709 );
1710 let wrapper_around = WrapperScriptConfig {
1711 command: ScriptCommand {
1712 program: "wrapper".into(),
1713 args: Vec::new(),
1714 relative_to: ScriptCommandRelativeTo::None,
1715 },
1716 target_runner: WrapperScriptTargetRunner::AroundWrapper,
1717 };
1718 let mut cli_wrapper_around = TestCommandCli::default();
1719 cli_wrapper_around.apply_wrappers(
1720 Some(&wrapper_around),
1721 Some(&runner),
1722 workspace_root,
1723 target_dir,
1724 );
1725 cli_wrapper_around.extend(["binary", "arg"]);
1726 assert_eq!(
1727 cli_wrapper_around.to_owned_cli(),
1728 vec!["runner", "wrapper", "binary", "arg"],
1729 );
1730 }
1731
1732 {
1734 let runner = PlatformRunner::debug_new(
1735 "runner".into(),
1736 Vec::new(),
1737 PlatformRunnerSource::Env("fake".to_owned()),
1738 );
1739 let wrapper_within = WrapperScriptConfig {
1740 command: ScriptCommand {
1741 program: "wrapper".into(),
1742 args: Vec::new(),
1743 relative_to: ScriptCommandRelativeTo::None,
1744 },
1745 target_runner: WrapperScriptTargetRunner::WithinWrapper,
1746 };
1747 let mut cli_wrapper_within = TestCommandCli::default();
1748 cli_wrapper_within.apply_wrappers(
1749 Some(&wrapper_within),
1750 Some(&runner),
1751 workspace_root,
1752 target_dir,
1753 );
1754 cli_wrapper_within.extend(["binary", "arg"]);
1755 assert_eq!(
1756 cli_wrapper_within.to_owned_cli(),
1757 vec!["wrapper", "runner", "binary", "arg"],
1758 );
1759 }
1760
1761 {
1763 let runner = PlatformRunner::debug_new(
1764 "runner".into(),
1765 Vec::new(),
1766 PlatformRunnerSource::Env("fake".to_owned()),
1767 );
1768 let wrapper_overrides = WrapperScriptConfig {
1769 command: ScriptCommand {
1770 program: "wrapper".into(),
1771 args: Vec::new(),
1772 relative_to: ScriptCommandRelativeTo::None,
1773 },
1774 target_runner: WrapperScriptTargetRunner::OverridesWrapper,
1775 };
1776 let mut cli_wrapper_overrides = TestCommandCli::default();
1777 cli_wrapper_overrides.apply_wrappers(
1778 Some(&wrapper_overrides),
1779 Some(&runner),
1780 workspace_root,
1781 target_dir,
1782 );
1783 cli_wrapper_overrides.extend(["binary", "arg"]);
1784 assert_eq!(
1785 cli_wrapper_overrides.to_owned_cli(),
1786 vec!["runner", "binary", "arg"],
1787 );
1788 }
1789
1790 {
1792 let wrapper_with_args = WrapperScriptConfig {
1793 command: ScriptCommand {
1794 program: "wrapper".into(),
1795 args: vec!["--flag".to_string(), "value".to_string()],
1796 relative_to: ScriptCommandRelativeTo::None,
1797 },
1798 target_runner: WrapperScriptTargetRunner::Ignore,
1799 };
1800 let mut cli_wrapper_args = TestCommandCli::default();
1801 cli_wrapper_args.apply_wrappers(
1802 Some(&wrapper_with_args),
1803 None,
1804 workspace_root,
1805 target_dir,
1806 );
1807 cli_wrapper_args.extend(["binary", "arg"]);
1808 assert_eq!(
1809 cli_wrapper_args.to_owned_cli(),
1810 vec!["wrapper", "--flag", "value", "binary", "arg"],
1811 );
1812 }
1813
1814 {
1816 let runner_with_args = PlatformRunner::debug_new(
1817 "runner".into(),
1818 vec!["--runner-flag".into(), "value".into()],
1819 PlatformRunnerSource::Env("fake".to_owned()),
1820 );
1821 let mut cli_runner_args = TestCommandCli::default();
1822 cli_runner_args.apply_wrappers(
1823 None,
1824 Some(&runner_with_args),
1825 workspace_root,
1826 target_dir,
1827 );
1828 cli_runner_args.extend(["binary", "arg"]);
1829 assert_eq!(
1830 cli_runner_args.to_owned_cli(),
1831 vec!["runner", "--runner-flag", "value", "binary", "arg"],
1832 );
1833 }
1834
1835 {
1837 let wrapper_relative_to_workspace_root = WrapperScriptConfig {
1838 command: ScriptCommand {
1839 program: "abc/def/my-wrapper".into(),
1840 args: vec!["--verbose".to_string()],
1841 relative_to: ScriptCommandRelativeTo::WorkspaceRoot,
1842 },
1843 target_runner: WrapperScriptTargetRunner::Ignore,
1844 };
1845 let mut cli_wrapper_relative = TestCommandCli::default();
1846 cli_wrapper_relative.apply_wrappers(
1847 Some(&wrapper_relative_to_workspace_root),
1848 None,
1849 workspace_root,
1850 target_dir,
1851 );
1852 cli_wrapper_relative.extend(["binary", "arg"]);
1853
1854 cfg_if::cfg_if! {
1855 if #[cfg(windows)] {
1856 let wrapper_path = "D:\\workspace\\root\\abc\\def\\my-wrapper";
1857 } else {
1858 let wrapper_path = "/workspace/root/abc/def/my-wrapper";
1859 }
1860 }
1861 assert_eq!(
1862 cli_wrapper_relative.to_owned_cli(),
1863 vec![wrapper_path, "--verbose", "binary", "arg"],
1864 );
1865 }
1866
1867 {
1869 let wrapper_relative_to_target = WrapperScriptConfig {
1870 command: ScriptCommand {
1871 program: "abc/def/my-wrapper".into(),
1872 args: vec!["--verbose".to_string()],
1873 relative_to: ScriptCommandRelativeTo::Target,
1874 },
1875 target_runner: WrapperScriptTargetRunner::Ignore,
1876 };
1877 let mut cli_wrapper_relative = TestCommandCli::default();
1878 cli_wrapper_relative.apply_wrappers(
1879 Some(&wrapper_relative_to_target),
1880 None,
1881 workspace_root,
1882 target_dir,
1883 );
1884 cli_wrapper_relative.extend(["binary", "arg"]);
1885 cfg_if::cfg_if! {
1886 if #[cfg(windows)] {
1887 let wrapper_path = "C:\\foo\\bar\\abc\\def\\my-wrapper";
1888 } else {
1889 let wrapper_path = "/foo/bar/abc/def/my-wrapper";
1890 }
1891 }
1892 assert_eq!(
1893 cli_wrapper_relative.to_owned_cli(),
1894 vec![wrapper_path, "--verbose", "binary", "arg"],
1895 );
1896 }
1897 }
1898
1899 static PACKAGE_GRAPH_FIXTURE: LazyLock<PackageGraph> = LazyLock::new(|| {
1900 static FIXTURE_JSON: &str = include_str!("../../../fixtures/cargo-metadata.json");
1901 let metadata = CargoMetadata::parse_json(FIXTURE_JSON).expect("fixture is valid JSON");
1902 metadata
1903 .build_graph()
1904 .expect("fixture is valid PackageGraph")
1905 });
1906
1907 static PACKAGE_METADATA_ID: &str = "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)";
1908 fn package_metadata() -> PackageMetadata<'static> {
1909 PACKAGE_GRAPH_FIXTURE
1910 .metadata(&PackageId::new(PACKAGE_METADATA_ID))
1911 .expect("package ID is valid")
1912 }
1913}