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, 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 test_count(&self) -> usize {
397 self.test_count
398 }
399
400 pub fn mode(&self) -> NextestRunMode {
402 self.mode
403 }
404
405 pub fn rust_build_meta(&self) -> &RustBuildMeta<TestListState> {
407 &self.rust_build_meta
408 }
409
410 pub fn skip_counts(&self) -> &SkipCounts {
412 self.skip_counts.get_or_init(|| {
413 let mut skipped_tests_non_benchmark = 0;
414 let mut skipped_tests_default_filter = 0;
415 let skipped_tests = self
416 .iter_tests()
417 .filter(|instance| match instance.test_info.filter_match {
418 FilterMatch::Mismatch {
419 reason: MismatchReason::NotBenchmark,
420 } => {
421 skipped_tests_non_benchmark += 1;
422 true
423 }
424 FilterMatch::Mismatch {
425 reason: MismatchReason::DefaultFilter,
426 } => {
427 skipped_tests_default_filter += 1;
428 true
429 }
430 FilterMatch::Mismatch { .. } => true,
431 FilterMatch::Matches => false,
432 })
433 .count();
434
435 let mut skipped_binaries_default_filter = 0;
436 let skipped_binaries = self
437 .rust_suites
438 .iter()
439 .filter(|suite| match suite.status {
440 RustTestSuiteStatus::Skipped {
441 reason: BinaryMismatchReason::DefaultSet,
442 } => {
443 skipped_binaries_default_filter += 1;
444 true
445 }
446 RustTestSuiteStatus::Skipped { .. } => true,
447 RustTestSuiteStatus::Listed { .. } => false,
448 })
449 .count();
450
451 SkipCounts {
452 skipped_tests,
453 skipped_tests_non_benchmark,
454 skipped_tests_default_filter,
455 skipped_binaries,
456 skipped_binaries_default_filter,
457 }
458 })
459 }
460
461 pub fn run_count(&self) -> usize {
465 self.test_count - self.skip_counts().skipped_tests
466 }
467
468 pub fn binary_count(&self) -> usize {
470 self.rust_suites.len()
471 }
472
473 pub fn listed_binary_count(&self) -> usize {
475 self.binary_count() - self.skip_counts().skipped_binaries
476 }
477
478 pub fn workspace_root(&self) -> &Utf8Path {
480 &self.workspace_root
481 }
482
483 pub fn cargo_env(&self) -> &EnvironmentMap {
485 &self.env
486 }
487
488 pub fn updated_dylib_path(&self) -> &OsStr {
490 &self.updated_dylib_path
491 }
492
493 pub fn to_summary(&self) -> TestListSummary {
495 let rust_suites = self
496 .rust_suites
497 .iter()
498 .map(|test_suite| {
499 let (status, test_cases) = test_suite.status.to_summary();
500 let testsuite = RustTestSuiteSummary {
501 package_name: test_suite.package.name().to_owned(),
502 binary: RustTestBinarySummary {
503 binary_name: test_suite.binary_name.clone(),
504 package_id: test_suite.package.id().repr().to_owned(),
505 kind: test_suite.kind.clone(),
506 binary_path: test_suite.binary_path.clone(),
507 binary_id: test_suite.binary_id.clone(),
508 build_platform: test_suite.build_platform,
509 },
510 cwd: test_suite.cwd.clone(),
511 status,
512 test_cases,
513 };
514 (test_suite.binary_id.clone(), testsuite)
515 })
516 .collect();
517 let mut summary = TestListSummary::new(self.rust_build_meta.to_summary());
518 summary.test_count = self.test_count;
519 summary.rust_suites = rust_suites;
520 summary
521 }
522
523 pub fn write(
525 &self,
526 output_format: OutputFormat,
527 writer: &mut dyn WriteStr,
528 colorize: bool,
529 ) -> Result<(), WriteTestListError> {
530 match output_format {
531 OutputFormat::Human { verbose } => self
532 .write_human(writer, verbose, colorize)
533 .map_err(WriteTestListError::Io),
534 OutputFormat::Oneline { verbose } => self
535 .write_oneline(writer, verbose, colorize)
536 .map_err(WriteTestListError::Io),
537 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
538 }
539 }
540
541 pub fn iter(&self) -> impl Iterator<Item = &RustTestSuite<'_>> + '_ {
543 self.rust_suites.iter()
544 }
545
546 pub fn get_suite(&self, binary_id: &RustBinaryId) -> Option<&RustTestSuite<'_>> {
548 self.rust_suites.get(binary_id)
549 }
550
551 pub fn iter_tests(&self) -> impl Iterator<Item = TestInstance<'_>> + '_ {
553 self.rust_suites.iter().flat_map(|test_suite| {
554 test_suite
555 .status
556 .test_cases()
557 .map(move |case| TestInstance::new(case, test_suite))
558 })
559 }
560
561 pub fn to_priority_queue(
563 &'g self,
564 profile: &'g EvaluatableProfile<'g>,
565 ) -> TestPriorityQueue<'g> {
566 TestPriorityQueue::new(self, profile)
567 }
568
569 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
571 let mut s = String::with_capacity(1024);
572 self.write(output_format, &mut s, false)?;
573 Ok(s)
574 }
575
576 #[cfg(test)]
582 pub(crate) fn empty() -> Self {
583 Self {
584 test_count: 0,
585 mode: NextestRunMode::Test,
586 workspace_root: Utf8PathBuf::new(),
587 rust_build_meta: RustBuildMeta::empty(),
588 env: EnvironmentMap::empty(),
589 updated_dylib_path: OsString::new(),
590 rust_suites: IdOrdMap::new(),
591 skip_counts: OnceLock::new(),
592 }
593 }
594
595 pub(crate) fn create_dylib_path(
596 rust_build_meta: &RustBuildMeta<TestListState>,
597 ) -> Result<OsString, CreateTestListError> {
598 let dylib_path = dylib_path();
599 let dylib_path_is_empty = dylib_path.is_empty();
600 let new_paths = rust_build_meta.dylib_paths();
601
602 let mut updated_dylib_path: Vec<PathBuf> =
603 Vec::with_capacity(dylib_path.len() + new_paths.len());
604 updated_dylib_path.extend(
605 new_paths
606 .iter()
607 .map(|path| path.clone().into_std_path_buf()),
608 );
609 updated_dylib_path.extend(dylib_path);
610
611 if cfg!(target_os = "macos") && dylib_path_is_empty {
618 if let Some(home) = home::home_dir() {
619 updated_dylib_path.push(home.join("lib"));
620 }
621 updated_dylib_path.push("/usr/local/lib".into());
622 updated_dylib_path.push("/usr/lib".into());
623 }
624
625 std::env::join_paths(updated_dylib_path)
626 .map_err(move |error| CreateTestListError::dylib_join_paths(new_paths, error))
627 }
628
629 fn process_output(
630 test_binary: RustTestArtifact<'g>,
631 filter: &TestFilterBuilder,
632 ecx: &EvalContext<'_>,
633 bound: FilterBound,
634 non_ignored: impl AsRef<str>,
635 ignored: impl AsRef<str>,
636 ) -> Result<RustTestSuite<'g>, CreateTestListError> {
637 let mut test_cases = IdOrdMap::new();
638
639 let mut non_ignored_filter = filter.build();
642 for (test_name, kind) in Self::parse(&test_binary.binary_id, non_ignored.as_ref())? {
643 let filter_match =
644 non_ignored_filter.filter_match(&test_binary, test_name, &kind, ecx, bound, false);
645 test_cases.insert_overwrite(RustTestCase {
646 name: test_name.into(),
647 test_info: RustTestCaseSummary {
648 kind: Some(kind),
649 ignored: false,
650 filter_match,
651 },
652 });
653 }
654
655 let mut ignored_filter = filter.build();
656 for (test_name, kind) in Self::parse(&test_binary.binary_id, ignored.as_ref())? {
657 let filter_match =
662 ignored_filter.filter_match(&test_binary, test_name, &kind, ecx, bound, true);
663 test_cases.insert_overwrite(RustTestCase {
664 name: test_name.into(),
665 test_info: RustTestCaseSummary {
666 kind: Some(kind),
667 ignored: true,
668 filter_match,
669 },
670 });
671 }
672
673 Ok(test_binary.into_test_suite(RustTestSuiteStatus::Listed {
674 test_cases: test_cases.into(),
675 }))
676 }
677
678 fn process_skipped(
679 test_binary: RustTestArtifact<'g>,
680 reason: BinaryMismatchReason,
681 ) -> RustTestSuite<'g> {
682 test_binary.into_test_suite(RustTestSuiteStatus::Skipped { reason })
683 }
684
685 fn parse<'a>(
687 binary_id: &'a RustBinaryId,
688 list_output: &'a str,
689 ) -> Result<Vec<(&'a str, RustTestKind)>, CreateTestListError> {
690 let mut list = parse_list_lines(binary_id, list_output).collect::<Result<Vec<_>, _>>()?;
691 list.sort_unstable();
692 Ok(list)
693 }
694
695 pub fn write_human(
697 &self,
698 writer: &mut dyn WriteStr,
699 verbose: bool,
700 colorize: bool,
701 ) -> io::Result<()> {
702 self.write_human_impl(None, writer, verbose, colorize)
703 }
704
705 pub(crate) fn write_human_with_filter(
707 &self,
708 filter: &TestListDisplayFilter<'_>,
709 writer: &mut dyn WriteStr,
710 verbose: bool,
711 colorize: bool,
712 ) -> io::Result<()> {
713 self.write_human_impl(Some(filter), writer, verbose, colorize)
714 }
715
716 fn write_human_impl(
717 &self,
718 filter: Option<&TestListDisplayFilter<'_>>,
719 mut writer: &mut dyn WriteStr,
720 verbose: bool,
721 colorize: bool,
722 ) -> io::Result<()> {
723 let mut styles = Styles::default();
724 if colorize {
725 styles.colorize();
726 }
727
728 for info in &self.rust_suites {
729 let matcher = match filter {
730 Some(filter) => match filter.matcher_for(&info.binary_id) {
731 Some(matcher) => matcher,
732 None => continue,
733 },
734 None => DisplayFilterMatcher::All,
735 };
736
737 if !verbose
740 && info
741 .status
742 .test_cases()
743 .all(|case| !case.test_info.filter_match.is_match())
744 {
745 continue;
746 }
747
748 writeln!(writer, "{}:", info.binary_id.style(styles.binary_id))?;
749 if verbose {
750 writeln!(
751 writer,
752 " {} {}",
753 "bin:".style(styles.field),
754 info.binary_path
755 )?;
756 writeln!(writer, " {} {}", "cwd:".style(styles.field), info.cwd)?;
757 writeln!(
758 writer,
759 " {} {}",
760 "build platform:".style(styles.field),
761 info.build_platform,
762 )?;
763 }
764
765 let mut indented = indented(writer).with_str(" ");
766
767 match &info.status {
768 RustTestSuiteStatus::Listed { test_cases } => {
769 let matching_tests: Vec<_> = test_cases
770 .iter()
771 .filter(|case| matcher.is_match(&case.name))
772 .collect();
773 if matching_tests.is_empty() {
774 writeln!(indented, "(no tests)")?;
775 } else {
776 for case in matching_tests {
777 match (verbose, case.test_info.filter_match.is_match()) {
778 (_, true) => {
779 write_test_name(&case.name, &styles, &mut indented)?;
780 writeln!(indented)?;
781 }
782 (true, false) => {
783 write_test_name(&case.name, &styles, &mut indented)?;
784 writeln!(indented, " (skipped)")?;
785 }
786 (false, false) => {
787 }
789 }
790 }
791 }
792 }
793 RustTestSuiteStatus::Skipped { reason } => {
794 writeln!(indented, "(test binary {reason}, skipped)")?;
795 }
796 }
797
798 writer = indented.into_inner();
799 }
800 Ok(())
801 }
802
803 pub fn write_oneline(
805 &self,
806 writer: &mut dyn WriteStr,
807 verbose: bool,
808 colorize: bool,
809 ) -> io::Result<()> {
810 let mut styles = Styles::default();
811 if colorize {
812 styles.colorize();
813 }
814
815 for info in &self.rust_suites {
816 match &info.status {
817 RustTestSuiteStatus::Listed { test_cases } => {
818 for case in test_cases.iter() {
819 let is_match = case.test_info.filter_match.is_match();
820 if !verbose && !is_match {
822 continue;
823 }
824
825 write!(writer, "{} ", info.binary_id.style(styles.binary_id))?;
826 write_test_name(&case.name, &styles, writer)?;
827
828 if verbose {
829 write!(
830 writer,
831 " [{}{}] [{}{}] [{}{}]{}",
832 "bin: ".style(styles.field),
833 info.binary_path,
834 "cwd: ".style(styles.field),
835 info.cwd,
836 "build platform: ".style(styles.field),
837 info.build_platform,
838 if is_match { "" } else { " (skipped)" },
839 )?;
840 }
841
842 writeln!(writer)?;
843 }
844 }
845 RustTestSuiteStatus::Skipped { .. } => {
846 }
848 }
849 }
850
851 Ok(())
852 }
853}
854
855fn parse_list_lines<'a>(
856 binary_id: &'a RustBinaryId,
857 list_output: &'a str,
858) -> impl Iterator<Item = Result<(&'a str, RustTestKind), CreateTestListError>> + 'a + use<'a> {
859 list_output
865 .lines()
866 .map(move |line| match line.strip_suffix(": test") {
867 Some(test_name) => Ok((test_name, RustTestKind::TEST)),
868 None => match line.strip_suffix(": benchmark") {
869 Some(test_name) => Ok((test_name, RustTestKind::BENCH)),
870 None => Err(CreateTestListError::parse_line(
871 binary_id.clone(),
872 format!(
873 "line {line:?} did not end with the string \": test\" or \": benchmark\""
874 ),
875 list_output,
876 )),
877 },
878 })
879}
880
881pub trait ListProfile {
883 fn filterset_ecx(&self) -> EvalContext<'_>;
885
886 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_>;
888}
889
890impl<'g> ListProfile for EvaluatableProfile<'g> {
891 fn filterset_ecx(&self) -> EvalContext<'_> {
892 self.filterset_ecx()
893 }
894
895 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
896 self.list_settings_for(query)
897 }
898}
899
900pub struct TestPriorityQueue<'a> {
902 tests: Vec<TestInstanceWithSettings<'a>>,
903}
904
905impl<'a> TestPriorityQueue<'a> {
906 fn new(test_list: &'a TestList<'a>, profile: &'a EvaluatableProfile<'a>) -> Self {
907 let mode = test_list.mode();
908 let mut tests = test_list
909 .iter_tests()
910 .map(|instance| {
911 let settings = profile.settings_for(mode, &instance.to_test_query());
912 TestInstanceWithSettings { instance, settings }
913 })
914 .collect::<Vec<_>>();
915 tests.sort_by_key(|test| test.settings.priority());
918
919 Self { tests }
920 }
921}
922
923impl<'a> IntoIterator for TestPriorityQueue<'a> {
924 type Item = TestInstanceWithSettings<'a>;
925 type IntoIter = std::vec::IntoIter<Self::Item>;
926
927 fn into_iter(self) -> Self::IntoIter {
928 self.tests.into_iter()
929 }
930}
931
932#[derive(Debug)]
936pub struct TestInstanceWithSettings<'a> {
937 pub instance: TestInstance<'a>,
939
940 pub settings: TestSettings<'a>,
942}
943
944#[derive(Clone, Debug, Eq, PartialEq)]
948pub struct RustTestSuite<'g> {
949 pub binary_id: RustBinaryId,
951
952 pub binary_path: Utf8PathBuf,
954
955 pub package: PackageMetadata<'g>,
957
958 pub binary_name: String,
960
961 pub kind: RustTestBinaryKind,
963
964 pub cwd: Utf8PathBuf,
967
968 pub build_platform: BuildPlatform,
970
971 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
973
974 pub status: RustTestSuiteStatus,
976}
977
978impl IdOrdItem for RustTestSuite<'_> {
979 type Key<'a>
980 = &'a RustBinaryId
981 where
982 Self: 'a;
983
984 fn key(&self) -> Self::Key<'_> {
985 &self.binary_id
986 }
987
988 id_upcast!();
989}
990
991impl RustTestArtifact<'_> {
992 async fn exec(
994 &self,
995 lctx: &LocalExecuteContext<'_>,
996 list_settings: &ListSettings<'_>,
997 target_runner: &TargetRunner,
998 ) -> Result<(String, String), CreateTestListError> {
999 if !self.cwd.is_dir() {
1002 return Err(CreateTestListError::CwdIsNotDir {
1003 binary_id: self.binary_id.clone(),
1004 cwd: self.cwd.clone(),
1005 });
1006 }
1007 let platform_runner = target_runner.for_build_platform(self.build_platform);
1008
1009 let non_ignored = self.exec_single(false, lctx, list_settings, platform_runner);
1010 let ignored = self.exec_single(true, lctx, list_settings, platform_runner);
1011
1012 let (non_ignored_out, ignored_out) = futures::future::join(non_ignored, ignored).await;
1013 Ok((non_ignored_out?, ignored_out?))
1014 }
1015
1016 async fn exec_single(
1017 &self,
1018 ignored: bool,
1019 lctx: &LocalExecuteContext<'_>,
1020 list_settings: &ListSettings<'_>,
1021 runner: Option<&PlatformRunner>,
1022 ) -> Result<String, CreateTestListError> {
1023 let mut cli = TestCommandCli::default();
1024 cli.apply_wrappers(
1025 list_settings.list_wrapper(),
1026 runner,
1027 lctx.workspace_root,
1028 &lctx.rust_build_meta.target_directory,
1029 );
1030 cli.push(self.binary_path.as_str());
1031
1032 cli.extend(["--list", "--format", "terse"]);
1033 if ignored {
1034 cli.push("--ignored");
1035 }
1036
1037 let cmd = TestCommand::new(
1038 lctx,
1039 cli.program
1040 .clone()
1041 .expect("at least one argument passed in")
1042 .into_owned(),
1043 &cli.args,
1044 &self.cwd,
1045 &self.package,
1046 &self.non_test_binaries,
1047 &Interceptor::None, );
1049
1050 let output =
1051 cmd.wait_with_output()
1052 .await
1053 .map_err(|error| CreateTestListError::CommandExecFail {
1054 binary_id: self.binary_id.clone(),
1055 command: cli.to_owned_cli(),
1056 error,
1057 })?;
1058
1059 if output.status.success() {
1060 String::from_utf8(output.stdout).map_err(|err| CreateTestListError::CommandNonUtf8 {
1061 binary_id: self.binary_id.clone(),
1062 command: cli.to_owned_cli(),
1063 stdout: err.into_bytes(),
1064 stderr: output.stderr,
1065 })
1066 } else {
1067 Err(CreateTestListError::CommandFail {
1068 binary_id: self.binary_id.clone(),
1069 command: cli.to_owned_cli(),
1070 exit_status: output.status,
1071 stdout: output.stdout,
1072 stderr: output.stderr,
1073 })
1074 }
1075 }
1076}
1077
1078#[derive(Clone, Debug, Eq, PartialEq)]
1082pub enum RustTestSuiteStatus {
1083 Listed {
1085 test_cases: DebugIgnore<IdOrdMap<RustTestCase>>,
1087 },
1088
1089 Skipped {
1091 reason: BinaryMismatchReason,
1093 },
1094}
1095
1096static EMPTY_TEST_CASE_MAP: IdOrdMap<RustTestCase> = IdOrdMap::new();
1097
1098impl RustTestSuiteStatus {
1099 pub fn test_count(&self) -> usize {
1101 match self {
1102 RustTestSuiteStatus::Listed { test_cases } => test_cases.len(),
1103 RustTestSuiteStatus::Skipped { .. } => 0,
1104 }
1105 }
1106
1107 pub fn test_cases(&self) -> impl Iterator<Item = &RustTestCase> + '_ {
1109 match self {
1110 RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
1111 RustTestSuiteStatus::Skipped { .. } => {
1112 EMPTY_TEST_CASE_MAP.iter()
1114 }
1115 }
1116 }
1117
1118 pub fn to_summary(
1120 &self,
1121 ) -> (
1122 RustTestSuiteStatusSummary,
1123 BTreeMap<String, RustTestCaseSummary>,
1124 ) {
1125 match self {
1126 Self::Listed { test_cases } => (
1127 RustTestSuiteStatusSummary::LISTED,
1128 test_cases
1129 .iter()
1130 .cloned()
1131 .map(|case| (case.name, case.test_info))
1132 .collect(),
1133 ),
1134 Self::Skipped {
1135 reason: BinaryMismatchReason::Expression,
1136 } => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
1137 Self::Skipped {
1138 reason: BinaryMismatchReason::DefaultSet,
1139 } => (
1140 RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER,
1141 BTreeMap::new(),
1142 ),
1143 }
1144 }
1145}
1146
1147#[derive(Clone, Debug, Eq, PartialEq)]
1149pub struct RustTestCase {
1150 pub name: String,
1152
1153 pub test_info: RustTestCaseSummary,
1155}
1156
1157impl IdOrdItem for RustTestCase {
1158 type Key<'a> = &'a str;
1159 fn key(&self) -> Self::Key<'_> {
1160 &self.name
1161 }
1162 id_upcast!();
1163}
1164
1165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1167pub struct TestInstance<'a> {
1168 pub name: &'a str,
1170
1171 pub suite_info: &'a RustTestSuite<'a>,
1173
1174 pub test_info: &'a RustTestCaseSummary,
1176}
1177
1178impl<'a> TestInstance<'a> {
1179 pub(crate) fn new(case: &'a RustTestCase, suite_info: &'a RustTestSuite) -> Self {
1181 Self {
1182 name: &case.name,
1183 suite_info,
1184 test_info: &case.test_info,
1185 }
1186 }
1187
1188 #[inline]
1191 pub fn id(&self) -> TestInstanceId<'a> {
1192 TestInstanceId {
1193 binary_id: &self.suite_info.binary_id,
1194 test_name: self.name,
1195 }
1196 }
1197
1198 pub fn to_test_query(&self) -> TestQuery<'a> {
1200 TestQuery {
1201 binary_query: BinaryQuery {
1202 package_id: self.suite_info.package.id(),
1203 binary_id: &self.suite_info.binary_id,
1204 kind: &self.suite_info.kind,
1205 binary_name: &self.suite_info.binary_name,
1206 platform: convert_build_platform(self.suite_info.build_platform),
1207 },
1208 test_name: self.name,
1209 }
1210 }
1211
1212 pub(crate) fn make_command(
1214 &self,
1215 ctx: &TestExecuteContext<'_>,
1216 test_list: &TestList<'_>,
1217 wrapper_script: Option<&WrapperScriptConfig>,
1218 extra_args: &[String],
1219 interceptor: &Interceptor,
1220 ) -> TestCommand {
1221 let cli = self.compute_cli(ctx, test_list, wrapper_script, extra_args);
1223
1224 let lctx = LocalExecuteContext {
1225 phase: TestCommandPhase::Run,
1226 workspace_root: test_list.workspace_root(),
1227 rust_build_meta: &test_list.rust_build_meta,
1228 double_spawn: ctx.double_spawn,
1229 dylib_path: test_list.updated_dylib_path(),
1230 profile_name: ctx.profile_name,
1231 env: &test_list.env,
1232 };
1233
1234 TestCommand::new(
1235 &lctx,
1236 cli.program
1237 .expect("at least one argument is guaranteed")
1238 .into_owned(),
1239 &cli.args,
1240 &self.suite_info.cwd,
1241 &self.suite_info.package,
1242 &self.suite_info.non_test_binaries,
1243 interceptor,
1244 )
1245 }
1246
1247 pub(crate) fn command_line(
1248 &self,
1249 ctx: &TestExecuteContext<'_>,
1250 test_list: &TestList<'_>,
1251 wrapper_script: Option<&WrapperScriptConfig>,
1252 extra_args: &[String],
1253 ) -> Vec<String> {
1254 self.compute_cli(ctx, test_list, wrapper_script, extra_args)
1255 .to_owned_cli()
1256 }
1257
1258 fn compute_cli(
1259 &self,
1260 ctx: &'a TestExecuteContext<'_>,
1261 test_list: &TestList<'_>,
1262 wrapper_script: Option<&'a WrapperScriptConfig>,
1263 extra_args: &'a [String],
1264 ) -> TestCommandCli<'a> {
1265 let platform_runner = ctx
1266 .target_runner
1267 .for_build_platform(self.suite_info.build_platform);
1268
1269 let mut cli = TestCommandCli::default();
1270 cli.apply_wrappers(
1271 wrapper_script,
1272 platform_runner,
1273 test_list.workspace_root(),
1274 &test_list.rust_build_meta().target_directory,
1275 );
1276 cli.push(self.suite_info.binary_path.as_str());
1277
1278 cli.extend(["--exact", self.name, "--nocapture"]);
1279 if self.test_info.ignored {
1280 cli.push("--ignored");
1281 }
1282 match test_list.mode() {
1283 NextestRunMode::Test => {}
1284 NextestRunMode::Benchmark => {
1285 cli.push("--bench");
1286 }
1287 }
1288 cli.extend(extra_args.iter().map(String::as_str));
1289
1290 cli
1291 }
1292}
1293
1294#[derive(Clone, Debug, Default)]
1295struct TestCommandCli<'a> {
1296 program: Option<Cow<'a, str>>,
1297 args: Vec<Cow<'a, str>>,
1298}
1299
1300impl<'a> TestCommandCli<'a> {
1301 fn apply_wrappers(
1302 &mut self,
1303 wrapper_script: Option<&'a WrapperScriptConfig>,
1304 platform_runner: Option<&'a PlatformRunner>,
1305 workspace_root: &Utf8Path,
1306 target_dir: &Utf8Path,
1307 ) {
1308 if let Some(wrapper) = wrapper_script {
1310 match wrapper.target_runner {
1311 WrapperScriptTargetRunner::Ignore => {
1312 self.push(wrapper.command.program(workspace_root, target_dir));
1314 self.extend(wrapper.command.args.iter().map(String::as_str));
1315 }
1316 WrapperScriptTargetRunner::AroundWrapper => {
1317 if let Some(runner) = platform_runner {
1319 self.push(runner.binary());
1320 self.extend(runner.args());
1321 }
1322 self.push(wrapper.command.program(workspace_root, target_dir));
1323 self.extend(wrapper.command.args.iter().map(String::as_str));
1324 }
1325 WrapperScriptTargetRunner::WithinWrapper => {
1326 self.push(wrapper.command.program(workspace_root, target_dir));
1328 self.extend(wrapper.command.args.iter().map(String::as_str));
1329 if let Some(runner) = platform_runner {
1330 self.push(runner.binary());
1331 self.extend(runner.args());
1332 }
1333 }
1334 WrapperScriptTargetRunner::OverridesWrapper => {
1335 if let Some(runner) = platform_runner {
1337 self.push(runner.binary());
1338 self.extend(runner.args());
1339 }
1340 }
1341 }
1342 } else {
1343 if let Some(runner) = platform_runner {
1345 self.push(runner.binary());
1346 self.extend(runner.args());
1347 }
1348 }
1349 }
1350
1351 fn push(&mut self, arg: impl Into<Cow<'a, str>>) {
1352 if self.program.is_none() {
1353 self.program = Some(arg.into());
1354 } else {
1355 self.args.push(arg.into());
1356 }
1357 }
1358
1359 fn extend(&mut self, args: impl IntoIterator<Item = &'a str>) {
1360 for arg in args {
1361 self.push(arg);
1362 }
1363 }
1364
1365 fn to_owned_cli(&self) -> Vec<String> {
1366 let mut owned_cli = Vec::new();
1367 if let Some(program) = &self.program {
1368 owned_cli.push(program.to_string());
1369 }
1370 owned_cli.extend(self.args.iter().map(|arg| arg.clone().into_owned()));
1371 owned_cli
1372 }
1373}
1374
1375#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize)]
1379pub struct TestInstanceId<'a> {
1380 pub binary_id: &'a RustBinaryId,
1382
1383 pub test_name: &'a str,
1385}
1386
1387impl TestInstanceId<'_> {
1388 pub fn attempt_id(
1392 &self,
1393 run_id: ReportUuid,
1394 stress_index: Option<u32>,
1395 attempt: u32,
1396 ) -> String {
1397 let mut out = String::new();
1398 swrite!(out, "{run_id}:{}", self.binary_id);
1399 if let Some(stress_index) = stress_index {
1400 swrite!(out, "@stress-{}", stress_index);
1401 }
1402 swrite!(out, "${}", self.test_name);
1403 if attempt > 1 {
1404 swrite!(out, "#{attempt}");
1405 }
1406
1407 out
1408 }
1409}
1410
1411impl fmt::Display for TestInstanceId<'_> {
1412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1413 write!(f, "{} {}", self.binary_id, self.test_name)
1414 }
1415}
1416
1417#[derive(Clone, Debug, PartialEq, Eq)]
1419pub struct OwnedTestInstanceId {
1420 pub binary_id: RustBinaryId,
1422
1423 pub test_name: String,
1425}
1426
1427impl OwnedTestInstanceId {
1428 pub fn as_ref(&self) -> TestInstanceId<'_> {
1430 TestInstanceId {
1431 binary_id: &self.binary_id,
1432 test_name: &self.test_name,
1433 }
1434 }
1435}
1436
1437impl TestInstanceId<'_> {
1438 pub fn to_owned(&self) -> OwnedTestInstanceId {
1440 OwnedTestInstanceId {
1441 binary_id: self.binary_id.clone(),
1442 test_name: self.test_name.to_string(),
1443 }
1444 }
1445}
1446
1447#[derive(Clone, Debug)]
1449pub struct TestExecuteContext<'a> {
1450 pub profile_name: &'a str,
1452
1453 pub double_spawn: &'a DoubleSpawnInfo,
1455
1456 pub target_runner: &'a TargetRunner,
1458}
1459
1460#[cfg(test)]
1461mod tests {
1462 use super::*;
1463 use crate::{
1464 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
1465 config::scripts::{ScriptCommand, ScriptCommandRelativeTo},
1466 list::SerializableFormat,
1467 platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
1468 target_runner::PlatformRunnerSource,
1469 test_filter::{RunIgnored, TestFilterPatterns},
1470 };
1471 use guppy::CargoMetadata;
1472 use iddqd::id_ord_map;
1473 use indoc::indoc;
1474 use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, ParseContext};
1475 use nextest_metadata::{FilterMatch, MismatchReason, PlatformLibdirUnavailable, RustTestKind};
1476 use pretty_assertions::assert_eq;
1477 use std::sync::LazyLock;
1478 use target_spec::Platform;
1479
1480 #[test]
1481 fn test_parse_test_list() {
1482 let non_ignored_output = indoc! {"
1484 tests::foo::test_bar: test
1485 tests::baz::test_quux: test
1486 benches::bench_foo: benchmark
1487 "};
1488 let ignored_output = indoc! {"
1489 tests::ignored::test_bar: test
1490 tests::baz::test_ignored: test
1491 benches::ignored_bench_foo: benchmark
1492 "};
1493
1494 let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
1495
1496 let test_filter = TestFilterBuilder::new(
1497 NextestRunMode::Test,
1498 RunIgnored::Default,
1499 None,
1500 TestFilterPatterns::default(),
1501 vec![
1503 Filterset::parse("platform(target)".to_owned(), &cx, FiltersetKind::Test).unwrap(),
1504 ],
1505 )
1506 .unwrap();
1507 let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
1508 let fake_binary_name = "fake-binary".to_owned();
1509 let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
1510
1511 let test_binary = RustTestArtifact {
1512 binary_path: "/fake/binary".into(),
1513 cwd: fake_cwd.clone(),
1514 package: package_metadata(),
1515 binary_name: fake_binary_name.clone(),
1516 binary_id: fake_binary_id.clone(),
1517 kind: RustTestBinaryKind::LIB,
1518 non_test_binaries: BTreeSet::new(),
1519 build_platform: BuildPlatform::Target,
1520 };
1521
1522 let skipped_binary_name = "skipped-binary".to_owned();
1523 let skipped_binary_id = RustBinaryId::new("fake-package::skipped-binary");
1524 let skipped_binary = RustTestArtifact {
1525 binary_path: "/fake/skipped-binary".into(),
1526 cwd: fake_cwd.clone(),
1527 package: package_metadata(),
1528 binary_name: skipped_binary_name.clone(),
1529 binary_id: skipped_binary_id.clone(),
1530 kind: RustTestBinaryKind::PROC_MACRO,
1531 non_test_binaries: BTreeSet::new(),
1532 build_platform: BuildPlatform::Host,
1533 };
1534
1535 let fake_triple = TargetTriple {
1536 platform: Platform::new(
1537 "aarch64-unknown-linux-gnu",
1538 target_spec::TargetFeatures::Unknown,
1539 )
1540 .unwrap(),
1541 source: TargetTripleSource::CliOption,
1542 location: TargetDefinitionLocation::Builtin,
1543 };
1544 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
1545 let build_platforms = BuildPlatforms {
1546 host: HostPlatform {
1547 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
1548 libdir: PlatformLibdir::Available(fake_host_libdir.into()),
1549 },
1550 target: Some(TargetPlatform {
1551 triple: fake_triple,
1552 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
1554 }),
1555 };
1556
1557 let fake_env = EnvironmentMap::empty();
1558 let rust_build_meta =
1559 RustBuildMeta::new("/fake", build_platforms).map_paths(&PathMapper::noop());
1560 let ecx = EvalContext {
1561 default_filter: &CompiledExpr::ALL,
1562 };
1563 let test_list = TestList::new_with_outputs(
1564 [
1565 (test_binary, &non_ignored_output, &ignored_output),
1566 (
1567 skipped_binary,
1568 &"should-not-show-up-stdout",
1569 &"should-not-show-up-stderr",
1570 ),
1571 ],
1572 Utf8PathBuf::from("/fake/path"),
1573 rust_build_meta,
1574 &test_filter,
1575 fake_env,
1576 &ecx,
1577 FilterBound::All,
1578 )
1579 .expect("valid output");
1580 assert_eq!(
1581 test_list.rust_suites,
1582 id_ord_map! {
1583 RustTestSuite {
1584 status: RustTestSuiteStatus::Listed {
1585 test_cases: id_ord_map! {
1586 RustTestCase {
1587 name: "tests::foo::test_bar".to_owned(),
1588 test_info: RustTestCaseSummary {
1589 kind: Some(RustTestKind::TEST),
1590 ignored: false,
1591 filter_match: FilterMatch::Matches,
1592 },
1593 },
1594 RustTestCase {
1595 name: "tests::baz::test_quux".to_owned(),
1596 test_info: RustTestCaseSummary {
1597 kind: Some(RustTestKind::TEST),
1598 ignored: false,
1599 filter_match: FilterMatch::Matches,
1600 },
1601 },
1602 RustTestCase {
1603 name: "benches::bench_foo".to_owned(),
1604 test_info: RustTestCaseSummary {
1605 kind: Some(RustTestKind::BENCH),
1606 ignored: false,
1607 filter_match: FilterMatch::Matches,
1608 },
1609 },
1610 RustTestCase {
1611 name: "tests::ignored::test_bar".to_owned(),
1612 test_info: RustTestCaseSummary {
1613 kind: Some(RustTestKind::TEST),
1614 ignored: true,
1615 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1616 },
1617 },
1618 RustTestCase {
1619 name: "tests::baz::test_ignored".to_owned(),
1620 test_info: RustTestCaseSummary {
1621 kind: Some(RustTestKind::TEST),
1622 ignored: true,
1623 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1624 },
1625 },
1626 RustTestCase {
1627 name: "benches::ignored_bench_foo".to_owned(),
1628 test_info: RustTestCaseSummary {
1629 kind: Some(RustTestKind::BENCH),
1630 ignored: true,
1631 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1632 },
1633 },
1634 }.into(),
1635 },
1636 cwd: fake_cwd.clone(),
1637 build_platform: BuildPlatform::Target,
1638 package: package_metadata(),
1639 binary_name: fake_binary_name,
1640 binary_id: fake_binary_id,
1641 binary_path: "/fake/binary".into(),
1642 kind: RustTestBinaryKind::LIB,
1643 non_test_binaries: BTreeSet::new(),
1644 },
1645 RustTestSuite {
1646 status: RustTestSuiteStatus::Skipped {
1647 reason: BinaryMismatchReason::Expression,
1648 },
1649 cwd: fake_cwd,
1650 build_platform: BuildPlatform::Host,
1651 package: package_metadata(),
1652 binary_name: skipped_binary_name,
1653 binary_id: skipped_binary_id,
1654 binary_path: "/fake/skipped-binary".into(),
1655 kind: RustTestBinaryKind::PROC_MACRO,
1656 non_test_binaries: BTreeSet::new(),
1657 },
1658 }
1659 );
1660
1661 static EXPECTED_HUMAN: &str = indoc! {"
1663 fake-package::fake-binary:
1664 benches::bench_foo
1665 tests::baz::test_quux
1666 tests::foo::test_bar
1667 "};
1668 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {"
1669 fake-package::fake-binary:
1670 bin: /fake/binary
1671 cwd: /fake/cwd
1672 build platform: target
1673 benches::bench_foo
1674 benches::ignored_bench_foo (skipped)
1675 tests::baz::test_ignored (skipped)
1676 tests::baz::test_quux
1677 tests::foo::test_bar
1678 tests::ignored::test_bar (skipped)
1679 fake-package::skipped-binary:
1680 bin: /fake/skipped-binary
1681 cwd: /fake/cwd
1682 build platform: host
1683 (test binary didn't match filtersets, skipped)
1684 "};
1685 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
1686 {
1687 "rust-build-meta": {
1688 "target-directory": "/fake",
1689 "base-output-directories": [],
1690 "non-test-binaries": {},
1691 "build-script-out-dirs": {},
1692 "linked-paths": [],
1693 "platforms": {
1694 "host": {
1695 "platform": {
1696 "triple": "x86_64-unknown-linux-gnu",
1697 "target-features": "unknown"
1698 },
1699 "libdir": {
1700 "status": "available",
1701 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
1702 }
1703 },
1704 "targets": [
1705 {
1706 "platform": {
1707 "triple": "aarch64-unknown-linux-gnu",
1708 "target-features": "unknown"
1709 },
1710 "libdir": {
1711 "status": "unavailable",
1712 "reason": "test"
1713 }
1714 }
1715 ]
1716 },
1717 "target-platforms": [
1718 {
1719 "triple": "aarch64-unknown-linux-gnu",
1720 "target-features": "unknown"
1721 }
1722 ],
1723 "target-platform": "aarch64-unknown-linux-gnu"
1724 },
1725 "test-count": 6,
1726 "rust-suites": {
1727 "fake-package::fake-binary": {
1728 "package-name": "metadata-helper",
1729 "binary-id": "fake-package::fake-binary",
1730 "binary-name": "fake-binary",
1731 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
1732 "kind": "lib",
1733 "binary-path": "/fake/binary",
1734 "build-platform": "target",
1735 "cwd": "/fake/cwd",
1736 "status": "listed",
1737 "testcases": {
1738 "benches::bench_foo": {
1739 "kind": "bench",
1740 "ignored": false,
1741 "filter-match": {
1742 "status": "matches"
1743 }
1744 },
1745 "benches::ignored_bench_foo": {
1746 "kind": "bench",
1747 "ignored": true,
1748 "filter-match": {
1749 "status": "mismatch",
1750 "reason": "ignored"
1751 }
1752 },
1753 "tests::baz::test_ignored": {
1754 "kind": "test",
1755 "ignored": true,
1756 "filter-match": {
1757 "status": "mismatch",
1758 "reason": "ignored"
1759 }
1760 },
1761 "tests::baz::test_quux": {
1762 "kind": "test",
1763 "ignored": false,
1764 "filter-match": {
1765 "status": "matches"
1766 }
1767 },
1768 "tests::foo::test_bar": {
1769 "kind": "test",
1770 "ignored": false,
1771 "filter-match": {
1772 "status": "matches"
1773 }
1774 },
1775 "tests::ignored::test_bar": {
1776 "kind": "test",
1777 "ignored": true,
1778 "filter-match": {
1779 "status": "mismatch",
1780 "reason": "ignored"
1781 }
1782 }
1783 }
1784 },
1785 "fake-package::skipped-binary": {
1786 "package-name": "metadata-helper",
1787 "binary-id": "fake-package::skipped-binary",
1788 "binary-name": "skipped-binary",
1789 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
1790 "kind": "proc-macro",
1791 "binary-path": "/fake/skipped-binary",
1792 "build-platform": "host",
1793 "cwd": "/fake/cwd",
1794 "status": "skipped",
1795 "testcases": {}
1796 }
1797 }
1798 }"#};
1799 static EXPECTED_ONELINE: &str = indoc! {"
1800 fake-package::fake-binary benches::bench_foo
1801 fake-package::fake-binary tests::baz::test_quux
1802 fake-package::fake-binary tests::foo::test_bar
1803 "};
1804 static EXPECTED_ONELINE_VERBOSE: &str = indoc! {"
1805 fake-package::fake-binary benches::bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
1806 fake-package::fake-binary benches::ignored_bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
1807 fake-package::fake-binary tests::baz::test_ignored [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
1808 fake-package::fake-binary tests::baz::test_quux [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
1809 fake-package::fake-binary tests::foo::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
1810 fake-package::fake-binary tests::ignored::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
1811 "};
1812
1813 assert_eq!(
1814 test_list
1815 .to_string(OutputFormat::Human { verbose: false })
1816 .expect("human succeeded"),
1817 EXPECTED_HUMAN
1818 );
1819 assert_eq!(
1820 test_list
1821 .to_string(OutputFormat::Human { verbose: true })
1822 .expect("human succeeded"),
1823 EXPECTED_HUMAN_VERBOSE
1824 );
1825 println!(
1826 "{}",
1827 test_list
1828 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1829 .expect("json-pretty succeeded")
1830 );
1831 assert_eq!(
1832 test_list
1833 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1834 .expect("json-pretty succeeded"),
1835 EXPECTED_JSON_PRETTY
1836 );
1837 assert_eq!(
1838 test_list
1839 .to_string(OutputFormat::Oneline { verbose: false })
1840 .expect("oneline succeeded"),
1841 EXPECTED_ONELINE
1842 );
1843 assert_eq!(
1844 test_list
1845 .to_string(OutputFormat::Oneline { verbose: true })
1846 .expect("oneline verbose succeeded"),
1847 EXPECTED_ONELINE_VERBOSE
1848 );
1849 }
1850
1851 #[test]
1852 fn apply_wrappers_examples() {
1853 cfg_if::cfg_if! {
1854 if #[cfg(windows)]
1855 {
1856 let workspace_root = Utf8Path::new("D:\\workspace\\root");
1857 let target_dir = Utf8Path::new("C:\\foo\\bar");
1858 } else {
1859 let workspace_root = Utf8Path::new("/workspace/root");
1860 let target_dir = Utf8Path::new("/foo/bar");
1861 }
1862 };
1863
1864 {
1866 let mut cli_no_wrappers = TestCommandCli::default();
1867 cli_no_wrappers.apply_wrappers(None, None, workspace_root, target_dir);
1868 cli_no_wrappers.extend(["binary", "arg"]);
1869 assert_eq!(cli_no_wrappers.to_owned_cli(), vec!["binary", "arg"]);
1870 }
1871
1872 {
1874 let runner = PlatformRunner::debug_new(
1875 "runner".into(),
1876 Vec::new(),
1877 PlatformRunnerSource::Env("fake".to_owned()),
1878 );
1879 let mut cli_runner_only = TestCommandCli::default();
1880 cli_runner_only.apply_wrappers(None, Some(&runner), workspace_root, target_dir);
1881 cli_runner_only.extend(["binary", "arg"]);
1882 assert_eq!(
1883 cli_runner_only.to_owned_cli(),
1884 vec!["runner", "binary", "arg"],
1885 );
1886 }
1887
1888 {
1890 let runner = PlatformRunner::debug_new(
1891 "runner".into(),
1892 Vec::new(),
1893 PlatformRunnerSource::Env("fake".to_owned()),
1894 );
1895 let wrapper_ignore = WrapperScriptConfig {
1896 command: ScriptCommand {
1897 program: "wrapper".into(),
1898 args: Vec::new(),
1899 relative_to: ScriptCommandRelativeTo::None,
1900 },
1901 target_runner: WrapperScriptTargetRunner::Ignore,
1902 };
1903 let mut cli_wrapper_ignore = TestCommandCli::default();
1904 cli_wrapper_ignore.apply_wrappers(
1905 Some(&wrapper_ignore),
1906 Some(&runner),
1907 workspace_root,
1908 target_dir,
1909 );
1910 cli_wrapper_ignore.extend(["binary", "arg"]);
1911 assert_eq!(
1912 cli_wrapper_ignore.to_owned_cli(),
1913 vec!["wrapper", "binary", "arg"],
1914 );
1915 }
1916
1917 {
1919 let runner = PlatformRunner::debug_new(
1920 "runner".into(),
1921 Vec::new(),
1922 PlatformRunnerSource::Env("fake".to_owned()),
1923 );
1924 let wrapper_around = WrapperScriptConfig {
1925 command: ScriptCommand {
1926 program: "wrapper".into(),
1927 args: Vec::new(),
1928 relative_to: ScriptCommandRelativeTo::None,
1929 },
1930 target_runner: WrapperScriptTargetRunner::AroundWrapper,
1931 };
1932 let mut cli_wrapper_around = TestCommandCli::default();
1933 cli_wrapper_around.apply_wrappers(
1934 Some(&wrapper_around),
1935 Some(&runner),
1936 workspace_root,
1937 target_dir,
1938 );
1939 cli_wrapper_around.extend(["binary", "arg"]);
1940 assert_eq!(
1941 cli_wrapper_around.to_owned_cli(),
1942 vec!["runner", "wrapper", "binary", "arg"],
1943 );
1944 }
1945
1946 {
1948 let runner = PlatformRunner::debug_new(
1949 "runner".into(),
1950 Vec::new(),
1951 PlatformRunnerSource::Env("fake".to_owned()),
1952 );
1953 let wrapper_within = WrapperScriptConfig {
1954 command: ScriptCommand {
1955 program: "wrapper".into(),
1956 args: Vec::new(),
1957 relative_to: ScriptCommandRelativeTo::None,
1958 },
1959 target_runner: WrapperScriptTargetRunner::WithinWrapper,
1960 };
1961 let mut cli_wrapper_within = TestCommandCli::default();
1962 cli_wrapper_within.apply_wrappers(
1963 Some(&wrapper_within),
1964 Some(&runner),
1965 workspace_root,
1966 target_dir,
1967 );
1968 cli_wrapper_within.extend(["binary", "arg"]);
1969 assert_eq!(
1970 cli_wrapper_within.to_owned_cli(),
1971 vec!["wrapper", "runner", "binary", "arg"],
1972 );
1973 }
1974
1975 {
1977 let runner = PlatformRunner::debug_new(
1978 "runner".into(),
1979 Vec::new(),
1980 PlatformRunnerSource::Env("fake".to_owned()),
1981 );
1982 let wrapper_overrides = WrapperScriptConfig {
1983 command: ScriptCommand {
1984 program: "wrapper".into(),
1985 args: Vec::new(),
1986 relative_to: ScriptCommandRelativeTo::None,
1987 },
1988 target_runner: WrapperScriptTargetRunner::OverridesWrapper,
1989 };
1990 let mut cli_wrapper_overrides = TestCommandCli::default();
1991 cli_wrapper_overrides.apply_wrappers(
1992 Some(&wrapper_overrides),
1993 Some(&runner),
1994 workspace_root,
1995 target_dir,
1996 );
1997 cli_wrapper_overrides.extend(["binary", "arg"]);
1998 assert_eq!(
1999 cli_wrapper_overrides.to_owned_cli(),
2000 vec!["runner", "binary", "arg"],
2001 );
2002 }
2003
2004 {
2006 let wrapper_with_args = WrapperScriptConfig {
2007 command: ScriptCommand {
2008 program: "wrapper".into(),
2009 args: vec!["--flag".to_string(), "value".to_string()],
2010 relative_to: ScriptCommandRelativeTo::None,
2011 },
2012 target_runner: WrapperScriptTargetRunner::Ignore,
2013 };
2014 let mut cli_wrapper_args = TestCommandCli::default();
2015 cli_wrapper_args.apply_wrappers(
2016 Some(&wrapper_with_args),
2017 None,
2018 workspace_root,
2019 target_dir,
2020 );
2021 cli_wrapper_args.extend(["binary", "arg"]);
2022 assert_eq!(
2023 cli_wrapper_args.to_owned_cli(),
2024 vec!["wrapper", "--flag", "value", "binary", "arg"],
2025 );
2026 }
2027
2028 {
2030 let runner_with_args = PlatformRunner::debug_new(
2031 "runner".into(),
2032 vec!["--runner-flag".into(), "value".into()],
2033 PlatformRunnerSource::Env("fake".to_owned()),
2034 );
2035 let mut cli_runner_args = TestCommandCli::default();
2036 cli_runner_args.apply_wrappers(
2037 None,
2038 Some(&runner_with_args),
2039 workspace_root,
2040 target_dir,
2041 );
2042 cli_runner_args.extend(["binary", "arg"]);
2043 assert_eq!(
2044 cli_runner_args.to_owned_cli(),
2045 vec!["runner", "--runner-flag", "value", "binary", "arg"],
2046 );
2047 }
2048
2049 {
2051 let wrapper_relative_to_workspace_root = WrapperScriptConfig {
2052 command: ScriptCommand {
2053 program: "abc/def/my-wrapper".into(),
2054 args: vec!["--verbose".to_string()],
2055 relative_to: ScriptCommandRelativeTo::WorkspaceRoot,
2056 },
2057 target_runner: WrapperScriptTargetRunner::Ignore,
2058 };
2059 let mut cli_wrapper_relative = TestCommandCli::default();
2060 cli_wrapper_relative.apply_wrappers(
2061 Some(&wrapper_relative_to_workspace_root),
2062 None,
2063 workspace_root,
2064 target_dir,
2065 );
2066 cli_wrapper_relative.extend(["binary", "arg"]);
2067
2068 cfg_if::cfg_if! {
2069 if #[cfg(windows)] {
2070 let wrapper_path = "D:\\workspace\\root\\abc\\def\\my-wrapper";
2071 } else {
2072 let wrapper_path = "/workspace/root/abc/def/my-wrapper";
2073 }
2074 }
2075 assert_eq!(
2076 cli_wrapper_relative.to_owned_cli(),
2077 vec![wrapper_path, "--verbose", "binary", "arg"],
2078 );
2079 }
2080
2081 {
2083 let wrapper_relative_to_target = WrapperScriptConfig {
2084 command: ScriptCommand {
2085 program: "abc/def/my-wrapper".into(),
2086 args: vec!["--verbose".to_string()],
2087 relative_to: ScriptCommandRelativeTo::Target,
2088 },
2089 target_runner: WrapperScriptTargetRunner::Ignore,
2090 };
2091 let mut cli_wrapper_relative = TestCommandCli::default();
2092 cli_wrapper_relative.apply_wrappers(
2093 Some(&wrapper_relative_to_target),
2094 None,
2095 workspace_root,
2096 target_dir,
2097 );
2098 cli_wrapper_relative.extend(["binary", "arg"]);
2099 cfg_if::cfg_if! {
2100 if #[cfg(windows)] {
2101 let wrapper_path = "C:\\foo\\bar\\abc\\def\\my-wrapper";
2102 } else {
2103 let wrapper_path = "/foo/bar/abc/def/my-wrapper";
2104 }
2105 }
2106 assert_eq!(
2107 cli_wrapper_relative.to_owned_cli(),
2108 vec![wrapper_path, "--verbose", "binary", "arg"],
2109 );
2110 }
2111 }
2112
2113 static PACKAGE_GRAPH_FIXTURE: LazyLock<PackageGraph> = LazyLock::new(|| {
2114 static FIXTURE_JSON: &str = include_str!("../../../fixtures/cargo-metadata.json");
2115 let metadata = CargoMetadata::parse_json(FIXTURE_JSON).expect("fixture is valid JSON");
2116 metadata
2117 .build_graph()
2118 .expect("fixture is valid PackageGraph")
2119 });
2120
2121 static PACKAGE_METADATA_ID: &str = "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)";
2122 fn package_metadata() -> PackageMetadata<'static> {
2123 PACKAGE_GRAPH_FIXTURE
2124 .metadata(&PackageId::new(PACKAGE_METADATA_ID))
2125 .expect("package ID is valid")
2126 }
2127
2128 #[test]
2129 fn test_parse_list_lines() {
2130 let binary_id = RustBinaryId::new("test-package::test-binary");
2131
2132 let input = indoc! {"
2134 simple_test: test
2135 module::nested_test: test
2136 deeply::nested::module::test_name: test
2137 "};
2138 let results: Vec<_> = parse_list_lines(&binary_id, input)
2139 .collect::<Result<_, _>>()
2140 .expect("parsed valid test output");
2141 insta::assert_debug_snapshot!("valid_tests", results);
2142
2143 let input = indoc! {"
2145 simple_bench: benchmark
2146 benches::module::my_benchmark: benchmark
2147 "};
2148 let results: Vec<_> = parse_list_lines(&binary_id, input)
2149 .collect::<Result<_, _>>()
2150 .expect("parsed valid benchmark output");
2151 insta::assert_debug_snapshot!("valid_benchmarks", results);
2152
2153 let input = indoc! {"
2155 test_one: test
2156 bench_one: benchmark
2157 test_two: test
2158 bench_two: benchmark
2159 "};
2160 let results: Vec<_> = parse_list_lines(&binary_id, input)
2161 .collect::<Result<_, _>>()
2162 .expect("parsed mixed output");
2163 insta::assert_debug_snapshot!("mixed_tests_and_benchmarks", results);
2164
2165 let input = indoc! {r#"
2167 test_with_underscore_123: test
2168 test::with::colons: test
2169 test_with_numbers_42: test
2170 "#};
2171 let results: Vec<_> = parse_list_lines(&binary_id, input)
2172 .collect::<Result<_, _>>()
2173 .expect("parsed tests with special characters");
2174 insta::assert_debug_snapshot!("special_characters", results);
2175
2176 let input = "";
2178 let results: Vec<_> = parse_list_lines(&binary_id, input)
2179 .collect::<Result<_, _>>()
2180 .expect("parsed empty output");
2181 insta::assert_debug_snapshot!("empty_input", results);
2182
2183 let input = "invalid_test: wrong_suffix";
2185 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2186 assert!(result.is_err());
2187 insta::assert_snapshot!("invalid_suffix_error", result.unwrap_err());
2188
2189 let input = "test_without_suffix";
2191 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2192 assert!(result.is_err());
2193 insta::assert_snapshot!("missing_suffix_error", result.unwrap_err());
2194
2195 let input = indoc! {"
2197 valid_test: test
2198 invalid_line
2199 another_valid: benchmark
2200 "};
2201 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2202 assert!(result.is_err());
2203 insta::assert_snapshot!("partial_valid_error", result.unwrap_err());
2204
2205 let input = indoc! {"
2207 valid_test: test
2208 \rinvalid_line
2209 another_valid: benchmark
2210 "};
2211 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2212 assert!(result.is_err());
2213 insta::assert_snapshot!("control_character_error", result.unwrap_err());
2214 }
2215}