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