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