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