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