1use super::{DisplayFilterMatcher, TestListDisplayFilter};
5use crate::{
6 cargo_config::EnvironmentMap,
7 config::{EvaluatableProfile, TestSettings},
8 double_spawn::DoubleSpawnInfo,
9 errors::{CreateTestListError, FromMessagesError, WriteTestListError},
10 helpers::{convert_build_platform, dylib_path, dylib_path_envvar, write_test_name},
11 indenter::indented,
12 list::{BinaryList, OutputFormat, RustBuildMeta, Styles, TestListState},
13 reuse_build::PathMapper,
14 target_runner::{PlatformRunner, TargetRunner},
15 test_command::{LocalExecuteContext, TestCommand},
16 test_filter::{BinaryMismatchReason, FilterBinaryMatch, FilterBound, TestFilterBuilder},
17 write_str::WriteStr,
18};
19use camino::{Utf8Path, Utf8PathBuf};
20use debug_ignore::DebugIgnore;
21use futures::prelude::*;
22use guppy::{
23 PackageId,
24 graph::{PackageGraph, PackageMetadata},
25};
26use nextest_filtering::{BinaryQuery, EvalContext, TestQuery};
27use nextest_metadata::{
28 BuildPlatform, FilterMatch, MismatchReason, RustBinaryId, RustNonTestBinaryKind,
29 RustTestBinaryKind, RustTestBinarySummary, RustTestCaseSummary, RustTestSuiteStatusSummary,
30 RustTestSuiteSummary, TestListSummary,
31};
32use owo_colors::OwoColorize;
33use std::{
34 collections::{BTreeMap, BTreeSet},
35 ffi::{OsStr, OsString},
36 fmt, io,
37 path::PathBuf,
38 sync::{Arc, OnceLock},
39};
40use tokio::runtime::Runtime;
41use tracing::debug;
42
43#[derive(Clone, Debug)]
48pub struct RustTestArtifact<'g> {
49 pub binary_id: RustBinaryId,
51
52 pub package: PackageMetadata<'g>,
55
56 pub binary_path: Utf8PathBuf,
58
59 pub binary_name: String,
61
62 pub kind: RustTestBinaryKind,
64
65 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
67
68 pub cwd: Utf8PathBuf,
70
71 pub build_platform: BuildPlatform,
73}
74
75impl<'g> RustTestArtifact<'g> {
76 pub fn from_binary_list(
78 graph: &'g PackageGraph,
79 binary_list: Arc<BinaryList>,
80 rust_build_meta: &RustBuildMeta<TestListState>,
81 path_mapper: &PathMapper,
82 platform_filter: Option<BuildPlatform>,
83 ) -> Result<Vec<Self>, FromMessagesError> {
84 let mut binaries = vec![];
85
86 for binary in &binary_list.rust_binaries {
87 if platform_filter.is_some() && platform_filter != Some(binary.build_platform) {
88 continue;
89 }
90
91 let package_id = PackageId::new(binary.package_id.clone());
93 let package = graph
94 .metadata(&package_id)
95 .map_err(FromMessagesError::PackageGraph)?;
96
97 let cwd = package
99 .manifest_path()
100 .parent()
101 .unwrap_or_else(|| {
102 panic!(
103 "manifest path {} doesn't have a parent",
104 package.manifest_path()
105 )
106 })
107 .to_path_buf();
108
109 let binary_path = path_mapper.map_binary(binary.path.clone());
110 let cwd = path_mapper.map_cwd(cwd);
111
112 let non_test_binaries = if binary.kind == RustTestBinaryKind::TEST
114 || binary.kind == RustTestBinaryKind::BENCH
115 {
116 match rust_build_meta.non_test_binaries.get(package_id.repr()) {
119 Some(binaries) => binaries
120 .iter()
121 .filter(|binary| {
122 binary.kind == RustNonTestBinaryKind::BIN_EXE
124 })
125 .map(|binary| {
126 let abs_path = rust_build_meta.target_directory.join(&binary.path);
128 (binary.name.clone(), abs_path)
129 })
130 .collect(),
131 None => BTreeSet::new(),
132 }
133 } else {
134 BTreeSet::new()
135 };
136
137 binaries.push(RustTestArtifact {
138 binary_id: binary.id.clone(),
139 package,
140 binary_path,
141 binary_name: binary.name.clone(),
142 kind: binary.kind.clone(),
143 cwd,
144 non_test_binaries,
145 build_platform: binary.build_platform,
146 })
147 }
148
149 Ok(binaries)
150 }
151
152 pub fn to_binary_query(&self) -> BinaryQuery<'_> {
154 BinaryQuery {
155 package_id: self.package.id(),
156 binary_id: &self.binary_id,
157 kind: &self.kind,
158 binary_name: &self.binary_name,
159 platform: convert_build_platform(self.build_platform),
160 }
161 }
162
163 fn into_test_suite(self, status: RustTestSuiteStatus) -> (RustBinaryId, RustTestSuite<'g>) {
167 let Self {
168 binary_id,
169 package,
170 binary_path,
171 binary_name,
172 kind,
173 non_test_binaries,
174 cwd,
175 build_platform,
176 } = self;
177 (
178 binary_id.clone(),
179 RustTestSuite {
180 binary_id,
181 binary_path,
182 package,
183 binary_name,
184 kind,
185 non_test_binaries,
186 cwd,
187 build_platform,
188 status,
189 },
190 )
191 }
192}
193
194#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct SkipCounts {
197 pub skipped_tests: usize,
199
200 pub skipped_tests_default_filter: usize,
202
203 pub skipped_binaries: usize,
205
206 pub skipped_binaries_default_filter: usize,
208}
209
210#[derive(Clone, Debug)]
212pub struct TestList<'g> {
213 test_count: usize,
214 rust_build_meta: RustBuildMeta<TestListState>,
215 rust_suites: BTreeMap<RustBinaryId, RustTestSuite<'g>>,
216 workspace_root: Utf8PathBuf,
217 env: EnvironmentMap,
218 updated_dylib_path: OsString,
219 skip_counts: OnceLock<SkipCounts>,
221}
222
223impl<'g> TestList<'g> {
224 #[expect(clippy::too_many_arguments)]
226 pub fn new<I>(
227 ctx: &TestExecuteContext<'_>,
228 test_artifacts: I,
229 rust_build_meta: RustBuildMeta<TestListState>,
230 filter: &TestFilterBuilder,
231 workspace_root: Utf8PathBuf,
232 env: EnvironmentMap,
233 ecx: &EvalContext<'_>,
234 bound: FilterBound,
235 list_threads: usize,
236 ) -> Result<Self, CreateTestListError>
237 where
238 I: IntoIterator<Item = RustTestArtifact<'g>>,
239 I::IntoIter: Send,
240 {
241 let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
242 debug!(
243 "updated {}: {}",
244 dylib_path_envvar(),
245 updated_dylib_path.to_string_lossy(),
246 );
247 let lctx = LocalExecuteContext {
248 rust_build_meta: &rust_build_meta,
249 double_spawn: ctx.double_spawn,
250 dylib_path: &updated_dylib_path,
251 profile_name: ctx.profile_name,
252 env: &env,
253 };
254
255 let runtime = Runtime::new().map_err(CreateTestListError::TokioRuntimeCreate)?;
256
257 let stream = futures::stream::iter(test_artifacts).map(|test_binary| {
258 async {
259 let binary_match = filter.filter_binary_match(&test_binary, ecx, bound);
260 match binary_match {
261 FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
262 debug!(
263 "executing test binary to obtain test list \
264 (match result is {binary_match:?}): {}",
265 test_binary.binary_id,
266 );
267 let (non_ignored, ignored) =
269 test_binary.exec(&lctx, ctx.target_runner).await?;
270 let (bin, info) = Self::process_output(
271 test_binary,
272 filter,
273 ecx,
274 bound,
275 non_ignored.as_str(),
276 ignored.as_str(),
277 )?;
278 Ok::<_, CreateTestListError>((bin, info))
279 }
280 FilterBinaryMatch::Mismatch { reason } => {
281 debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
282 Ok(Self::process_skipped(test_binary, reason))
283 }
284 }
285 }
286 });
287 let fut = stream.buffer_unordered(list_threads).try_collect();
288
289 let rust_suites: BTreeMap<_, _> = runtime.block_on(fut)?;
290
291 runtime.shutdown_background();
294
295 let test_count = rust_suites
296 .values()
297 .map(|suite| suite.status.test_count())
298 .sum();
299
300 Ok(Self {
301 rust_suites,
302 workspace_root,
303 env,
304 rust_build_meta,
305 updated_dylib_path,
306 test_count,
307 skip_counts: OnceLock::new(),
308 })
309 }
310
311 #[cfg(test)]
313 fn new_with_outputs(
314 test_bin_outputs: impl IntoIterator<
315 Item = (RustTestArtifact<'g>, impl AsRef<str>, impl AsRef<str>),
316 >,
317 workspace_root: Utf8PathBuf,
318 rust_build_meta: RustBuildMeta<TestListState>,
319 filter: &TestFilterBuilder,
320 env: EnvironmentMap,
321 ecx: &EvalContext<'_>,
322 bound: FilterBound,
323 ) -> Result<Self, CreateTestListError> {
324 let mut test_count = 0;
325
326 let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
327
328 let rust_suites = test_bin_outputs
329 .into_iter()
330 .map(|(test_binary, non_ignored, ignored)| {
331 let binary_match = filter.filter_binary_match(&test_binary, ecx, bound);
332 match binary_match {
333 FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
334 debug!(
335 "processing output for binary \
336 (match result is {binary_match:?}): {}",
337 test_binary.binary_id,
338 );
339 let (bin, info) = Self::process_output(
340 test_binary,
341 filter,
342 ecx,
343 bound,
344 non_ignored.as_ref(),
345 ignored.as_ref(),
346 )?;
347 test_count += info.status.test_count();
348 Ok((bin, info))
349 }
350 FilterBinaryMatch::Mismatch { reason } => {
351 debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
352 Ok(Self::process_skipped(test_binary, reason))
353 }
354 }
355 })
356 .collect::<Result<BTreeMap<_, _>, _>>()?;
357
358 Ok(Self {
359 rust_suites,
360 workspace_root,
361 env,
362 rust_build_meta,
363 updated_dylib_path,
364 test_count,
365 skip_counts: OnceLock::new(),
366 })
367 }
368
369 pub fn test_count(&self) -> usize {
371 self.test_count
372 }
373
374 pub fn rust_build_meta(&self) -> &RustBuildMeta<TestListState> {
376 &self.rust_build_meta
377 }
378
379 pub fn skip_counts(&self) -> &SkipCounts {
381 self.skip_counts.get_or_init(|| {
382 let mut skipped_tests_default_filter = 0;
383 let skipped_tests = self
384 .iter_tests()
385 .filter(|instance| match instance.test_info.filter_match {
386 FilterMatch::Mismatch {
387 reason: MismatchReason::DefaultFilter,
388 } => {
389 skipped_tests_default_filter += 1;
390 true
391 }
392 FilterMatch::Mismatch { .. } => true,
393 FilterMatch::Matches => false,
394 })
395 .count();
396
397 let mut skipped_binaries_default_filter = 0;
398 let skipped_binaries = self
399 .rust_suites
400 .values()
401 .filter(|suite| match suite.status {
402 RustTestSuiteStatus::Skipped {
403 reason: BinaryMismatchReason::DefaultSet,
404 } => {
405 skipped_binaries_default_filter += 1;
406 true
407 }
408 RustTestSuiteStatus::Skipped { .. } => true,
409 RustTestSuiteStatus::Listed { .. } => false,
410 })
411 .count();
412
413 SkipCounts {
414 skipped_tests,
415 skipped_tests_default_filter,
416 skipped_binaries,
417 skipped_binaries_default_filter,
418 }
419 })
420 }
421
422 pub fn run_count(&self) -> usize {
426 self.test_count - self.skip_counts().skipped_tests
427 }
428
429 pub fn binary_count(&self) -> usize {
431 self.rust_suites.len()
432 }
433
434 pub fn listed_binary_count(&self) -> usize {
436 self.binary_count() - self.skip_counts().skipped_binaries
437 }
438
439 pub fn workspace_root(&self) -> &Utf8Path {
441 &self.workspace_root
442 }
443
444 pub fn cargo_env(&self) -> &EnvironmentMap {
446 &self.env
447 }
448
449 pub fn updated_dylib_path(&self) -> &OsStr {
451 &self.updated_dylib_path
452 }
453
454 pub fn to_summary(&self) -> TestListSummary {
456 let rust_suites = self
457 .rust_suites
458 .values()
459 .map(|test_suite| {
460 let (status, test_cases) = test_suite.status.to_summary();
461 let testsuite = RustTestSuiteSummary {
462 package_name: test_suite.package.name().to_owned(),
463 binary: RustTestBinarySummary {
464 binary_name: test_suite.binary_name.clone(),
465 package_id: test_suite.package.id().repr().to_owned(),
466 kind: test_suite.kind.clone(),
467 binary_path: test_suite.binary_path.clone(),
468 binary_id: test_suite.binary_id.clone(),
469 build_platform: test_suite.build_platform,
470 },
471 cwd: test_suite.cwd.clone(),
472 status,
473 test_cases,
474 };
475 (test_suite.binary_id.clone(), testsuite)
476 })
477 .collect();
478 let mut summary = TestListSummary::new(self.rust_build_meta.to_summary());
479 summary.test_count = self.test_count;
480 summary.rust_suites = rust_suites;
481 summary
482 }
483
484 pub fn write(
486 &self,
487 output_format: OutputFormat,
488 writer: &mut dyn WriteStr,
489 colorize: bool,
490 ) -> Result<(), WriteTestListError> {
491 match output_format {
492 OutputFormat::Human { verbose } => self
493 .write_human(writer, verbose, colorize)
494 .map_err(WriteTestListError::Io),
495 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
496 }
497 }
498
499 pub fn iter(&self) -> impl Iterator<Item = &RustTestSuite> + '_ {
501 self.rust_suites.values()
502 }
503
504 pub fn iter_tests(&self) -> impl Iterator<Item = TestInstance<'_>> + '_ {
506 self.rust_suites.values().flat_map(|test_suite| {
507 test_suite
508 .status
509 .test_cases()
510 .map(move |(name, test_info)| TestInstance::new(name, test_suite, test_info))
511 })
512 }
513
514 pub fn to_priority_queue(
516 &'g self,
517 profile: &'g EvaluatableProfile<'g>,
518 ) -> TestPriorityQueue<'g> {
519 TestPriorityQueue::new(self, profile)
520 }
521
522 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
524 let mut s = String::with_capacity(1024);
525 self.write(output_format, &mut s, false)?;
526 Ok(s)
527 }
528
529 #[cfg(test)]
535 pub(crate) fn empty() -> Self {
536 Self {
537 test_count: 0,
538 workspace_root: Utf8PathBuf::new(),
539 rust_build_meta: RustBuildMeta::empty(),
540 env: EnvironmentMap::empty(),
541 updated_dylib_path: OsString::new(),
542 rust_suites: BTreeMap::new(),
543 skip_counts: OnceLock::new(),
544 }
545 }
546
547 pub(crate) fn create_dylib_path(
548 rust_build_meta: &RustBuildMeta<TestListState>,
549 ) -> Result<OsString, CreateTestListError> {
550 let dylib_path = dylib_path();
551 let dylib_path_is_empty = dylib_path.is_empty();
552 let new_paths = rust_build_meta.dylib_paths();
553
554 let mut updated_dylib_path: Vec<PathBuf> =
555 Vec::with_capacity(dylib_path.len() + new_paths.len());
556 updated_dylib_path.extend(
557 new_paths
558 .iter()
559 .map(|path| path.clone().into_std_path_buf()),
560 );
561 updated_dylib_path.extend(dylib_path);
562
563 if cfg!(target_os = "macos") && dylib_path_is_empty {
570 if let Some(home) = home::home_dir() {
571 updated_dylib_path.push(home.join("lib"));
572 }
573 updated_dylib_path.push("/usr/local/lib".into());
574 updated_dylib_path.push("/usr/lib".into());
575 }
576
577 std::env::join_paths(updated_dylib_path)
578 .map_err(move |error| CreateTestListError::dylib_join_paths(new_paths, error))
579 }
580
581 fn process_output(
582 test_binary: RustTestArtifact<'g>,
583 filter: &TestFilterBuilder,
584 ecx: &EvalContext<'_>,
585 bound: FilterBound,
586 non_ignored: impl AsRef<str>,
587 ignored: impl AsRef<str>,
588 ) -> Result<(RustBinaryId, RustTestSuite<'g>), CreateTestListError> {
589 let mut test_cases = BTreeMap::new();
590
591 let mut non_ignored_filter = filter.build();
594 for test_name in Self::parse(&test_binary.binary_id, non_ignored.as_ref())? {
595 test_cases.insert(
596 test_name.into(),
597 RustTestCaseSummary {
598 ignored: false,
599 filter_match: non_ignored_filter.filter_match(
600 &test_binary,
601 test_name,
602 ecx,
603 bound,
604 false,
605 ),
606 },
607 );
608 }
609
610 let mut ignored_filter = filter.build();
611 for test_name in Self::parse(&test_binary.binary_id, ignored.as_ref())? {
612 test_cases.insert(
617 test_name.into(),
618 RustTestCaseSummary {
619 ignored: true,
620 filter_match: ignored_filter.filter_match(
621 &test_binary,
622 test_name,
623 ecx,
624 bound,
625 true,
626 ),
627 },
628 );
629 }
630
631 Ok(test_binary.into_test_suite(RustTestSuiteStatus::Listed {
632 test_cases: test_cases.into(),
633 }))
634 }
635
636 fn process_skipped(
637 test_binary: RustTestArtifact<'g>,
638 reason: BinaryMismatchReason,
639 ) -> (RustBinaryId, RustTestSuite<'g>) {
640 test_binary.into_test_suite(RustTestSuiteStatus::Skipped { reason })
641 }
642
643 fn parse<'a>(
645 binary_id: &'a RustBinaryId,
646 list_output: &'a str,
647 ) -> Result<Vec<&'a str>, CreateTestListError> {
648 let mut list = Self::parse_impl(binary_id, list_output).collect::<Result<Vec<_>, _>>()?;
649 list.sort_unstable();
650 Ok(list)
651 }
652
653 fn parse_impl<'a>(
654 binary_id: &'a RustBinaryId,
655 list_output: &'a str,
656 ) -> impl Iterator<Item = Result<&'a str, CreateTestListError>> + 'a {
657 list_output.lines().map(move |line| {
663 line.strip_suffix(": test")
664 .or_else(|| line.strip_suffix(": benchmark"))
665 .ok_or_else(|| {
666 CreateTestListError::parse_line(
667 binary_id.clone(),
668 format!(
669 "line '{line}' did not end with the string ': test' or ': benchmark'"
670 ),
671 list_output,
672 )
673 })
674 })
675 }
676
677 pub fn write_human(
679 &self,
680 writer: &mut dyn WriteStr,
681 verbose: bool,
682 colorize: bool,
683 ) -> io::Result<()> {
684 self.write_human_impl(None, writer, verbose, colorize)
685 }
686
687 pub(crate) fn write_human_with_filter(
689 &self,
690 filter: &TestListDisplayFilter<'_>,
691 writer: &mut dyn WriteStr,
692 verbose: bool,
693 colorize: bool,
694 ) -> io::Result<()> {
695 self.write_human_impl(Some(filter), writer, verbose, colorize)
696 }
697
698 fn write_human_impl(
699 &self,
700 filter: Option<&TestListDisplayFilter<'_>>,
701 mut writer: &mut dyn WriteStr,
702 verbose: bool,
703 colorize: bool,
704 ) -> io::Result<()> {
705 let mut styles = Styles::default();
706 if colorize {
707 styles.colorize();
708 }
709
710 for info in self.rust_suites.values() {
711 let matcher = match filter {
712 Some(filter) => match filter.matcher_for(&info.binary_id) {
713 Some(matcher) => matcher,
714 None => continue,
715 },
716 None => DisplayFilterMatcher::All,
717 };
718
719 if !verbose
722 && info
723 .status
724 .test_cases()
725 .all(|(_, test_case)| !test_case.filter_match.is_match())
726 {
727 continue;
728 }
729
730 writeln!(writer, "{}:", info.binary_id.style(styles.binary_id))?;
731 if verbose {
732 writeln!(
733 writer,
734 " {} {}",
735 "bin:".style(styles.field),
736 info.binary_path
737 )?;
738 writeln!(writer, " {} {}", "cwd:".style(styles.field), info.cwd)?;
739 writeln!(
740 writer,
741 " {} {}",
742 "build platform:".style(styles.field),
743 info.build_platform,
744 )?;
745 }
746
747 let mut indented = indented(writer).with_str(" ");
748
749 match &info.status {
750 RustTestSuiteStatus::Listed { test_cases } => {
751 let matching_tests: Vec<_> = test_cases
752 .iter()
753 .filter(|(name, _)| matcher.is_match(name))
754 .collect();
755 if matching_tests.is_empty() {
756 writeln!(indented, "(no tests)")?;
757 } else {
758 for (name, info) in matching_tests {
759 match (verbose, info.filter_match.is_match()) {
760 (_, true) => {
761 write_test_name(name, &styles, &mut indented)?;
762 writeln!(indented)?;
763 }
764 (true, false) => {
765 write_test_name(name, &styles, &mut indented)?;
766 writeln!(indented, " (skipped)")?;
767 }
768 (false, false) => {
769 }
771 }
772 }
773 }
774 }
775 RustTestSuiteStatus::Skipped { reason } => {
776 writeln!(indented, "(test binary {reason}, skipped)")?;
777 }
778 }
779
780 writer = indented.into_inner();
781 }
782 Ok(())
783 }
784}
785
786pub struct TestPriorityQueue<'a> {
788 tests: Vec<TestInstanceWithSettings<'a>>,
789}
790
791impl<'a> TestPriorityQueue<'a> {
792 fn new(test_list: &'a TestList<'a>, profile: &'a EvaluatableProfile<'a>) -> Self {
793 let mut tests = test_list
794 .iter_tests()
795 .map(|instance| {
796 let settings = profile.settings_for(&instance.to_test_query());
797 TestInstanceWithSettings { instance, settings }
798 })
799 .collect::<Vec<_>>();
800 tests.sort_by_key(|test| test.settings.priority());
803
804 Self { tests }
805 }
806}
807
808impl<'a> IntoIterator for TestPriorityQueue<'a> {
809 type Item = TestInstanceWithSettings<'a>;
810 type IntoIter = std::vec::IntoIter<Self::Item>;
811
812 fn into_iter(self) -> Self::IntoIter {
813 self.tests.into_iter()
814 }
815}
816
817#[derive(Debug)]
821pub struct TestInstanceWithSettings<'a> {
822 pub instance: TestInstance<'a>,
824
825 pub settings: TestSettings<'a>,
827}
828
829#[derive(Clone, Debug, Eq, PartialEq)]
833pub struct RustTestSuite<'g> {
834 pub binary_id: RustBinaryId,
836
837 pub binary_path: Utf8PathBuf,
839
840 pub package: PackageMetadata<'g>,
842
843 pub binary_name: String,
845
846 pub kind: RustTestBinaryKind,
848
849 pub cwd: Utf8PathBuf,
852
853 pub build_platform: BuildPlatform,
855
856 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
858
859 pub status: RustTestSuiteStatus,
861}
862
863impl RustTestArtifact<'_> {
864 async fn exec(
866 &self,
867 lctx: &LocalExecuteContext<'_>,
868 target_runner: &TargetRunner,
869 ) -> Result<(String, String), CreateTestListError> {
870 if !self.cwd.is_dir() {
873 return Err(CreateTestListError::CwdIsNotDir {
874 binary_id: self.binary_id.clone(),
875 cwd: self.cwd.clone(),
876 });
877 }
878 let platform_runner = target_runner.for_build_platform(self.build_platform);
879
880 let non_ignored = self.exec_single(false, lctx, platform_runner);
881 let ignored = self.exec_single(true, lctx, platform_runner);
882
883 let (non_ignored_out, ignored_out) = futures::future::join(non_ignored, ignored).await;
884 Ok((non_ignored_out?, ignored_out?))
885 }
886
887 async fn exec_single(
888 &self,
889 ignored: bool,
890 lctx: &LocalExecuteContext<'_>,
891 runner: Option<&PlatformRunner>,
892 ) -> Result<String, CreateTestListError> {
893 let mut argv = Vec::new();
894
895 let program: String = if let Some(runner) = runner {
896 argv.extend(runner.args());
897 argv.push(self.binary_path.as_str());
898 runner.binary().into()
899 } else {
900 debug_assert!(
901 self.binary_path.is_absolute(),
902 "binary path {} is absolute",
903 self.binary_path
904 );
905 self.binary_path.clone().into()
906 };
907
908 argv.extend(["--list", "--format", "terse"]);
909 if ignored {
910 argv.push("--ignored");
911 }
912
913 let cmd = TestCommand::new(
914 lctx,
915 program.clone(),
916 &argv,
917 &self.cwd,
918 &self.package,
919 &self.non_test_binaries,
920 );
921
922 let output =
923 cmd.wait_with_output()
924 .await
925 .map_err(|error| CreateTestListError::CommandExecFail {
926 binary_id: self.binary_id.clone(),
927 command: std::iter::once(program.clone())
928 .chain(argv.iter().map(|&s| s.to_owned()))
929 .collect(),
930 error,
931 })?;
932
933 if output.status.success() {
934 String::from_utf8(output.stdout).map_err(|err| CreateTestListError::CommandNonUtf8 {
935 binary_id: self.binary_id.clone(),
936 command: std::iter::once(program)
937 .chain(argv.iter().map(|&s| s.to_owned()))
938 .collect(),
939 stdout: err.into_bytes(),
940 stderr: output.stderr,
941 })
942 } else {
943 Err(CreateTestListError::CommandFail {
944 binary_id: self.binary_id.clone(),
945 command: std::iter::once(program)
946 .chain(argv.iter().map(|&s| s.to_owned()))
947 .collect(),
948 exit_status: output.status,
949 stdout: output.stdout,
950 stderr: output.stderr,
951 })
952 }
953 }
954}
955
956#[derive(Clone, Debug, Eq, PartialEq)]
960pub enum RustTestSuiteStatus {
961 Listed {
963 test_cases: DebugIgnore<BTreeMap<String, RustTestCaseSummary>>,
965 },
966
967 Skipped {
969 reason: BinaryMismatchReason,
971 },
972}
973
974static EMPTY_TEST_CASE_MAP: BTreeMap<String, RustTestCaseSummary> = BTreeMap::new();
975
976impl RustTestSuiteStatus {
977 pub fn test_count(&self) -> usize {
979 match self {
980 RustTestSuiteStatus::Listed { test_cases } => test_cases.len(),
981 RustTestSuiteStatus::Skipped { .. } => 0,
982 }
983 }
984
985 pub fn test_cases(&self) -> impl Iterator<Item = (&str, &RustTestCaseSummary)> + '_ {
987 match self {
988 RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
989 RustTestSuiteStatus::Skipped { .. } => {
990 EMPTY_TEST_CASE_MAP.iter()
992 }
993 }
994 .map(|(name, case)| (name.as_str(), case))
995 }
996
997 pub fn to_summary(
999 &self,
1000 ) -> (
1001 RustTestSuiteStatusSummary,
1002 BTreeMap<String, RustTestCaseSummary>,
1003 ) {
1004 match self {
1005 Self::Listed { test_cases } => {
1006 (RustTestSuiteStatusSummary::LISTED, test_cases.clone().0)
1007 }
1008 Self::Skipped {
1009 reason: BinaryMismatchReason::Expression,
1010 } => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
1011 Self::Skipped {
1012 reason: BinaryMismatchReason::DefaultSet,
1013 } => (
1014 RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER,
1015 BTreeMap::new(),
1016 ),
1017 }
1018 }
1019}
1020
1021#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1023pub struct TestInstance<'a> {
1024 pub name: &'a str,
1026
1027 pub suite_info: &'a RustTestSuite<'a>,
1029
1030 pub test_info: &'a RustTestCaseSummary,
1032}
1033
1034impl<'a> TestInstance<'a> {
1035 pub(crate) fn new(
1037 name: &'a (impl AsRef<str> + ?Sized),
1038 suite_info: &'a RustTestSuite,
1039 test_info: &'a RustTestCaseSummary,
1040 ) -> Self {
1041 Self {
1042 name: name.as_ref(),
1043 suite_info,
1044 test_info,
1045 }
1046 }
1047
1048 #[inline]
1051 pub fn id(&self) -> TestInstanceId<'a> {
1052 TestInstanceId {
1053 binary_id: &self.suite_info.binary_id,
1054 test_name: self.name,
1055 }
1056 }
1057
1058 pub fn to_test_query(&self) -> TestQuery<'a> {
1060 TestQuery {
1061 binary_query: BinaryQuery {
1062 package_id: self.suite_info.package.id(),
1063 binary_id: &self.suite_info.binary_id,
1064 kind: &self.suite_info.kind,
1065 binary_name: &self.suite_info.binary_name,
1066 platform: convert_build_platform(self.suite_info.build_platform),
1067 },
1068 test_name: self.name,
1069 }
1070 }
1071
1072 pub(crate) fn make_command(
1074 &self,
1075 ctx: &TestExecuteContext<'_>,
1076 test_list: &TestList<'_>,
1077 extra_args: &[String],
1078 ) -> TestCommand {
1079 let platform_runner = ctx
1080 .target_runner
1081 .for_build_platform(self.suite_info.build_platform);
1082 let mut args = Vec::new();
1085
1086 let program: String = match platform_runner {
1087 Some(runner) => {
1088 args.extend(runner.args());
1089 args.push(self.suite_info.binary_path.as_str());
1090 runner.binary().into()
1091 }
1092 None => self.suite_info.binary_path.to_owned().into(),
1093 };
1094
1095 args.extend(["--exact", self.name, "--nocapture"]);
1096 if self.test_info.ignored {
1097 args.push("--ignored");
1098 }
1099 args.extend(extra_args.iter().map(String::as_str));
1100
1101 let lctx = LocalExecuteContext {
1102 rust_build_meta: &test_list.rust_build_meta,
1103 double_spawn: ctx.double_spawn,
1104 dylib_path: test_list.updated_dylib_path(),
1105 profile_name: ctx.profile_name,
1106 env: &test_list.env,
1107 };
1108
1109 TestCommand::new(
1110 &lctx,
1111 program,
1112 &args,
1113 &self.suite_info.cwd,
1114 &self.suite_info.package,
1115 &self.suite_info.non_test_binaries,
1116 )
1117 }
1118}
1119
1120#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
1124pub struct TestInstanceId<'a> {
1125 pub binary_id: &'a RustBinaryId,
1127
1128 pub test_name: &'a str,
1130}
1131
1132impl fmt::Display for TestInstanceId<'_> {
1133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1134 write!(f, "{} {}", self.binary_id, self.test_name)
1135 }
1136}
1137
1138#[derive(Clone, Debug)]
1140pub struct TestExecuteContext<'a> {
1141 pub profile_name: &'a str,
1143
1144 pub double_spawn: &'a DoubleSpawnInfo,
1146
1147 pub target_runner: &'a TargetRunner,
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 use super::*;
1154 use crate::{
1155 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
1156 list::SerializableFormat,
1157 platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
1158 test_filter::{RunIgnored, TestFilterPatterns},
1159 };
1160 use guppy::CargoMetadata;
1161 use indoc::indoc;
1162 use maplit::btreemap;
1163 use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, ParseContext};
1164 use nextest_metadata::{FilterMatch, MismatchReason, PlatformLibdirUnavailable};
1165 use pretty_assertions::assert_eq;
1166 use std::sync::LazyLock;
1167 use target_spec::Platform;
1168
1169 #[test]
1170 fn test_parse_test_list() {
1171 let non_ignored_output = indoc! {"
1173 tests::foo::test_bar: test
1174 tests::baz::test_quux: test
1175 benches::bench_foo: benchmark
1176 "};
1177 let ignored_output = indoc! {"
1178 tests::ignored::test_bar: test
1179 tests::baz::test_ignored: test
1180 benches::ignored_bench_foo: benchmark
1181 "};
1182
1183 let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
1184
1185 let test_filter = TestFilterBuilder::new(
1186 RunIgnored::Default,
1187 None,
1188 TestFilterPatterns::default(),
1189 vec![
1191 Filterset::parse("platform(target)".to_owned(), &cx, FiltersetKind::Test).unwrap(),
1192 ],
1193 )
1194 .unwrap();
1195 let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
1196 let fake_binary_name = "fake-binary".to_owned();
1197 let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
1198
1199 let test_binary = RustTestArtifact {
1200 binary_path: "/fake/binary".into(),
1201 cwd: fake_cwd.clone(),
1202 package: package_metadata(),
1203 binary_name: fake_binary_name.clone(),
1204 binary_id: fake_binary_id.clone(),
1205 kind: RustTestBinaryKind::LIB,
1206 non_test_binaries: BTreeSet::new(),
1207 build_platform: BuildPlatform::Target,
1208 };
1209
1210 let skipped_binary_name = "skipped-binary".to_owned();
1211 let skipped_binary_id = RustBinaryId::new("fake-package::skipped-binary");
1212 let skipped_binary = RustTestArtifact {
1213 binary_path: "/fake/skipped-binary".into(),
1214 cwd: fake_cwd.clone(),
1215 package: package_metadata(),
1216 binary_name: skipped_binary_name.clone(),
1217 binary_id: skipped_binary_id.clone(),
1218 kind: RustTestBinaryKind::PROC_MACRO,
1219 non_test_binaries: BTreeSet::new(),
1220 build_platform: BuildPlatform::Host,
1221 };
1222
1223 let fake_triple = TargetTriple {
1224 platform: Platform::new(
1225 "aarch64-unknown-linux-gnu",
1226 target_spec::TargetFeatures::Unknown,
1227 )
1228 .unwrap(),
1229 source: TargetTripleSource::CliOption,
1230 location: TargetDefinitionLocation::Builtin,
1231 };
1232 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
1233 let build_platforms = BuildPlatforms {
1234 host: HostPlatform {
1235 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
1236 libdir: PlatformLibdir::Available(fake_host_libdir.into()),
1237 },
1238 target: Some(TargetPlatform {
1239 triple: fake_triple,
1240 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
1242 }),
1243 };
1244
1245 let fake_env = EnvironmentMap::empty();
1246 let rust_build_meta =
1247 RustBuildMeta::new("/fake", build_platforms).map_paths(&PathMapper::noop());
1248 let ecx = EvalContext {
1249 default_filter: &CompiledExpr::ALL,
1250 };
1251 let test_list = TestList::new_with_outputs(
1252 [
1253 (test_binary, &non_ignored_output, &ignored_output),
1254 (
1255 skipped_binary,
1256 &"should-not-show-up-stdout",
1257 &"should-not-show-up-stderr",
1258 ),
1259 ],
1260 Utf8PathBuf::from("/fake/path"),
1261 rust_build_meta,
1262 &test_filter,
1263 fake_env,
1264 &ecx,
1265 FilterBound::All,
1266 )
1267 .expect("valid output");
1268 assert_eq!(
1269 test_list.rust_suites,
1270 btreemap! {
1271 fake_binary_id.clone() => RustTestSuite {
1272 status: RustTestSuiteStatus::Listed {
1273 test_cases: btreemap! {
1274 "tests::foo::test_bar".to_owned() => RustTestCaseSummary {
1275 ignored: false,
1276 filter_match: FilterMatch::Matches,
1277 },
1278 "tests::baz::test_quux".to_owned() => RustTestCaseSummary {
1279 ignored: false,
1280 filter_match: FilterMatch::Matches,
1281 },
1282 "benches::bench_foo".to_owned() => RustTestCaseSummary {
1283 ignored: false,
1284 filter_match: FilterMatch::Matches,
1285 },
1286 "tests::ignored::test_bar".to_owned() => RustTestCaseSummary {
1287 ignored: true,
1288 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1289 },
1290 "tests::baz::test_ignored".to_owned() => RustTestCaseSummary {
1291 ignored: true,
1292 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1293 },
1294 "benches::ignored_bench_foo".to_owned() => RustTestCaseSummary {
1295 ignored: true,
1296 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1297 },
1298 }.into(),
1299 },
1300 cwd: fake_cwd.clone(),
1301 build_platform: BuildPlatform::Target,
1302 package: package_metadata(),
1303 binary_name: fake_binary_name,
1304 binary_id: fake_binary_id,
1305 binary_path: "/fake/binary".into(),
1306 kind: RustTestBinaryKind::LIB,
1307 non_test_binaries: BTreeSet::new(),
1308 },
1309 skipped_binary_id.clone() => RustTestSuite {
1310 status: RustTestSuiteStatus::Skipped {
1311 reason: BinaryMismatchReason::Expression,
1312 },
1313 cwd: fake_cwd,
1314 build_platform: BuildPlatform::Host,
1315 package: package_metadata(),
1316 binary_name: skipped_binary_name,
1317 binary_id: skipped_binary_id,
1318 binary_path: "/fake/skipped-binary".into(),
1319 kind: RustTestBinaryKind::PROC_MACRO,
1320 non_test_binaries: BTreeSet::new(),
1321 },
1322 }
1323 );
1324
1325 static EXPECTED_HUMAN: &str = indoc! {"
1327 fake-package::fake-binary:
1328 benches::bench_foo
1329 tests::baz::test_quux
1330 tests::foo::test_bar
1331 "};
1332 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {"
1333 fake-package::fake-binary:
1334 bin: /fake/binary
1335 cwd: /fake/cwd
1336 build platform: target
1337 benches::bench_foo
1338 benches::ignored_bench_foo (skipped)
1339 tests::baz::test_ignored (skipped)
1340 tests::baz::test_quux
1341 tests::foo::test_bar
1342 tests::ignored::test_bar (skipped)
1343 fake-package::skipped-binary:
1344 bin: /fake/skipped-binary
1345 cwd: /fake/cwd
1346 build platform: host
1347 (test binary didn't match filtersets, skipped)
1348 "};
1349 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
1350 {
1351 "rust-build-meta": {
1352 "target-directory": "/fake",
1353 "base-output-directories": [],
1354 "non-test-binaries": {},
1355 "build-script-out-dirs": {},
1356 "linked-paths": [],
1357 "platforms": {
1358 "host": {
1359 "platform": {
1360 "triple": "x86_64-unknown-linux-gnu",
1361 "target-features": "unknown"
1362 },
1363 "libdir": {
1364 "status": "available",
1365 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
1366 }
1367 },
1368 "targets": [
1369 {
1370 "platform": {
1371 "triple": "aarch64-unknown-linux-gnu",
1372 "target-features": "unknown"
1373 },
1374 "libdir": {
1375 "status": "unavailable",
1376 "reason": "test"
1377 }
1378 }
1379 ]
1380 },
1381 "target-platforms": [
1382 {
1383 "triple": "aarch64-unknown-linux-gnu",
1384 "target-features": "unknown"
1385 }
1386 ],
1387 "target-platform": "aarch64-unknown-linux-gnu"
1388 },
1389 "test-count": 6,
1390 "rust-suites": {
1391 "fake-package::fake-binary": {
1392 "package-name": "metadata-helper",
1393 "binary-id": "fake-package::fake-binary",
1394 "binary-name": "fake-binary",
1395 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
1396 "kind": "lib",
1397 "binary-path": "/fake/binary",
1398 "build-platform": "target",
1399 "cwd": "/fake/cwd",
1400 "status": "listed",
1401 "testcases": {
1402 "benches::bench_foo": {
1403 "ignored": false,
1404 "filter-match": {
1405 "status": "matches"
1406 }
1407 },
1408 "benches::ignored_bench_foo": {
1409 "ignored": true,
1410 "filter-match": {
1411 "status": "mismatch",
1412 "reason": "ignored"
1413 }
1414 },
1415 "tests::baz::test_ignored": {
1416 "ignored": true,
1417 "filter-match": {
1418 "status": "mismatch",
1419 "reason": "ignored"
1420 }
1421 },
1422 "tests::baz::test_quux": {
1423 "ignored": false,
1424 "filter-match": {
1425 "status": "matches"
1426 }
1427 },
1428 "tests::foo::test_bar": {
1429 "ignored": false,
1430 "filter-match": {
1431 "status": "matches"
1432 }
1433 },
1434 "tests::ignored::test_bar": {
1435 "ignored": true,
1436 "filter-match": {
1437 "status": "mismatch",
1438 "reason": "ignored"
1439 }
1440 }
1441 }
1442 },
1443 "fake-package::skipped-binary": {
1444 "package-name": "metadata-helper",
1445 "binary-id": "fake-package::skipped-binary",
1446 "binary-name": "skipped-binary",
1447 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
1448 "kind": "proc-macro",
1449 "binary-path": "/fake/skipped-binary",
1450 "build-platform": "host",
1451 "cwd": "/fake/cwd",
1452 "status": "skipped",
1453 "testcases": {}
1454 }
1455 }
1456 }"#};
1457
1458 assert_eq!(
1459 test_list
1460 .to_string(OutputFormat::Human { verbose: false })
1461 .expect("human succeeded"),
1462 EXPECTED_HUMAN
1463 );
1464 assert_eq!(
1465 test_list
1466 .to_string(OutputFormat::Human { verbose: true })
1467 .expect("human succeeded"),
1468 EXPECTED_HUMAN_VERBOSE
1469 );
1470 println!(
1471 "{}",
1472 test_list
1473 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1474 .expect("json-pretty succeeded")
1475 );
1476 assert_eq!(
1477 test_list
1478 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1479 .expect("json-pretty succeeded"),
1480 EXPECTED_JSON_PRETTY
1481 );
1482 }
1483
1484 static PACKAGE_GRAPH_FIXTURE: LazyLock<PackageGraph> = LazyLock::new(|| {
1485 static FIXTURE_JSON: &str = include_str!("../../../fixtures/cargo-metadata.json");
1486 let metadata = CargoMetadata::parse_json(FIXTURE_JSON).expect("fixture is valid JSON");
1487 metadata
1488 .build_graph()
1489 .expect("fixture is valid PackageGraph")
1490 });
1491
1492 static PACKAGE_METADATA_ID: &str = "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)";
1493 fn package_metadata() -> PackageMetadata<'static> {
1494 PACKAGE_GRAPH_FIXTURE
1495 .metadata(&PackageId::new(PACKAGE_METADATA_ID))
1496 .expect("package ID is valid")
1497 }
1498}