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