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