1use super::{DisplayFilterMatcher, TestListDisplayFilter};
5use crate::{
6 cargo_config::EnvironmentMap,
7 config::{
8 core::EvaluatableProfile,
9 overrides::{ListSettings, TestSettings, group_membership::PrecomputedGroupMembership},
10 scripts::{ScriptCommandEnvMap, WrapperScriptConfig, WrapperScriptTargetRunner},
11 },
12 double_spawn::DoubleSpawnInfo,
13 errors::{
14 CreateTestListError, FromMessagesError, TestListFromSummaryError, WriteTestListError,
15 },
16 helpers::{convert_build_platform, dylib_path, dylib_path_envvar, write_test_name},
17 indenter::indented,
18 list::{BinaryList, OutputFormat, RustBuildMeta, Styles, TestListState},
19 partition::{Partitioner, PartitionerBuilder, PartitionerScope},
20 reuse_build::PathMapper,
21 run_mode::NextestRunMode,
22 runner::Interceptor,
23 target_runner::{PlatformRunner, TargetRunner},
24 test_command::{LocalExecuteContext, TestCommand, TestCommandPhase},
25 test_filter::{BinaryMismatchReason, FilterBinaryMatch, FilterBound, TestFilter},
26 write_str::WriteStr,
27};
28use camino::{Utf8Path, Utf8PathBuf};
29use debug_ignore::DebugIgnore;
30use futures::prelude::*;
31use guppy::{
32 PackageId,
33 graph::{PackageGraph, PackageMetadata},
34};
35use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
36use nextest_filtering::{BinaryQuery, EvalContext, GroupLookup, TestQuery};
37use nextest_metadata::{
38 BuildPlatform, FilterMatch, MismatchReason, RustBinaryId, RustNonTestBinaryKind,
39 RustTestBinaryKind, RustTestBinarySummary, RustTestCaseSummary, RustTestKind,
40 RustTestSuiteStatusSummary, RustTestSuiteSummary, TestCaseName, TestListSummary,
41};
42use owo_colors::OwoColorize;
43use quick_junit::ReportUuid;
44use serde::{Deserialize, Serialize};
45use std::{
46 borrow::{Borrow, Cow},
47 collections::{BTreeMap, BTreeSet},
48 ffi::{OsStr, OsString},
49 fmt,
50 hash::{Hash, Hasher},
51 io,
52 path::PathBuf,
53 sync::{Arc, OnceLock},
54};
55use swrite::{SWrite, swrite};
56use tokio::runtime::Runtime;
57use tracing::debug;
58
59#[derive(Clone, Debug)]
64pub struct RustTestArtifact<'g> {
65 pub binary_id: RustBinaryId,
67
68 pub package: PackageMetadata<'g>,
71
72 pub binary_path: Utf8PathBuf,
74
75 pub binary_name: String,
77
78 pub kind: RustTestBinaryKind,
80
81 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
83
84 pub cwd: Utf8PathBuf,
86
87 pub build_platform: BuildPlatform,
89}
90
91impl<'g> RustTestArtifact<'g> {
92 pub fn from_binary_list(
94 graph: &'g PackageGraph,
95 binary_list: Arc<BinaryList>,
96 rust_build_meta: &RustBuildMeta<TestListState>,
97 path_mapper: &PathMapper,
98 platform_filter: Option<BuildPlatform>,
99 ) -> Result<Vec<Self>, FromMessagesError> {
100 let mut binaries = vec![];
101
102 for binary in &binary_list.rust_binaries {
103 if platform_filter.is_some() && platform_filter != Some(binary.build_platform) {
104 continue;
105 }
106
107 let package_id = PackageId::new(binary.package_id.clone());
109 let package = graph
110 .metadata(&package_id)
111 .map_err(FromMessagesError::PackageGraph)?;
112
113 let cwd = package
115 .manifest_path()
116 .parent()
117 .unwrap_or_else(|| {
118 panic!(
119 "manifest path {} doesn't have a parent",
120 package.manifest_path()
121 )
122 })
123 .to_path_buf();
124
125 let binary_path = path_mapper.map_build_path(binary.path.clone());
127 let cwd = path_mapper.map_cwd(cwd);
128
129 let non_test_binaries = if binary.kind == RustTestBinaryKind::TEST
131 || binary.kind == RustTestBinaryKind::BENCH
132 {
133 match rust_build_meta.non_test_binaries.get(package_id.repr()) {
136 Some(binaries) => binaries
137 .iter()
138 .filter(|binary| {
139 binary.kind == RustNonTestBinaryKind::BIN_EXE
141 })
142 .map(|binary| {
143 let abs_path = rust_build_meta.target_directory.join(&binary.path);
145 (binary.name.clone(), abs_path)
146 })
147 .collect(),
148 None => BTreeSet::new(),
149 }
150 } else {
151 BTreeSet::new()
152 };
153
154 binaries.push(RustTestArtifact {
155 binary_id: binary.id.clone(),
156 package,
157 binary_path,
158 binary_name: binary.name.clone(),
159 kind: binary.kind.clone(),
160 cwd,
161 non_test_binaries,
162 build_platform: binary.build_platform,
163 })
164 }
165
166 Ok(binaries)
167 }
168
169 pub fn to_binary_query(&self) -> BinaryQuery<'_> {
171 BinaryQuery {
172 package_id: self.package.id(),
173 binary_id: &self.binary_id,
174 kind: &self.kind,
175 binary_name: &self.binary_name,
176 platform: convert_build_platform(self.build_platform),
177 }
178 }
179
180 fn into_test_suite(self, status: RustTestSuiteStatus) -> RustTestSuite<'g> {
184 let Self {
185 binary_id,
186 package,
187 binary_path,
188 binary_name,
189 kind,
190 non_test_binaries,
191 cwd,
192 build_platform,
193 } = self;
194
195 RustTestSuite {
196 binary_id,
197 binary_path,
198 package,
199 binary_name,
200 kind,
201 non_test_binaries,
202 cwd,
203 build_platform,
204 status,
205 }
206 }
207}
208
209#[derive(Clone, Debug, Eq, PartialEq)]
211pub struct SkipCounts {
212 pub skipped_tests: usize,
214
215 pub skipped_tests_rerun: usize,
218
219 pub skipped_tests_non_benchmark: usize,
223
224 pub skipped_tests_default_filter: usize,
226
227 pub skipped_binaries: usize,
229
230 pub skipped_binaries_default_filter: usize,
232}
233
234#[derive(Clone, Debug)]
236pub struct TestList<'g> {
237 test_count: usize,
238 mode: NextestRunMode,
239 rust_build_meta: RustBuildMeta<TestListState>,
240 rust_suites: IdOrdMap<RustTestSuite<'g>>,
241 workspace_root: Utf8PathBuf,
242 env: EnvironmentMap,
243 updated_dylib_path: OsString,
244 skip_counts: OnceLock<SkipCounts>,
246}
247
248impl<'g> TestList<'g> {
249 #[expect(clippy::too_many_arguments)]
251 pub fn new<I>(
252 ctx: &TestExecuteContext<'_>,
253 test_artifacts: I,
254 rust_build_meta: RustBuildMeta<TestListState>,
255 filter: &TestFilter,
256 partitioner_builder: Option<&PartitionerBuilder>,
257 workspace_root: Utf8PathBuf,
258 env: EnvironmentMap,
259 profile: &impl ListProfile,
260 bound: FilterBound,
261 list_threads: usize,
262 ) -> Result<Self, CreateTestListError>
263 where
264 I: IntoIterator<Item = RustTestArtifact<'g>>,
265 I::IntoIter: Send,
266 {
267 let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
268 debug!(
269 "updated {}: {}",
270 dylib_path_envvar(),
271 updated_dylib_path.to_string_lossy(),
272 );
273 let lctx = LocalExecuteContext {
274 phase: TestCommandPhase::List,
275 workspace_root: &workspace_root,
278 rust_build_meta: &rust_build_meta,
279 double_spawn: ctx.double_spawn,
280 dylib_path: &updated_dylib_path,
281 profile_name: ctx.profile_name,
282 env: &env,
283 };
284
285 let ecx = profile.filterset_ecx();
286
287 let runtime = Runtime::new().map_err(CreateTestListError::TokioRuntimeCreate)?;
288
289 let stream = futures::stream::iter(test_artifacts).map(|test_binary| {
293 async {
294 let binary_query = test_binary.to_binary_query();
295 let binary_match = filter.filter_binary_match(&binary_query, &ecx, bound);
296 match binary_match {
297 FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
298 debug!(
299 "executing test binary to obtain test list \
300 (match result is {binary_match:?}): {}",
301 test_binary.binary_id,
302 );
303 let list_settings = profile.list_settings_for(&binary_query);
305 let (non_ignored, ignored) = test_binary
306 .exec(&lctx, &list_settings, ctx.target_runner)
307 .await?;
308 let parsed = Self::parse_output(
309 test_binary,
310 non_ignored.as_str(),
311 ignored.as_str(),
312 )?;
313 Ok::<_, CreateTestListError>(parsed)
314 }
315 FilterBinaryMatch::Mismatch { reason } => {
316 debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
317 Ok(Self::make_skipped(test_binary, reason))
318 }
319 }
320 }
321 });
322 let fut = stream
323 .buffer_unordered(list_threads)
324 .try_collect::<Vec<_>>();
325
326 let parsed_binaries: Vec<ParsedTestBinary<'g>> = runtime.block_on(fut)?;
327
328 runtime.shutdown_background();
331
332 let group_membership = if filter.has_group_predicates() {
338 let test_queries = Self::collect_test_queries_from_parsed(&parsed_binaries);
339 Some(profile.precompute_group_memberships(test_queries.into_iter()))
340 } else {
341 None
342 };
343 let groups = group_membership.as_ref().map(|g| g as &dyn GroupLookup);
344
345 let mut rust_suites = Self::build_suites(parsed_binaries, filter, &ecx, bound, groups);
346 Self::apply_partitioning(&mut rust_suites, partitioner_builder);
347
348 let test_count = rust_suites
349 .iter()
350 .map(|suite| suite.status.test_count())
351 .sum();
352
353 Ok(Self {
354 rust_suites,
355 mode: filter.mode(),
356 workspace_root,
357 env,
358 rust_build_meta,
359 updated_dylib_path,
360 test_count,
361 skip_counts: OnceLock::new(),
362 })
363 }
364
365 #[cfg(test)]
367 #[expect(clippy::too_many_arguments)]
368 pub(crate) fn new_with_outputs(
369 test_bin_outputs: impl IntoIterator<
370 Item = (RustTestArtifact<'g>, impl AsRef<str>, impl AsRef<str>),
371 >,
372 workspace_root: Utf8PathBuf,
373 rust_build_meta: RustBuildMeta<TestListState>,
374 filter: &TestFilter,
375 partitioner_builder: Option<&PartitionerBuilder>,
376 env: EnvironmentMap,
377 ecx: &EvalContext<'_>,
378 bound: FilterBound,
379 ) -> Result<Self, CreateTestListError> {
380 let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
381
382 let parsed_binaries = test_bin_outputs
383 .into_iter()
384 .map(|(test_binary, non_ignored, ignored)| {
385 let binary_query = test_binary.to_binary_query();
386 let binary_match = filter.filter_binary_match(&binary_query, ecx, bound);
387 match binary_match {
388 FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
389 debug!(
390 "processing output for binary \
391 (match result is {binary_match:?}): {}",
392 test_binary.binary_id,
393 );
394 Self::parse_output(test_binary, non_ignored.as_ref(), ignored.as_ref())
395 }
396 FilterBinaryMatch::Mismatch { reason } => {
397 debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
398 Ok(Self::make_skipped(test_binary, reason))
399 }
400 }
401 })
402 .collect::<Result<Vec<_>, _>>()?;
403
404 let mut rust_suites = Self::build_suites(parsed_binaries, filter, ecx, bound, None);
405
406 Self::apply_partitioning(&mut rust_suites, partitioner_builder);
407
408 let test_count = rust_suites
409 .iter()
410 .map(|suite| suite.status.test_count())
411 .sum();
412
413 Ok(Self {
414 rust_suites,
415 mode: filter.mode(),
416 workspace_root,
417 env,
418 rust_build_meta,
419 updated_dylib_path,
420 test_count,
421 skip_counts: OnceLock::new(),
422 })
423 }
424
425 pub fn from_summary(
431 graph: &'g PackageGraph,
432 summary: &TestListSummary,
433 mode: NextestRunMode,
434 ) -> Result<Self, TestListFromSummaryError> {
435 let rust_build_meta = RustBuildMeta::from_summary(summary.rust_build_meta.clone())
437 .map_err(TestListFromSummaryError::RustBuildMeta)?;
438
439 let workspace_root = graph.workspace().root().to_path_buf();
441
442 let env = EnvironmentMap::empty();
444
445 let updated_dylib_path = OsString::new();
447
448 let mut rust_suites = IdOrdMap::new();
450 let mut test_count = 0;
451
452 for (binary_id, suite_summary) in &summary.rust_suites {
453 let package_id = PackageId::new(suite_summary.binary.package_id.clone());
455 let package = graph.metadata(&package_id).map_err(|_| {
456 TestListFromSummaryError::PackageNotFound {
457 name: suite_summary.package_name.clone(),
458 package_id: suite_summary.binary.package_id.clone(),
459 }
460 })?;
461
462 let status = if suite_summary.status == RustTestSuiteStatusSummary::SKIPPED {
464 RustTestSuiteStatus::Skipped {
465 reason: BinaryMismatchReason::Expression,
466 }
467 } else if suite_summary.status == RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER {
468 RustTestSuiteStatus::Skipped {
469 reason: BinaryMismatchReason::DefaultSet,
470 }
471 } else {
472 let test_cases: IdOrdMap<RustTestCase> = suite_summary
474 .test_cases
475 .iter()
476 .map(|(name, info)| RustTestCase {
477 name: name.clone(),
478 test_info: info.clone(),
479 })
480 .collect();
481
482 test_count += test_cases.len();
483
484 RustTestSuiteStatus::Listed {
486 test_cases: DebugIgnore(test_cases),
487 }
488 };
489
490 let suite = RustTestSuite {
491 binary_id: binary_id.clone(),
492 binary_path: suite_summary.binary.binary_path.clone(),
493 package,
494 binary_name: suite_summary.binary.binary_name.clone(),
495 kind: suite_summary.binary.kind.clone(),
496 non_test_binaries: BTreeSet::new(), cwd: suite_summary.cwd.clone(),
498 build_platform: suite_summary.binary.build_platform,
499 status,
500 };
501
502 let _ = rust_suites.insert_unique(suite);
503 }
504
505 Ok(Self {
506 rust_suites,
507 mode,
508 workspace_root,
509 env,
510 rust_build_meta,
511 updated_dylib_path,
512 test_count,
513 skip_counts: OnceLock::new(),
514 })
515 }
516
517 pub fn test_count(&self) -> usize {
519 self.test_count
520 }
521
522 pub fn mode(&self) -> NextestRunMode {
524 self.mode
525 }
526
527 pub fn rust_build_meta(&self) -> &RustBuildMeta<TestListState> {
529 &self.rust_build_meta
530 }
531
532 pub fn skip_counts(&self) -> &SkipCounts {
534 self.skip_counts.get_or_init(|| {
535 let mut skipped_tests_rerun = 0;
536 let mut skipped_tests_non_benchmark = 0;
537 let mut skipped_tests_default_filter = 0;
538 let skipped_tests = self
539 .iter_tests()
540 .filter(|instance| match instance.test_info.filter_match {
541 FilterMatch::Mismatch {
542 reason: MismatchReason::RerunAlreadyPassed,
543 } => {
544 skipped_tests_rerun += 1;
545 true
546 }
547 FilterMatch::Mismatch {
548 reason: MismatchReason::NotBenchmark,
549 } => {
550 skipped_tests_non_benchmark += 1;
551 true
552 }
553 FilterMatch::Mismatch {
554 reason: MismatchReason::DefaultFilter,
555 } => {
556 skipped_tests_default_filter += 1;
557 true
558 }
559 FilterMatch::Mismatch { .. } => true,
560 FilterMatch::Matches => false,
561 })
562 .count();
563
564 let mut skipped_binaries_default_filter = 0;
565 let skipped_binaries = self
566 .rust_suites
567 .iter()
568 .filter(|suite| match suite.status {
569 RustTestSuiteStatus::Skipped {
570 reason: BinaryMismatchReason::DefaultSet,
571 } => {
572 skipped_binaries_default_filter += 1;
573 true
574 }
575 RustTestSuiteStatus::Skipped { .. } => true,
576 RustTestSuiteStatus::Listed { .. } => false,
577 })
578 .count();
579
580 SkipCounts {
581 skipped_tests,
582 skipped_tests_rerun,
583 skipped_tests_non_benchmark,
584 skipped_tests_default_filter,
585 skipped_binaries,
586 skipped_binaries_default_filter,
587 }
588 })
589 }
590
591 pub fn run_count(&self) -> usize {
595 self.test_count - self.skip_counts().skipped_tests
596 }
597
598 pub fn binary_count(&self) -> usize {
600 self.rust_suites.len()
601 }
602
603 pub fn listed_binary_count(&self) -> usize {
605 self.binary_count() - self.skip_counts().skipped_binaries
606 }
607
608 pub fn workspace_root(&self) -> &Utf8Path {
610 &self.workspace_root
611 }
612
613 pub fn cargo_env(&self) -> &EnvironmentMap {
615 &self.env
616 }
617
618 pub fn updated_dylib_path(&self) -> &OsStr {
620 &self.updated_dylib_path
621 }
622
623 pub fn to_summary(&self) -> TestListSummary {
625 let rust_suites = self
626 .rust_suites
627 .iter()
628 .map(|test_suite| {
629 let (status, test_cases) = test_suite.status.to_summary();
630 let testsuite = RustTestSuiteSummary {
631 package_name: test_suite.package.name().to_owned(),
632 binary: RustTestBinarySummary {
633 binary_name: test_suite.binary_name.clone(),
634 package_id: test_suite.package.id().repr().to_owned(),
635 kind: test_suite.kind.clone(),
636 binary_path: test_suite.binary_path.clone(),
637 binary_id: test_suite.binary_id.clone(),
638 build_platform: test_suite.build_platform,
639 },
640 cwd: test_suite.cwd.clone(),
641 status,
642 test_cases,
643 };
644 (test_suite.binary_id.clone(), testsuite)
645 })
646 .collect();
647 let mut summary = TestListSummary::new(self.rust_build_meta.to_summary());
648 summary.test_count = self.test_count;
649 summary.rust_suites = rust_suites;
650 summary
651 }
652
653 pub fn write(
655 &self,
656 output_format: OutputFormat,
657 writer: &mut dyn WriteStr,
658 colorize: bool,
659 ) -> Result<(), WriteTestListError> {
660 match output_format {
661 OutputFormat::Human { verbose } => self
662 .write_human(writer, verbose, colorize)
663 .map_err(WriteTestListError::Io),
664 OutputFormat::Oneline { verbose } => self
665 .write_oneline(writer, verbose, colorize)
666 .map_err(WriteTestListError::Io),
667 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
668 }
669 }
670
671 pub fn iter(&self) -> impl Iterator<Item = &RustTestSuite<'_>> + '_ {
673 self.rust_suites.iter()
674 }
675
676 pub fn get_suite(&self, binary_id: &RustBinaryId) -> Option<&RustTestSuite<'_>> {
678 self.rust_suites.get(binary_id)
679 }
680
681 pub fn iter_tests(&self) -> impl Iterator<Item = TestInstance<'_>> + '_ {
683 self.rust_suites.iter().flat_map(|test_suite| {
684 test_suite
685 .status
686 .test_cases()
687 .map(move |case| TestInstance::new(case, test_suite))
688 })
689 }
690
691 pub fn to_priority_queue(
693 &'g self,
694 profile: &'g EvaluatableProfile<'g>,
695 ) -> TestPriorityQueue<'g> {
696 TestPriorityQueue::new(self, profile)
697 }
698
699 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
701 let mut s = String::with_capacity(1024);
702 self.write(output_format, &mut s, false)?;
703 Ok(s)
704 }
705
706 pub fn empty() -> Self {
714 Self {
715 test_count: 0,
716 mode: NextestRunMode::Test,
717 workspace_root: Utf8PathBuf::new(),
718 rust_build_meta: RustBuildMeta::empty(),
719 env: EnvironmentMap::empty(),
720 updated_dylib_path: OsString::new(),
721 rust_suites: IdOrdMap::new(),
722 skip_counts: OnceLock::new(),
723 }
724 }
725
726 pub(crate) fn create_dylib_path(
727 rust_build_meta: &RustBuildMeta<TestListState>,
728 ) -> Result<OsString, CreateTestListError> {
729 let dylib_path = dylib_path();
730 let dylib_path_is_empty = dylib_path.is_empty();
731 let new_paths = rust_build_meta.dylib_paths();
732
733 let mut updated_dylib_path: Vec<PathBuf> =
734 Vec::with_capacity(dylib_path.len() + new_paths.len());
735 updated_dylib_path.extend(
736 new_paths
737 .iter()
738 .map(|path| path.clone().into_std_path_buf()),
739 );
740 updated_dylib_path.extend(dylib_path);
741
742 if cfg!(target_os = "macos") && dylib_path_is_empty {
749 if let Some(home) = home::home_dir() {
750 updated_dylib_path.push(home.join("lib"));
751 }
752 updated_dylib_path.push("/usr/local/lib".into());
753 updated_dylib_path.push("/usr/lib".into());
754 }
755
756 std::env::join_paths(updated_dylib_path)
757 .map_err(move |error| CreateTestListError::dylib_join_paths(new_paths, error))
758 }
759
760 fn parse_output(
763 test_binary: RustTestArtifact<'g>,
764 non_ignored: impl AsRef<str>,
765 ignored: impl AsRef<str>,
766 ) -> Result<ParsedTestBinary<'g>, CreateTestListError> {
767 let mut test_cases = Vec::new();
768
769 for (test_name, kind) in Self::parse(&test_binary.binary_id, non_ignored.as_ref())? {
770 test_cases.push(ParsedTestCase {
771 name: TestCaseName::new(test_name),
772 kind,
773 ignored: false,
774 });
775 }
776
777 for (test_name, kind) in Self::parse(&test_binary.binary_id, ignored.as_ref())? {
778 test_cases.push(ParsedTestCase {
783 name: TestCaseName::new(test_name),
784 kind,
785 ignored: true,
786 });
787 }
788
789 Ok(ParsedTestBinary::Listed {
790 artifact: test_binary,
791 test_cases,
792 })
793 }
794
795 fn build_suites(
806 parsed: impl IntoIterator<Item = ParsedTestBinary<'g>>,
807 filter: &TestFilter,
808 ecx: &EvalContext<'_>,
809 bound: FilterBound,
810 groups: Option<&dyn GroupLookup>,
811 ) -> IdOrdMap<RustTestSuite<'g>> {
812 parsed
813 .into_iter()
814 .map(|binary| match binary {
815 ParsedTestBinary::Listed {
816 artifact,
817 test_cases,
818 } => {
819 let filtered = {
820 let query = artifact.to_binary_query();
821 let mut map = IdOrdMap::new();
822 for tc in test_cases {
823 let filter_match = filter.filter_match(
824 query, &tc.name, &tc.kind, ecx, bound, tc.ignored, groups,
825 );
826 map.insert_overwrite(RustTestCase {
831 name: tc.name,
832 test_info: RustTestCaseSummary {
833 kind: Some(tc.kind),
834 ignored: tc.ignored,
835 filter_match,
836 },
837 });
838 }
839 map
840 };
841 artifact.into_test_suite(RustTestSuiteStatus::Listed {
842 test_cases: filtered.into(),
843 })
844 }
845 ParsedTestBinary::Skipped { artifact, reason } => {
846 artifact.into_test_suite(RustTestSuiteStatus::Skipped { reason })
847 }
848 })
849 .collect()
850 }
851
852 fn make_skipped(
853 test_binary: RustTestArtifact<'g>,
854 reason: BinaryMismatchReason,
855 ) -> ParsedTestBinary<'g> {
856 ParsedTestBinary::Skipped {
857 artifact: test_binary,
858 reason,
859 }
860 }
861
862 fn collect_test_queries_from_parsed<'a>(
869 parsed_binaries: &'a [ParsedTestBinary<'g>],
870 ) -> Vec<TestQuery<'a>> {
871 parsed_binaries
872 .iter()
873 .filter_map(|binary| match binary {
874 ParsedTestBinary::Listed {
875 artifact,
876 test_cases,
877 } => Some((artifact, test_cases)),
878 ParsedTestBinary::Skipped { .. } => None,
879 })
880 .flat_map(|(artifact, test_cases)| {
881 let binary_query = artifact.to_binary_query();
882 test_cases.iter().map(move |tc| TestQuery {
883 binary_query,
884 test_name: &tc.name,
885 })
886 })
887 .collect()
888 }
889
890 fn apply_partitioning(
896 rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
897 partitioner_builder: Option<&PartitionerBuilder>,
898 ) {
899 let Some(partitioner_builder) = partitioner_builder else {
900 return;
901 };
902
903 match partitioner_builder.scope() {
904 PartitionerScope::PerBinary => {
905 Self::apply_per_binary_partitioning(rust_suites, partitioner_builder);
906 }
907 PartitionerScope::CrossBinary => {
908 Self::apply_cross_binary_partitioning(rust_suites, partitioner_builder);
909 }
910 }
911 }
912
913 fn apply_per_binary_partitioning(
916 rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
917 partitioner_builder: &PartitionerBuilder,
918 ) {
919 for mut suite in rust_suites.iter_mut() {
920 let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
921 continue;
922 };
923
924 let mut non_ignored_partitioner = partitioner_builder.build();
926 apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
927
928 let mut ignored_partitioner = partitioner_builder.build();
929 apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
930 }
931 }
932
933 fn apply_cross_binary_partitioning(
937 rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
938 partitioner_builder: &PartitionerBuilder,
939 ) {
940 let mut non_ignored_partitioner = partitioner_builder.build();
942 for mut suite in rust_suites.iter_mut() {
943 let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
944 continue;
945 };
946 apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
947 }
948
949 let mut ignored_partitioner = partitioner_builder.build();
951 for mut suite in rust_suites.iter_mut() {
952 let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
953 continue;
954 };
955 apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
956 }
957 }
958
959 fn parse<'a>(
961 binary_id: &'a RustBinaryId,
962 list_output: &'a str,
963 ) -> Result<Vec<(&'a str, RustTestKind)>, CreateTestListError> {
964 let mut list = parse_list_lines(binary_id, list_output).collect::<Result<Vec<_>, _>>()?;
965 list.sort_unstable();
966 Ok(list)
967 }
968
969 pub fn write_human(
971 &self,
972 writer: &mut dyn WriteStr,
973 verbose: bool,
974 colorize: bool,
975 ) -> io::Result<()> {
976 self.write_human_impl(None, writer, verbose, colorize)
977 }
978
979 pub(crate) fn write_human_with_filter(
981 &self,
982 filter: &TestListDisplayFilter<'_>,
983 writer: &mut dyn WriteStr,
984 verbose: bool,
985 colorize: bool,
986 ) -> io::Result<()> {
987 self.write_human_impl(Some(filter), writer, verbose, colorize)
988 }
989
990 fn write_human_impl(
991 &self,
992 filter: Option<&TestListDisplayFilter<'_>>,
993 mut writer: &mut dyn WriteStr,
994 verbose: bool,
995 colorize: bool,
996 ) -> io::Result<()> {
997 let mut styles = Styles::default();
998 if colorize {
999 styles.colorize();
1000 }
1001
1002 for info in &self.rust_suites {
1003 let matcher = match filter {
1004 Some(filter) => match filter.matcher_for(&info.binary_id) {
1005 Some(matcher) => matcher,
1006 None => continue,
1007 },
1008 None => DisplayFilterMatcher::All,
1009 };
1010
1011 if !verbose
1014 && info
1015 .status
1016 .test_cases()
1017 .all(|case| !case.test_info.filter_match.is_match())
1018 {
1019 continue;
1020 }
1021
1022 writeln!(writer, "{}:", info.binary_id.style(styles.binary_id))?;
1023 if verbose {
1024 writeln!(
1025 writer,
1026 " {} {}",
1027 "bin:".style(styles.field),
1028 info.binary_path
1029 )?;
1030 writeln!(writer, " {} {}", "cwd:".style(styles.field), info.cwd)?;
1031 writeln!(
1032 writer,
1033 " {} {}",
1034 "build platform:".style(styles.field),
1035 info.build_platform,
1036 )?;
1037 }
1038
1039 let mut indented = indented(writer).with_str(" ");
1040
1041 match &info.status {
1042 RustTestSuiteStatus::Listed { test_cases } => {
1043 let matching_tests: Vec<_> = test_cases
1044 .iter()
1045 .filter(|case| matcher.is_match(&case.name))
1046 .collect();
1047 if matching_tests.is_empty() {
1048 writeln!(indented, "(no tests)")?;
1049 } else {
1050 for case in matching_tests {
1051 match (verbose, case.test_info.filter_match.is_match()) {
1052 (_, true) => {
1053 write_test_name(&case.name, &styles, &mut indented)?;
1054 writeln!(indented)?;
1055 }
1056 (true, false) => {
1057 write_test_name(&case.name, &styles, &mut indented)?;
1058 writeln!(indented, " (skipped)")?;
1059 }
1060 (false, false) => {
1061 }
1063 }
1064 }
1065 }
1066 }
1067 RustTestSuiteStatus::Skipped { reason } => {
1068 writeln!(indented, "(test binary {reason}, skipped)")?;
1069 }
1070 }
1071
1072 writer = indented.into_inner();
1073 }
1074 Ok(())
1075 }
1076
1077 pub fn write_oneline(
1079 &self,
1080 writer: &mut dyn WriteStr,
1081 verbose: bool,
1082 colorize: bool,
1083 ) -> io::Result<()> {
1084 let mut styles = Styles::default();
1085 if colorize {
1086 styles.colorize();
1087 }
1088
1089 for info in &self.rust_suites {
1090 match &info.status {
1091 RustTestSuiteStatus::Listed { test_cases } => {
1092 for case in test_cases.iter() {
1093 let is_match = case.test_info.filter_match.is_match();
1094 if !verbose && !is_match {
1096 continue;
1097 }
1098
1099 write!(writer, "{} ", info.binary_id.style(styles.binary_id))?;
1100 write_test_name(&case.name, &styles, writer)?;
1101
1102 if verbose {
1103 write!(
1104 writer,
1105 " [{}{}] [{}{}] [{}{}]{}",
1106 "bin: ".style(styles.field),
1107 info.binary_path,
1108 "cwd: ".style(styles.field),
1109 info.cwd,
1110 "build platform: ".style(styles.field),
1111 info.build_platform,
1112 if is_match { "" } else { " (skipped)" },
1113 )?;
1114 }
1115
1116 writeln!(writer)?;
1117 }
1118 }
1119 RustTestSuiteStatus::Skipped { .. } => {
1120 }
1122 }
1123 }
1124
1125 Ok(())
1126 }
1127}
1128
1129fn apply_partitioner_to_tests(
1131 test_cases: &mut IdOrdMap<RustTestCase>,
1132 partitioner: &mut dyn Partitioner,
1133 ignored: bool,
1134) {
1135 for mut test_case in test_cases.iter_mut() {
1136 if test_case.test_info.ignored == ignored {
1137 apply_partition_to_test(&mut test_case, partitioner);
1138 }
1139 }
1140}
1141
1142fn apply_partition_to_test(test_case: &mut RustTestCase, partitioner: &mut dyn Partitioner) {
1150 match test_case.test_info.filter_match {
1151 FilterMatch::Matches => {
1152 if !partitioner.test_matches(test_case.name.as_str()) {
1153 test_case.test_info.filter_match = FilterMatch::Mismatch {
1154 reason: MismatchReason::Partition,
1155 };
1156 }
1157 }
1158 FilterMatch::Mismatch {
1159 reason: MismatchReason::RerunAlreadyPassed,
1160 } => {
1161 let _ = partitioner.test_matches(test_case.name.as_str());
1163 }
1164 FilterMatch::Mismatch { .. } => {
1165 }
1167 }
1168}
1169
1170fn parse_list_lines<'a>(
1171 binary_id: &'a RustBinaryId,
1172 list_output: &'a str,
1173) -> impl Iterator<Item = Result<(&'a str, RustTestKind), CreateTestListError>> + 'a + use<'a> {
1174 list_output
1180 .lines()
1181 .map(move |line| match line.strip_suffix(": test") {
1182 Some(test_name) => Ok((test_name, RustTestKind::TEST)),
1183 None => match line.strip_suffix(": benchmark") {
1184 Some(test_name) => Ok((test_name, RustTestKind::BENCH)),
1185 None => Err(CreateTestListError::parse_line(
1186 binary_id.clone(),
1187 format!(
1188 "line {line:?} did not end with the string \": test\" or \": benchmark\""
1189 ),
1190 list_output,
1191 )),
1192 },
1193 })
1194}
1195
1196pub trait ListProfile {
1198 fn filterset_ecx(&self) -> EvalContext<'_>;
1200
1201 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_>;
1203
1204 fn precompute_group_memberships<'a>(
1206 &self,
1207 _tests: impl Iterator<Item = TestQuery<'a>>,
1208 ) -> PrecomputedGroupMembership;
1209}
1210
1211impl<'g> ListProfile for EvaluatableProfile<'g> {
1212 fn filterset_ecx(&self) -> EvalContext<'_> {
1213 self.filterset_ecx()
1214 }
1215
1216 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1217 self.list_settings_for(query)
1218 }
1219
1220 fn precompute_group_memberships<'a>(
1221 &self,
1222 tests: impl Iterator<Item = TestQuery<'a>>,
1223 ) -> PrecomputedGroupMembership {
1224 EvaluatableProfile::precompute_group_memberships(self, tests)
1225 }
1226}
1227
1228pub struct TestPriorityQueue<'a> {
1230 tests: Vec<TestInstanceWithSettings<'a>>,
1231}
1232
1233impl<'a> TestPriorityQueue<'a> {
1234 fn new(test_list: &'a TestList<'a>, profile: &'a EvaluatableProfile<'a>) -> Self {
1235 let mode = test_list.mode();
1236 let mut tests = test_list
1237 .iter_tests()
1238 .map(|instance| {
1239 let settings = profile.settings_for(mode, &instance.to_test_query());
1240 TestInstanceWithSettings { instance, settings }
1241 })
1242 .collect::<Vec<_>>();
1243 tests.sort_by_key(|test| test.settings.priority());
1246
1247 Self { tests }
1248 }
1249}
1250
1251impl<'a> IntoIterator for TestPriorityQueue<'a> {
1252 type Item = TestInstanceWithSettings<'a>;
1253 type IntoIter = std::vec::IntoIter<Self::Item>;
1254
1255 fn into_iter(self) -> Self::IntoIter {
1256 self.tests.into_iter()
1257 }
1258}
1259
1260#[derive(Debug)]
1264pub struct TestInstanceWithSettings<'a> {
1265 pub instance: TestInstance<'a>,
1267
1268 pub settings: TestSettings<'a>,
1270}
1271
1272#[derive(Clone, Debug, Eq, PartialEq)]
1276pub struct RustTestSuite<'g> {
1277 pub binary_id: RustBinaryId,
1279
1280 pub binary_path: Utf8PathBuf,
1282
1283 pub package: PackageMetadata<'g>,
1285
1286 pub binary_name: String,
1288
1289 pub kind: RustTestBinaryKind,
1291
1292 pub cwd: Utf8PathBuf,
1295
1296 pub build_platform: BuildPlatform,
1298
1299 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
1301
1302 pub status: RustTestSuiteStatus,
1304}
1305
1306impl<'g> RustTestSuite<'g> {
1307 pub fn to_binary_query(&self) -> BinaryQuery<'_> {
1309 BinaryQuery {
1310 package_id: self.package.id(),
1311 binary_id: &self.binary_id,
1312 kind: &self.kind,
1313 binary_name: &self.binary_name,
1314 platform: convert_build_platform(self.build_platform),
1315 }
1316 }
1317}
1318
1319impl IdOrdItem for RustTestSuite<'_> {
1320 type Key<'a>
1321 = &'a RustBinaryId
1322 where
1323 Self: 'a;
1324
1325 fn key(&self) -> Self::Key<'_> {
1326 &self.binary_id
1327 }
1328
1329 id_upcast!();
1330}
1331
1332impl RustTestArtifact<'_> {
1333 async fn exec(
1335 &self,
1336 lctx: &LocalExecuteContext<'_>,
1337 list_settings: &ListSettings<'_>,
1338 target_runner: &TargetRunner,
1339 ) -> Result<(String, String), CreateTestListError> {
1340 if !self.cwd.is_dir() {
1343 return Err(CreateTestListError::CwdIsNotDir {
1344 binary_id: self.binary_id.clone(),
1345 cwd: self.cwd.clone(),
1346 });
1347 }
1348 let platform_runner = target_runner.for_build_platform(self.build_platform);
1349
1350 let non_ignored = self.exec_single(false, lctx, list_settings, platform_runner);
1351 let ignored = self.exec_single(true, lctx, list_settings, platform_runner);
1352
1353 let (non_ignored_out, ignored_out) = futures::future::join(non_ignored, ignored).await;
1354 Ok((non_ignored_out?, ignored_out?))
1355 }
1356
1357 async fn exec_single(
1358 &self,
1359 ignored: bool,
1360 lctx: &LocalExecuteContext<'_>,
1361 list_settings: &ListSettings<'_>,
1362 runner: Option<&PlatformRunner>,
1363 ) -> Result<String, CreateTestListError> {
1364 let mut cli = TestCommandCli::default();
1365 cli.apply_wrappers(
1366 list_settings.list_wrapper(),
1367 runner,
1368 lctx.workspace_root,
1369 &lctx.rust_build_meta.target_directory,
1370 );
1371 cli.push(self.binary_path.as_str());
1372
1373 cli.extend(["--list", "--format", "terse"]);
1374 if ignored {
1375 cli.push("--ignored");
1376 }
1377
1378 let cmd = TestCommand::new(
1379 lctx,
1380 cli.program
1381 .clone()
1382 .expect("at least one argument passed in")
1383 .into_owned(),
1384 &cli.args,
1385 cli.env,
1386 &self.cwd,
1387 &self.package,
1388 &self.non_test_binaries,
1389 &Interceptor::None, );
1391
1392 let output =
1393 cmd.wait_with_output()
1394 .await
1395 .map_err(|error| CreateTestListError::CommandExecFail {
1396 binary_id: self.binary_id.clone(),
1397 command: cli.to_owned_cli(),
1398 error,
1399 })?;
1400
1401 if output.status.success() {
1402 String::from_utf8(output.stdout).map_err(|err| CreateTestListError::CommandNonUtf8 {
1403 binary_id: self.binary_id.clone(),
1404 command: cli.to_owned_cli(),
1405 stdout: err.into_bytes(),
1406 stderr: output.stderr,
1407 })
1408 } else {
1409 Err(CreateTestListError::CommandFail {
1410 binary_id: self.binary_id.clone(),
1411 command: cli.to_owned_cli(),
1412 exit_status: output.status,
1413 stdout: output.stdout,
1414 stderr: output.stderr,
1415 })
1416 }
1417 }
1418}
1419
1420enum ParsedTestBinary<'g> {
1426 Listed {
1428 artifact: RustTestArtifact<'g>,
1430
1431 test_cases: Vec<ParsedTestCase>,
1433 },
1434
1435 Skipped {
1437 artifact: RustTestArtifact<'g>,
1439
1440 reason: BinaryMismatchReason,
1442 },
1443}
1444
1445struct ParsedTestCase {
1450 name: TestCaseName,
1451 kind: RustTestKind,
1452 ignored: bool,
1453}
1454
1455#[derive(Clone, Debug, Eq, PartialEq)]
1459pub enum RustTestSuiteStatus {
1460 Listed {
1462 test_cases: DebugIgnore<IdOrdMap<RustTestCase>>,
1464 },
1465
1466 Skipped {
1468 reason: BinaryMismatchReason,
1470 },
1471}
1472
1473static EMPTY_TEST_CASE_MAP: IdOrdMap<RustTestCase> = IdOrdMap::new();
1474
1475impl RustTestSuiteStatus {
1476 pub fn test_count(&self) -> usize {
1478 match self {
1479 RustTestSuiteStatus::Listed { test_cases } => test_cases.len(),
1480 RustTestSuiteStatus::Skipped { .. } => 0,
1481 }
1482 }
1483
1484 pub fn get(&self, name: &TestCaseName) -> Option<&RustTestCase> {
1486 match self {
1487 RustTestSuiteStatus::Listed { test_cases } => test_cases.get(name),
1488 RustTestSuiteStatus::Skipped { .. } => None,
1489 }
1490 }
1491
1492 pub fn test_cases(&self) -> impl Iterator<Item = &RustTestCase> + '_ {
1494 match self {
1495 RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
1496 RustTestSuiteStatus::Skipped { .. } => {
1497 EMPTY_TEST_CASE_MAP.iter()
1499 }
1500 }
1501 }
1502
1503 pub fn to_summary(
1505 &self,
1506 ) -> (
1507 RustTestSuiteStatusSummary,
1508 BTreeMap<TestCaseName, RustTestCaseSummary>,
1509 ) {
1510 match self {
1511 Self::Listed { test_cases } => (
1512 RustTestSuiteStatusSummary::LISTED,
1513 test_cases
1514 .iter()
1515 .cloned()
1516 .map(|case| (case.name, case.test_info))
1517 .collect(),
1518 ),
1519 Self::Skipped {
1520 reason: BinaryMismatchReason::Expression,
1521 } => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
1522 Self::Skipped {
1523 reason: BinaryMismatchReason::DefaultSet,
1524 } => (
1525 RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER,
1526 BTreeMap::new(),
1527 ),
1528 }
1529 }
1530}
1531
1532#[derive(Clone, Debug, Eq, PartialEq)]
1534pub struct RustTestCase {
1535 pub name: TestCaseName,
1537
1538 pub test_info: RustTestCaseSummary,
1540}
1541
1542impl IdOrdItem for RustTestCase {
1543 type Key<'a> = &'a TestCaseName;
1544 fn key(&self) -> Self::Key<'_> {
1545 &self.name
1546 }
1547 id_upcast!();
1548}
1549
1550#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1552pub struct TestInstance<'a> {
1553 pub name: &'a TestCaseName,
1555
1556 pub suite_info: &'a RustTestSuite<'a>,
1558
1559 pub test_info: &'a RustTestCaseSummary,
1561}
1562
1563impl<'a> TestInstance<'a> {
1564 pub(crate) fn new(case: &'a RustTestCase, suite_info: &'a RustTestSuite) -> Self {
1566 Self {
1567 name: &case.name,
1568 suite_info,
1569 test_info: &case.test_info,
1570 }
1571 }
1572
1573 #[inline]
1576 pub fn id(&self) -> TestInstanceId<'a> {
1577 TestInstanceId {
1578 binary_id: &self.suite_info.binary_id,
1579 test_name: self.name,
1580 }
1581 }
1582
1583 pub fn to_test_query(&self) -> TestQuery<'a> {
1585 TestQuery {
1586 binary_query: BinaryQuery {
1587 package_id: self.suite_info.package.id(),
1588 binary_id: &self.suite_info.binary_id,
1589 kind: &self.suite_info.kind,
1590 binary_name: &self.suite_info.binary_name,
1591 platform: convert_build_platform(self.suite_info.build_platform),
1592 },
1593 test_name: self.name,
1594 }
1595 }
1596
1597 pub(crate) fn make_command(
1599 &self,
1600 ctx: &TestExecuteContext<'_>,
1601 test_list: &TestList<'_>,
1602 wrapper_script: Option<&WrapperScriptConfig>,
1603 extra_args: &[String],
1604 interceptor: &Interceptor,
1605 ) -> TestCommand {
1606 let cli = self.compute_cli(ctx, test_list, wrapper_script, extra_args);
1608
1609 let lctx = LocalExecuteContext {
1610 phase: TestCommandPhase::Run,
1611 workspace_root: test_list.workspace_root(),
1612 rust_build_meta: &test_list.rust_build_meta,
1613 double_spawn: ctx.double_spawn,
1614 dylib_path: test_list.updated_dylib_path(),
1615 profile_name: ctx.profile_name,
1616 env: &test_list.env,
1617 };
1618
1619 TestCommand::new(
1620 &lctx,
1621 cli.program
1622 .expect("at least one argument is guaranteed")
1623 .into_owned(),
1624 &cli.args,
1625 cli.env,
1626 &self.suite_info.cwd,
1627 &self.suite_info.package,
1628 &self.suite_info.non_test_binaries,
1629 interceptor,
1630 )
1631 }
1632
1633 pub(crate) fn command_line(
1634 &self,
1635 ctx: &TestExecuteContext<'_>,
1636 test_list: &TestList<'_>,
1637 wrapper_script: Option<&WrapperScriptConfig>,
1638 extra_args: &[String],
1639 ) -> Vec<String> {
1640 self.compute_cli(ctx, test_list, wrapper_script, extra_args)
1641 .to_owned_cli()
1642 }
1643
1644 fn compute_cli(
1645 &self,
1646 ctx: &'a TestExecuteContext<'_>,
1647 test_list: &TestList<'_>,
1648 wrapper_script: Option<&'a WrapperScriptConfig>,
1649 extra_args: &'a [String],
1650 ) -> TestCommandCli<'a> {
1651 let platform_runner = ctx
1652 .target_runner
1653 .for_build_platform(self.suite_info.build_platform);
1654
1655 let mut cli = TestCommandCli::default();
1656 cli.apply_wrappers(
1657 wrapper_script,
1658 platform_runner,
1659 test_list.workspace_root(),
1660 &test_list.rust_build_meta().target_directory,
1661 );
1662 cli.push(self.suite_info.binary_path.as_str());
1663
1664 cli.extend(["--exact", self.name.as_str(), "--nocapture"]);
1665 if self.test_info.ignored {
1666 cli.push("--ignored");
1667 }
1668 match test_list.mode() {
1669 NextestRunMode::Test => {}
1670 NextestRunMode::Benchmark => {
1671 cli.push("--bench");
1672 }
1673 }
1674 cli.extend(extra_args.iter().map(String::as_str));
1675
1676 cli
1677 }
1678}
1679
1680#[derive(Clone, Debug, Default)]
1681struct TestCommandCli<'a> {
1682 program: Option<Cow<'a, str>>,
1683 args: Vec<Cow<'a, str>>,
1684 env: Option<&'a ScriptCommandEnvMap>,
1685}
1686
1687impl<'a> TestCommandCli<'a> {
1688 fn apply_wrappers(
1689 &mut self,
1690 wrapper_script: Option<&'a WrapperScriptConfig>,
1691 platform_runner: Option<&'a PlatformRunner>,
1692 workspace_root: &Utf8Path,
1693 target_dir: &Utf8Path,
1694 ) {
1695 if let Some(wrapper) = wrapper_script {
1697 match wrapper.target_runner {
1698 WrapperScriptTargetRunner::Ignore => {
1699 self.env = Some(&wrapper.command.env);
1701 self.push(wrapper.command.program(workspace_root, target_dir));
1702 self.extend(wrapper.command.args.iter().map(String::as_str));
1703 }
1704 WrapperScriptTargetRunner::AroundWrapper => {
1705 self.env = Some(&wrapper.command.env);
1707 if let Some(runner) = platform_runner {
1708 self.push(runner.binary());
1709 self.extend(runner.args());
1710 }
1711 self.push(wrapper.command.program(workspace_root, target_dir));
1712 self.extend(wrapper.command.args.iter().map(String::as_str));
1713 }
1714 WrapperScriptTargetRunner::WithinWrapper => {
1715 self.env = Some(&wrapper.command.env);
1717 self.push(wrapper.command.program(workspace_root, target_dir));
1718 self.extend(wrapper.command.args.iter().map(String::as_str));
1719 if let Some(runner) = platform_runner {
1720 self.push(runner.binary());
1721 self.extend(runner.args());
1722 }
1723 }
1724 WrapperScriptTargetRunner::OverridesWrapper => {
1725 if let Some(runner) = platform_runner {
1726 self.push(runner.binary());
1729 self.extend(runner.args());
1730 } else {
1731 self.env = Some(&wrapper.command.env);
1733 self.push(wrapper.command.program(workspace_root, target_dir));
1734 self.extend(wrapper.command.args.iter().map(String::as_str));
1735 }
1736 }
1737 }
1738 } else {
1739 if let Some(runner) = platform_runner {
1741 self.push(runner.binary());
1742 self.extend(runner.args());
1743 }
1744 }
1745 }
1746
1747 fn push(&mut self, arg: impl Into<Cow<'a, str>>) {
1748 if self.program.is_none() {
1749 self.program = Some(arg.into());
1750 } else {
1751 self.args.push(arg.into());
1752 }
1753 }
1754
1755 fn extend(&mut self, args: impl IntoIterator<Item = &'a str>) {
1756 for arg in args {
1757 self.push(arg);
1758 }
1759 }
1760
1761 fn to_owned_cli(&self) -> Vec<String> {
1762 let mut owned_cli = Vec::new();
1763 if let Some(program) = &self.program {
1764 owned_cli.push(program.to_string());
1765 }
1766 owned_cli.extend(self.args.iter().map(|arg| arg.to_string()));
1767 owned_cli
1768 }
1769}
1770
1771#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize)]
1775pub struct TestInstanceId<'a> {
1776 pub binary_id: &'a RustBinaryId,
1778
1779 pub test_name: &'a TestCaseName,
1781}
1782
1783impl TestInstanceId<'_> {
1784 pub fn attempt_id(
1788 &self,
1789 run_id: ReportUuid,
1790 stress_index: Option<u32>,
1791 attempt: u32,
1792 ) -> String {
1793 let mut out = String::new();
1794 swrite!(out, "{run_id}:{}", self.binary_id);
1795 if let Some(stress_index) = stress_index {
1796 swrite!(out, "@stress-{}", stress_index);
1797 }
1798 swrite!(out, "${}", self.test_name);
1799 if attempt > 1 {
1800 swrite!(out, "#{attempt}");
1801 }
1802
1803 out
1804 }
1805}
1806
1807impl fmt::Display for TestInstanceId<'_> {
1808 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1809 write!(f, "{} {}", self.binary_id, self.test_name)
1810 }
1811}
1812
1813#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
1815#[serde(rename_all = "kebab-case")]
1816#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1817pub struct OwnedTestInstanceId {
1818 pub binary_id: RustBinaryId,
1820
1821 #[serde(rename = "name")]
1823 pub test_name: TestCaseName,
1824}
1825
1826impl OwnedTestInstanceId {
1827 pub fn as_ref(&self) -> TestInstanceId<'_> {
1829 TestInstanceId {
1830 binary_id: &self.binary_id,
1831 test_name: &self.test_name,
1832 }
1833 }
1834}
1835
1836impl TestInstanceId<'_> {
1837 pub fn to_owned(&self) -> OwnedTestInstanceId {
1839 OwnedTestInstanceId {
1840 binary_id: self.binary_id.clone(),
1841 test_name: self.test_name.clone(),
1842 }
1843 }
1844}
1845
1846pub trait TestInstanceIdKey {
1852 fn key<'k>(&'k self) -> TestInstanceId<'k>;
1854}
1855
1856impl TestInstanceIdKey for OwnedTestInstanceId {
1857 fn key<'k>(&'k self) -> TestInstanceId<'k> {
1858 TestInstanceId {
1859 binary_id: &self.binary_id,
1860 test_name: &self.test_name,
1861 }
1862 }
1863}
1864
1865impl<'a> TestInstanceIdKey for TestInstanceId<'a> {
1866 fn key<'k>(&'k self) -> TestInstanceId<'k> {
1867 *self
1868 }
1869}
1870
1871impl<'a> Borrow<dyn TestInstanceIdKey + 'a> for OwnedTestInstanceId {
1872 fn borrow(&self) -> &(dyn TestInstanceIdKey + 'a) {
1873 self
1874 }
1875}
1876
1877impl<'a> PartialEq for dyn TestInstanceIdKey + 'a {
1878 fn eq(&self, other: &(dyn TestInstanceIdKey + 'a)) -> bool {
1879 self.key() == other.key()
1880 }
1881}
1882
1883impl<'a> Eq for dyn TestInstanceIdKey + 'a {}
1884
1885impl<'a> PartialOrd for dyn TestInstanceIdKey + 'a {
1886 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1887 Some(self.cmp(other))
1888 }
1889}
1890
1891impl<'a> Ord for dyn TestInstanceIdKey + 'a {
1892 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1893 self.key().cmp(&other.key())
1894 }
1895}
1896
1897impl<'a> Hash for dyn TestInstanceIdKey + 'a {
1898 fn hash<H: Hasher>(&self, state: &mut H) {
1899 self.key().hash(state);
1900 }
1901}
1902
1903#[derive(Clone, Debug)]
1905pub struct TestExecuteContext<'a> {
1906 pub profile_name: &'a str,
1908
1909 pub double_spawn: &'a DoubleSpawnInfo,
1911
1912 pub target_runner: &'a TargetRunner,
1914}
1915
1916#[cfg(test)]
1917mod tests {
1918 use super::*;
1919 use crate::{
1920 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
1921 config::scripts::{ScriptCommand, ScriptCommandEnvMap, ScriptCommandRelativeTo},
1922 list::{
1923 SerializableFormat,
1924 test_helpers::{PACKAGE_GRAPH_FIXTURE, package_metadata},
1925 },
1926 platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
1927 target_runner::PlatformRunnerSource,
1928 test_filter::{RunIgnored, TestFilterPatterns},
1929 };
1930 use iddqd::id_ord_map;
1931 use indoc::indoc;
1932 use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, KnownGroups, ParseContext};
1933 use nextest_metadata::{FilterMatch, MismatchReason, PlatformLibdirUnavailable, RustTestKind};
1934 use pretty_assertions::assert_eq;
1935 use std::{
1936 collections::{BTreeMap, HashSet},
1937 hash::DefaultHasher,
1938 };
1939 use target_spec::Platform;
1940 use test_strategy::proptest;
1941
1942 #[test]
1943 fn test_parse_test_list() {
1944 let non_ignored_output = indoc! {"
1946 tests::foo::test_bar: test
1947 tests::baz::test_quux: test
1948 benches::bench_foo: benchmark
1949 "};
1950 let ignored_output = indoc! {"
1951 tests::ignored::test_bar: test
1952 tests::baz::test_ignored: test
1953 benches::ignored_bench_foo: benchmark
1954 "};
1955
1956 let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
1957
1958 let test_filter = TestFilter::new(
1959 NextestRunMode::Test,
1960 RunIgnored::Default,
1961 TestFilterPatterns::default(),
1962 vec![
1964 Filterset::parse(
1965 "platform(target)".to_owned(),
1966 &cx,
1967 FiltersetKind::Test,
1968 &KnownGroups::Known {
1969 custom_groups: HashSet::new(),
1970 },
1971 )
1972 .unwrap(),
1973 ],
1974 )
1975 .unwrap();
1976 let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
1977 let fake_binary_name = "fake-binary".to_owned();
1978 let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
1979
1980 let test_binary = RustTestArtifact {
1981 binary_path: "/fake/binary".into(),
1982 cwd: fake_cwd.clone(),
1983 package: package_metadata(),
1984 binary_name: fake_binary_name.clone(),
1985 binary_id: fake_binary_id.clone(),
1986 kind: RustTestBinaryKind::LIB,
1987 non_test_binaries: BTreeSet::new(),
1988 build_platform: BuildPlatform::Target,
1989 };
1990
1991 let skipped_binary_name = "skipped-binary".to_owned();
1992 let skipped_binary_id = RustBinaryId::new("fake-package::skipped-binary");
1993 let skipped_binary = RustTestArtifact {
1994 binary_path: "/fake/skipped-binary".into(),
1995 cwd: fake_cwd.clone(),
1996 package: package_metadata(),
1997 binary_name: skipped_binary_name.clone(),
1998 binary_id: skipped_binary_id.clone(),
1999 kind: RustTestBinaryKind::PROC_MACRO,
2000 non_test_binaries: BTreeSet::new(),
2001 build_platform: BuildPlatform::Host,
2002 };
2003
2004 let fake_triple = TargetTriple {
2005 platform: Platform::new(
2006 "aarch64-unknown-linux-gnu",
2007 target_spec::TargetFeatures::Unknown,
2008 )
2009 .unwrap(),
2010 source: TargetTripleSource::CliOption,
2011 location: TargetDefinitionLocation::Builtin,
2012 };
2013 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
2014 let build_platforms = BuildPlatforms {
2015 host: HostPlatform {
2016 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
2017 libdir: PlatformLibdir::Available(fake_host_libdir.into()),
2018 },
2019 target: Some(TargetPlatform {
2020 triple: fake_triple,
2021 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
2023 }),
2024 };
2025
2026 let fake_env = EnvironmentMap::empty();
2027 let rust_build_meta =
2028 RustBuildMeta::new("/fake", "/fake", build_platforms).map_paths(&PathMapper::noop());
2029 let ecx = EvalContext {
2030 default_filter: &CompiledExpr::ALL,
2031 };
2032 let test_list = TestList::new_with_outputs(
2033 [
2034 (test_binary, &non_ignored_output, &ignored_output),
2035 (
2036 skipped_binary,
2037 &"should-not-show-up-stdout",
2038 &"should-not-show-up-stderr",
2039 ),
2040 ],
2041 Utf8PathBuf::from("/fake/path"),
2042 rust_build_meta,
2043 &test_filter,
2044 None,
2045 fake_env,
2046 &ecx,
2047 FilterBound::All,
2048 )
2049 .expect("valid output");
2050 assert_eq!(
2051 test_list.rust_suites,
2052 id_ord_map! {
2053 RustTestSuite {
2054 status: RustTestSuiteStatus::Listed {
2055 test_cases: id_ord_map! {
2056 RustTestCase {
2057 name: TestCaseName::new("tests::foo::test_bar"),
2058 test_info: RustTestCaseSummary {
2059 kind: Some(RustTestKind::TEST),
2060 ignored: false,
2061 filter_match: FilterMatch::Matches,
2062 },
2063 },
2064 RustTestCase {
2065 name: TestCaseName::new("tests::baz::test_quux"),
2066 test_info: RustTestCaseSummary {
2067 kind: Some(RustTestKind::TEST),
2068 ignored: false,
2069 filter_match: FilterMatch::Matches,
2070 },
2071 },
2072 RustTestCase {
2073 name: TestCaseName::new("benches::bench_foo"),
2074 test_info: RustTestCaseSummary {
2075 kind: Some(RustTestKind::BENCH),
2076 ignored: false,
2077 filter_match: FilterMatch::Matches,
2078 },
2079 },
2080 RustTestCase {
2081 name: TestCaseName::new("tests::ignored::test_bar"),
2082 test_info: RustTestCaseSummary {
2083 kind: Some(RustTestKind::TEST),
2084 ignored: true,
2085 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
2086 },
2087 },
2088 RustTestCase {
2089 name: TestCaseName::new("tests::baz::test_ignored"),
2090 test_info: RustTestCaseSummary {
2091 kind: Some(RustTestKind::TEST),
2092 ignored: true,
2093 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
2094 },
2095 },
2096 RustTestCase {
2097 name: TestCaseName::new("benches::ignored_bench_foo"),
2098 test_info: RustTestCaseSummary {
2099 kind: Some(RustTestKind::BENCH),
2100 ignored: true,
2101 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
2102 },
2103 },
2104 }.into(),
2105 },
2106 cwd: fake_cwd.clone(),
2107 build_platform: BuildPlatform::Target,
2108 package: package_metadata(),
2109 binary_name: fake_binary_name,
2110 binary_id: fake_binary_id,
2111 binary_path: "/fake/binary".into(),
2112 kind: RustTestBinaryKind::LIB,
2113 non_test_binaries: BTreeSet::new(),
2114 },
2115 RustTestSuite {
2116 status: RustTestSuiteStatus::Skipped {
2117 reason: BinaryMismatchReason::Expression,
2118 },
2119 cwd: fake_cwd,
2120 build_platform: BuildPlatform::Host,
2121 package: package_metadata(),
2122 binary_name: skipped_binary_name,
2123 binary_id: skipped_binary_id,
2124 binary_path: "/fake/skipped-binary".into(),
2125 kind: RustTestBinaryKind::PROC_MACRO,
2126 non_test_binaries: BTreeSet::new(),
2127 },
2128 }
2129 );
2130
2131 static EXPECTED_HUMAN: &str = indoc! {"
2133 fake-package::fake-binary:
2134 benches::bench_foo
2135 tests::baz::test_quux
2136 tests::foo::test_bar
2137 "};
2138 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {"
2139 fake-package::fake-binary:
2140 bin: /fake/binary
2141 cwd: /fake/cwd
2142 build platform: target
2143 benches::bench_foo
2144 benches::ignored_bench_foo (skipped)
2145 tests::baz::test_ignored (skipped)
2146 tests::baz::test_quux
2147 tests::foo::test_bar
2148 tests::ignored::test_bar (skipped)
2149 fake-package::skipped-binary:
2150 bin: /fake/skipped-binary
2151 cwd: /fake/cwd
2152 build platform: host
2153 (test binary didn't match filtersets, skipped)
2154 "};
2155 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
2156 {
2157 "rust-build-meta": {
2158 "target-directory": "/fake",
2159 "build-directory": "/fake",
2160 "base-output-directories": [],
2161 "non-test-binaries": {},
2162 "build-script-out-dirs": {},
2163 "build-script-info": {},
2164 "linked-paths": [],
2165 "platforms": {
2166 "host": {
2167 "platform": {
2168 "triple": "x86_64-unknown-linux-gnu",
2169 "target-features": "unknown"
2170 },
2171 "libdir": {
2172 "status": "available",
2173 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
2174 }
2175 },
2176 "targets": [
2177 {
2178 "platform": {
2179 "triple": "aarch64-unknown-linux-gnu",
2180 "target-features": "unknown"
2181 },
2182 "libdir": {
2183 "status": "unavailable",
2184 "reason": "test"
2185 }
2186 }
2187 ]
2188 },
2189 "target-platforms": [
2190 {
2191 "triple": "aarch64-unknown-linux-gnu",
2192 "target-features": "unknown"
2193 }
2194 ],
2195 "target-platform": "aarch64-unknown-linux-gnu"
2196 },
2197 "test-count": 6,
2198 "rust-suites": {
2199 "fake-package::fake-binary": {
2200 "package-name": "metadata-helper",
2201 "binary-id": "fake-package::fake-binary",
2202 "binary-name": "fake-binary",
2203 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
2204 "kind": "lib",
2205 "binary-path": "/fake/binary",
2206 "build-platform": "target",
2207 "cwd": "/fake/cwd",
2208 "status": "listed",
2209 "testcases": {
2210 "benches::bench_foo": {
2211 "kind": "bench",
2212 "ignored": false,
2213 "filter-match": {
2214 "status": "matches"
2215 }
2216 },
2217 "benches::ignored_bench_foo": {
2218 "kind": "bench",
2219 "ignored": true,
2220 "filter-match": {
2221 "status": "mismatch",
2222 "reason": "ignored"
2223 }
2224 },
2225 "tests::baz::test_ignored": {
2226 "kind": "test",
2227 "ignored": true,
2228 "filter-match": {
2229 "status": "mismatch",
2230 "reason": "ignored"
2231 }
2232 },
2233 "tests::baz::test_quux": {
2234 "kind": "test",
2235 "ignored": false,
2236 "filter-match": {
2237 "status": "matches"
2238 }
2239 },
2240 "tests::foo::test_bar": {
2241 "kind": "test",
2242 "ignored": false,
2243 "filter-match": {
2244 "status": "matches"
2245 }
2246 },
2247 "tests::ignored::test_bar": {
2248 "kind": "test",
2249 "ignored": true,
2250 "filter-match": {
2251 "status": "mismatch",
2252 "reason": "ignored"
2253 }
2254 }
2255 }
2256 },
2257 "fake-package::skipped-binary": {
2258 "package-name": "metadata-helper",
2259 "binary-id": "fake-package::skipped-binary",
2260 "binary-name": "skipped-binary",
2261 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
2262 "kind": "proc-macro",
2263 "binary-path": "/fake/skipped-binary",
2264 "build-platform": "host",
2265 "cwd": "/fake/cwd",
2266 "status": "skipped",
2267 "testcases": {}
2268 }
2269 }
2270 }"#};
2271 static EXPECTED_ONELINE: &str = indoc! {"
2272 fake-package::fake-binary benches::bench_foo
2273 fake-package::fake-binary tests::baz::test_quux
2274 fake-package::fake-binary tests::foo::test_bar
2275 "};
2276 static EXPECTED_ONELINE_VERBOSE: &str = indoc! {"
2277 fake-package::fake-binary benches::bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2278 fake-package::fake-binary benches::ignored_bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2279 fake-package::fake-binary tests::baz::test_ignored [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2280 fake-package::fake-binary tests::baz::test_quux [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2281 fake-package::fake-binary tests::foo::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2282 fake-package::fake-binary tests::ignored::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2283 "};
2284
2285 assert_eq!(
2286 test_list
2287 .to_string(OutputFormat::Human { verbose: false })
2288 .expect("human succeeded"),
2289 EXPECTED_HUMAN
2290 );
2291 assert_eq!(
2292 test_list
2293 .to_string(OutputFormat::Human { verbose: true })
2294 .expect("human succeeded"),
2295 EXPECTED_HUMAN_VERBOSE
2296 );
2297 println!(
2298 "{}",
2299 test_list
2300 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
2301 .expect("json-pretty succeeded")
2302 );
2303 assert_eq!(
2304 test_list
2305 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
2306 .expect("json-pretty succeeded"),
2307 EXPECTED_JSON_PRETTY
2308 );
2309 assert_eq!(
2310 test_list
2311 .to_string(OutputFormat::Oneline { verbose: false })
2312 .expect("oneline succeeded"),
2313 EXPECTED_ONELINE
2314 );
2315 assert_eq!(
2316 test_list
2317 .to_string(OutputFormat::Oneline { verbose: true })
2318 .expect("oneline verbose succeeded"),
2319 EXPECTED_ONELINE_VERBOSE
2320 );
2321 }
2322
2323 #[test]
2327 fn test_ignored_overrides_non_ignored() {
2328 let non_ignored_output = indoc! {"
2331 tests::unique_non_ignored: test
2332 tests::overlap_test: test
2333 "};
2334 let ignored_output = indoc! {"
2335 tests::unique_ignored: test
2336 tests::overlap_test: test
2337 "};
2338
2339 let test_filter = TestFilter::new(
2340 NextestRunMode::Test,
2341 RunIgnored::All,
2342 TestFilterPatterns::default(),
2343 Vec::new(),
2344 )
2345 .unwrap();
2346 let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
2347 let fake_binary_id = RustBinaryId::new("fake-package::overlap-binary");
2348
2349 let test_binary = RustTestArtifact {
2350 binary_path: "/fake/binary".into(),
2351 cwd: fake_cwd.clone(),
2352 package: package_metadata(),
2353 binary_name: "overlap-binary".to_owned(),
2354 binary_id: fake_binary_id.clone(),
2355 kind: RustTestBinaryKind::LIB,
2356 non_test_binaries: BTreeSet::new(),
2357 build_platform: BuildPlatform::Target,
2358 };
2359
2360 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
2361 let build_platforms = BuildPlatforms {
2362 host: HostPlatform {
2363 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
2364 libdir: PlatformLibdir::Available(fake_host_libdir.into()),
2365 },
2366 target: None,
2367 };
2368
2369 let fake_env = EnvironmentMap::empty();
2370 let rust_build_meta =
2371 RustBuildMeta::new("/fake", "/fake", build_platforms).map_paths(&PathMapper::noop());
2372 let ecx = EvalContext {
2373 default_filter: &CompiledExpr::ALL,
2374 };
2375 let test_list = TestList::new_with_outputs(
2376 [(test_binary, &non_ignored_output, &ignored_output)],
2377 Utf8PathBuf::from("/fake/path"),
2378 rust_build_meta,
2379 &test_filter,
2380 None,
2381 fake_env,
2382 &ecx,
2383 FilterBound::All,
2384 )
2385 .expect("valid output");
2386
2387 let suite = test_list
2389 .rust_suites
2390 .get(&fake_binary_id)
2391 .expect("suite exists");
2392 match &suite.status {
2393 RustTestSuiteStatus::Listed { test_cases } => {
2394 let overlap = test_cases
2395 .get(&TestCaseName::new("tests::overlap_test"))
2396 .expect("overlap_test exists");
2397 assert!(
2398 overlap.test_info.ignored,
2399 "overlapping test should be marked ignored"
2400 );
2401 }
2402 other => panic!("expected Listed status, got {other:?}"),
2403 }
2404 }
2405
2406 #[test]
2407 fn apply_wrappers_examples() {
2408 cfg_if::cfg_if! {
2409 if #[cfg(windows)]
2410 {
2411 let workspace_root = Utf8Path::new("D:\\workspace\\root");
2412 let target_dir = Utf8Path::new("C:\\foo\\bar");
2413 } else {
2414 let workspace_root = Utf8Path::new("/workspace/root");
2415 let target_dir = Utf8Path::new("/foo/bar");
2416 }
2417 };
2418
2419 {
2421 let mut cli_no_wrappers = TestCommandCli::default();
2422 cli_no_wrappers.apply_wrappers(None, None, workspace_root, target_dir);
2423 cli_no_wrappers.extend(["binary", "arg"]);
2424 assert!(cli_no_wrappers.env.is_none());
2425 assert_eq!(cli_no_wrappers.to_owned_cli(), vec!["binary", "arg"]);
2426 }
2427
2428 {
2430 let runner = PlatformRunner::debug_new(
2431 "runner".into(),
2432 Vec::new(),
2433 PlatformRunnerSource::Env("fake".to_owned()),
2434 );
2435 let mut cli_runner_only = TestCommandCli::default();
2436 cli_runner_only.apply_wrappers(None, Some(&runner), workspace_root, target_dir);
2437 cli_runner_only.extend(["binary", "arg"]);
2438 assert!(cli_runner_only.env.is_none());
2439 assert_eq!(
2440 cli_runner_only.to_owned_cli(),
2441 vec!["runner", "binary", "arg"],
2442 );
2443 }
2444
2445 {
2447 let runner = PlatformRunner::debug_new(
2448 "runner".into(),
2449 Vec::new(),
2450 PlatformRunnerSource::Env("fake".to_owned()),
2451 );
2452 let wrapper_ignore = WrapperScriptConfig {
2453 command: ScriptCommand {
2454 program: "wrapper".into(),
2455 args: Vec::new(),
2456 env: ScriptCommandEnvMap::default(),
2457 relative_to: ScriptCommandRelativeTo::None,
2458 },
2459 target_runner: WrapperScriptTargetRunner::Ignore,
2460 };
2461 let mut cli_wrapper_ignore = TestCommandCli::default();
2462 cli_wrapper_ignore.apply_wrappers(
2463 Some(&wrapper_ignore),
2464 Some(&runner),
2465 workspace_root,
2466 target_dir,
2467 );
2468 cli_wrapper_ignore.extend(["binary", "arg"]);
2469 assert_eq!(
2470 cli_wrapper_ignore.env,
2471 Some(&ScriptCommandEnvMap::default())
2472 );
2473 assert_eq!(
2474 cli_wrapper_ignore.to_owned_cli(),
2475 vec!["wrapper", "binary", "arg"],
2476 );
2477 }
2478
2479 {
2481 let runner = PlatformRunner::debug_new(
2482 "runner".into(),
2483 Vec::new(),
2484 PlatformRunnerSource::Env("fake".to_owned()),
2485 );
2486 let env = ScriptCommandEnvMap::new(BTreeMap::from([(
2487 String::from("MSG"),
2488 String::from("hello world"),
2489 )]))
2490 .expect("valid env var keys");
2491 let wrapper_around = WrapperScriptConfig {
2492 command: ScriptCommand {
2493 program: "wrapper".into(),
2494 args: Vec::new(),
2495 env: env.clone(),
2496 relative_to: ScriptCommandRelativeTo::None,
2497 },
2498 target_runner: WrapperScriptTargetRunner::AroundWrapper,
2499 };
2500 let mut cli_wrapper_around = TestCommandCli::default();
2501 cli_wrapper_around.apply_wrappers(
2502 Some(&wrapper_around),
2503 Some(&runner),
2504 workspace_root,
2505 target_dir,
2506 );
2507 cli_wrapper_around.extend(["binary", "arg"]);
2508 assert_eq!(cli_wrapper_around.env, Some(&env));
2509 assert_eq!(
2510 cli_wrapper_around.to_owned_cli(),
2511 vec!["runner", "wrapper", "binary", "arg"],
2512 );
2513 }
2514
2515 {
2517 let runner = PlatformRunner::debug_new(
2518 "runner".into(),
2519 Vec::new(),
2520 PlatformRunnerSource::Env("fake".to_owned()),
2521 );
2522 let wrapper_within = WrapperScriptConfig {
2523 command: ScriptCommand {
2524 program: "wrapper".into(),
2525 args: Vec::new(),
2526 env: ScriptCommandEnvMap::default(),
2527 relative_to: ScriptCommandRelativeTo::None,
2528 },
2529 target_runner: WrapperScriptTargetRunner::WithinWrapper,
2530 };
2531 let mut cli_wrapper_within = TestCommandCli::default();
2532 cli_wrapper_within.apply_wrappers(
2533 Some(&wrapper_within),
2534 Some(&runner),
2535 workspace_root,
2536 target_dir,
2537 );
2538 cli_wrapper_within.extend(["binary", "arg"]);
2539 assert_eq!(
2540 cli_wrapper_within.env,
2541 Some(&ScriptCommandEnvMap::default())
2542 );
2543 assert_eq!(
2544 cli_wrapper_within.to_owned_cli(),
2545 vec!["wrapper", "runner", "binary", "arg"],
2546 );
2547 }
2548
2549 {
2552 let runner = PlatformRunner::debug_new(
2553 "runner".into(),
2554 Vec::new(),
2555 PlatformRunnerSource::Env("fake".to_owned()),
2556 );
2557 let wrapper_overrides = WrapperScriptConfig {
2558 command: ScriptCommand {
2559 program: "wrapper".into(),
2560 args: Vec::new(),
2561 env: ScriptCommandEnvMap::default(),
2562 relative_to: ScriptCommandRelativeTo::None,
2563 },
2564 target_runner: WrapperScriptTargetRunner::OverridesWrapper,
2565 };
2566 let mut cli_wrapper_overrides = TestCommandCli::default();
2567 cli_wrapper_overrides.apply_wrappers(
2568 Some(&wrapper_overrides),
2569 Some(&runner),
2570 workspace_root,
2571 target_dir,
2572 );
2573 cli_wrapper_overrides.extend(["binary", "arg"]);
2574 assert!(
2575 cli_wrapper_overrides.env.is_none(),
2576 "overrides-wrapper with runner should not apply wrapper env"
2577 );
2578 assert_eq!(
2579 cli_wrapper_overrides.to_owned_cli(),
2580 vec!["runner", "binary", "arg"],
2581 );
2582 }
2583
2584 {
2587 let wrapper_overrides = WrapperScriptConfig {
2588 command: ScriptCommand {
2589 program: "wrapper".into(),
2590 args: Vec::new(),
2591 env: ScriptCommandEnvMap::default(),
2592 relative_to: ScriptCommandRelativeTo::None,
2593 },
2594 target_runner: WrapperScriptTargetRunner::OverridesWrapper,
2595 };
2596 let mut cli_wrapper_overrides_no_runner = TestCommandCli::default();
2597 cli_wrapper_overrides_no_runner.apply_wrappers(
2598 Some(&wrapper_overrides),
2599 None,
2600 workspace_root,
2601 target_dir,
2602 );
2603 cli_wrapper_overrides_no_runner.extend(["binary", "arg"]);
2604 assert_eq!(
2605 cli_wrapper_overrides_no_runner.env,
2606 Some(&ScriptCommandEnvMap::default()),
2607 "overrides-wrapper without runner should apply wrapper env"
2608 );
2609 assert_eq!(
2610 cli_wrapper_overrides_no_runner.to_owned_cli(),
2611 vec!["wrapper", "binary", "arg"],
2612 );
2613 }
2614
2615 {
2617 let wrapper_with_args = WrapperScriptConfig {
2618 command: ScriptCommand {
2619 program: "wrapper".into(),
2620 args: vec!["--flag".to_string(), "value".to_string()],
2621 env: ScriptCommandEnvMap::default(),
2622 relative_to: ScriptCommandRelativeTo::None,
2623 },
2624 target_runner: WrapperScriptTargetRunner::Ignore,
2625 };
2626 let mut cli_wrapper_args = TestCommandCli::default();
2627 cli_wrapper_args.apply_wrappers(
2628 Some(&wrapper_with_args),
2629 None,
2630 workspace_root,
2631 target_dir,
2632 );
2633 cli_wrapper_args.extend(["binary", "arg"]);
2634 assert_eq!(cli_wrapper_args.env, Some(&ScriptCommandEnvMap::default()));
2635 assert_eq!(
2636 cli_wrapper_args.to_owned_cli(),
2637 vec!["wrapper", "--flag", "value", "binary", "arg"],
2638 );
2639 }
2640
2641 {
2643 let runner_with_args = PlatformRunner::debug_new(
2644 "runner".into(),
2645 vec!["--runner-flag".into(), "value".into()],
2646 PlatformRunnerSource::Env("fake".to_owned()),
2647 );
2648 let mut cli_runner_args = TestCommandCli::default();
2649 cli_runner_args.apply_wrappers(
2650 None,
2651 Some(&runner_with_args),
2652 workspace_root,
2653 target_dir,
2654 );
2655 cli_runner_args.extend(["binary", "arg"]);
2656 assert!(cli_runner_args.env.is_none());
2657 assert_eq!(
2658 cli_runner_args.to_owned_cli(),
2659 vec!["runner", "--runner-flag", "value", "binary", "arg"],
2660 );
2661 }
2662
2663 {
2665 let wrapper_relative_to_workspace_root = WrapperScriptConfig {
2666 command: ScriptCommand {
2667 program: "abc/def/my-wrapper".into(),
2668 args: vec!["--verbose".to_string()],
2669 env: ScriptCommandEnvMap::default(),
2670 relative_to: ScriptCommandRelativeTo::WorkspaceRoot,
2671 },
2672 target_runner: WrapperScriptTargetRunner::Ignore,
2673 };
2674 let mut cli_wrapper_relative = TestCommandCli::default();
2675 cli_wrapper_relative.apply_wrappers(
2676 Some(&wrapper_relative_to_workspace_root),
2677 None,
2678 workspace_root,
2679 target_dir,
2680 );
2681 cli_wrapper_relative.extend(["binary", "arg"]);
2682
2683 cfg_if::cfg_if! {
2684 if #[cfg(windows)] {
2685 let wrapper_path = "D:\\workspace\\root\\abc\\def\\my-wrapper";
2686 } else {
2687 let wrapper_path = "/workspace/root/abc/def/my-wrapper";
2688 }
2689 }
2690 assert_eq!(
2691 cli_wrapper_relative.env,
2692 Some(&ScriptCommandEnvMap::default())
2693 );
2694 assert_eq!(
2695 cli_wrapper_relative.to_owned_cli(),
2696 vec![wrapper_path, "--verbose", "binary", "arg"],
2697 );
2698 }
2699
2700 {
2702 let wrapper_relative_to_target = WrapperScriptConfig {
2703 command: ScriptCommand {
2704 program: "abc/def/my-wrapper".into(),
2705 args: vec!["--verbose".to_string()],
2706 env: ScriptCommandEnvMap::default(),
2707 relative_to: ScriptCommandRelativeTo::Target,
2708 },
2709 target_runner: WrapperScriptTargetRunner::Ignore,
2710 };
2711 let mut cli_wrapper_relative = TestCommandCli::default();
2712 cli_wrapper_relative.apply_wrappers(
2713 Some(&wrapper_relative_to_target),
2714 None,
2715 workspace_root,
2716 target_dir,
2717 );
2718 cli_wrapper_relative.extend(["binary", "arg"]);
2719 cfg_if::cfg_if! {
2720 if #[cfg(windows)] {
2721 let wrapper_path = "C:\\foo\\bar\\abc\\def\\my-wrapper";
2722 } else {
2723 let wrapper_path = "/foo/bar/abc/def/my-wrapper";
2724 }
2725 }
2726 assert_eq!(
2727 cli_wrapper_relative.env,
2728 Some(&ScriptCommandEnvMap::default())
2729 );
2730 assert_eq!(
2731 cli_wrapper_relative.to_owned_cli(),
2732 vec![wrapper_path, "--verbose", "binary", "arg"],
2733 );
2734 }
2735 }
2736
2737 #[test]
2738 fn test_parse_list_lines() {
2739 let binary_id = RustBinaryId::new("test-package::test-binary");
2740
2741 let input = indoc! {"
2743 simple_test: test
2744 module::nested_test: test
2745 deeply::nested::module::test_name: test
2746 "};
2747 let results: Vec<_> = parse_list_lines(&binary_id, input)
2748 .collect::<Result<_, _>>()
2749 .expect("parsed valid test output");
2750 insta::assert_debug_snapshot!("valid_tests", results);
2751
2752 let input = indoc! {"
2754 simple_bench: benchmark
2755 benches::module::my_benchmark: benchmark
2756 "};
2757 let results: Vec<_> = parse_list_lines(&binary_id, input)
2758 .collect::<Result<_, _>>()
2759 .expect("parsed valid benchmark output");
2760 insta::assert_debug_snapshot!("valid_benchmarks", results);
2761
2762 let input = indoc! {"
2764 test_one: test
2765 bench_one: benchmark
2766 test_two: test
2767 bench_two: benchmark
2768 "};
2769 let results: Vec<_> = parse_list_lines(&binary_id, input)
2770 .collect::<Result<_, _>>()
2771 .expect("parsed mixed output");
2772 insta::assert_debug_snapshot!("mixed_tests_and_benchmarks", results);
2773
2774 let input = indoc! {r#"
2776 test_with_underscore_123: test
2777 test::with::colons: test
2778 test_with_numbers_42: test
2779 "#};
2780 let results: Vec<_> = parse_list_lines(&binary_id, input)
2781 .collect::<Result<_, _>>()
2782 .expect("parsed tests with special characters");
2783 insta::assert_debug_snapshot!("special_characters", results);
2784
2785 let input = "";
2787 let results: Vec<_> = parse_list_lines(&binary_id, input)
2788 .collect::<Result<_, _>>()
2789 .expect("parsed empty output");
2790 insta::assert_debug_snapshot!("empty_input", results);
2791
2792 let input = "invalid_test: wrong_suffix";
2794 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2795 assert!(result.is_err());
2796 insta::assert_snapshot!("invalid_suffix_error", result.unwrap_err());
2797
2798 let input = "test_without_suffix";
2800 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2801 assert!(result.is_err());
2802 insta::assert_snapshot!("missing_suffix_error", result.unwrap_err());
2803
2804 let input = indoc! {"
2806 valid_test: test
2807 invalid_line
2808 another_valid: benchmark
2809 "};
2810 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2811 assert!(result.is_err());
2812 insta::assert_snapshot!("partial_valid_error", result.unwrap_err());
2813
2814 let input = indoc! {"
2816 valid_test: test
2817 \rinvalid_line
2818 another_valid: benchmark
2819 "};
2820 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2821 assert!(result.is_err());
2822 insta::assert_snapshot!("control_character_error", result.unwrap_err());
2823 }
2824
2825 #[proptest]
2828 fn test_instance_id_key_borrow_consistency(
2829 owned1: OwnedTestInstanceId,
2830 owned2: OwnedTestInstanceId,
2831 ) {
2832 let borrowed1: &dyn TestInstanceIdKey = &owned1;
2834 let borrowed2: &dyn TestInstanceIdKey = &owned2;
2835
2836 assert_eq!(
2838 owned1 == owned2,
2839 borrowed1 == borrowed2,
2840 "Eq must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2841 );
2842
2843 assert_eq!(
2845 owned1.partial_cmp(&owned2),
2846 borrowed1.partial_cmp(borrowed2),
2847 "PartialOrd must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2848 );
2849
2850 assert_eq!(
2852 owned1.cmp(&owned2),
2853 borrowed1.cmp(borrowed2),
2854 "Ord must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2855 );
2856
2857 fn hash_value(x: &impl Hash) -> u64 {
2859 let mut hasher = DefaultHasher::new();
2860 x.hash(&mut hasher);
2861 hasher.finish()
2862 }
2863
2864 assert_eq!(
2865 hash_value(&owned1),
2866 hash_value(&borrowed1),
2867 "Hash must be consistent for owned1 and its borrowed form"
2868 );
2869 assert_eq!(
2870 hash_value(&owned2),
2871 hash_value(&borrowed2),
2872 "Hash must be consistent for owned2 and its borrowed form"
2873 );
2874 }
2875
2876 #[derive(Debug)]
2879 struct MockGroupLookup {
2880 group_name: String,
2881 }
2882
2883 impl GroupLookup for MockGroupLookup {
2884 fn is_member_test(
2885 &self,
2886 _test: &nextest_filtering::TestQuery<'_>,
2887 matcher: &nextest_filtering::NameMatcher,
2888 ) -> bool {
2889 matcher.is_match(&self.group_name)
2890 }
2891 }
2892
2893 #[test]
2896 fn test_build_suites_with_group_filter() {
2897 let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
2898
2899 let test_filter = TestFilter::new(
2902 NextestRunMode::Test,
2903 RunIgnored::Default,
2904 TestFilterPatterns::default(),
2905 vec![
2906 Filterset::parse(
2907 "group(serial)".to_owned(),
2908 &cx,
2909 FiltersetKind::Test,
2910 &KnownGroups::Known {
2911 custom_groups: HashSet::from(["serial".to_owned()]),
2912 },
2913 )
2914 .unwrap(),
2915 ],
2916 )
2917 .unwrap();
2918
2919 assert!(
2920 test_filter.has_group_predicates(),
2921 "filter with group() must report has_group_predicates"
2922 );
2923
2924 let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
2925
2926 let make_parsed = || {
2927 vec![ParsedTestBinary::Listed {
2928 artifact: RustTestArtifact {
2929 binary_path: "/fake/binary".into(),
2930 cwd: "/fake/cwd".into(),
2931 package: package_metadata(),
2932 binary_name: "fake-binary".to_owned(),
2933 binary_id: fake_binary_id.clone(),
2934 kind: RustTestBinaryKind::LIB,
2935 non_test_binaries: BTreeSet::new(),
2936 build_platform: BuildPlatform::Target,
2937 },
2938 test_cases: vec![
2939 ParsedTestCase {
2940 name: TestCaseName::new("serial_test"),
2941 kind: RustTestKind::TEST,
2942 ignored: false,
2943 },
2944 ParsedTestCase {
2945 name: TestCaseName::new("parallel_test"),
2946 kind: RustTestKind::TEST,
2947 ignored: false,
2948 },
2949 ],
2950 }]
2951 };
2952
2953 let ecx = EvalContext {
2954 default_filter: &CompiledExpr::ALL,
2955 };
2956
2957 let lookup = MockGroupLookup {
2959 group_name: "serial".to_owned(),
2960 };
2961 let suites = TestList::build_suites(
2962 make_parsed(),
2963 &test_filter,
2964 &ecx,
2965 FilterBound::All,
2966 Some(&lookup),
2967 );
2968 let suite = suites.get(&fake_binary_id).expect("suite exists");
2969 for case in suite.status.test_cases() {
2971 assert_eq!(
2972 case.test_info.filter_match,
2973 FilterMatch::Matches,
2974 "{} should match with serial group lookup",
2975 case.name,
2976 );
2977 }
2978
2979 let lookup_other = MockGroupLookup {
2981 group_name: "batch".to_owned(),
2982 };
2983 let suites = TestList::build_suites(
2984 make_parsed(),
2985 &test_filter,
2986 &ecx,
2987 FilterBound::All,
2988 Some(&lookup_other),
2989 );
2990 let suite = suites.get(&fake_binary_id).expect("suite exists");
2991 for case in suite.status.test_cases() {
2993 assert_eq!(
2994 case.test_info.filter_match,
2995 FilterMatch::Mismatch {
2996 reason: MismatchReason::Expression,
2997 },
2998 "{} should not match with batch group lookup",
2999 case.name,
3000 );
3001 }
3002 }
3003}