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 {
582 reason: MismatchReason::RerunAlreadyPassed,
583 } => {
584 skipped_tests_rerun += 1;
585 true
586 }
587 FilterMatch::Mismatch {
588 reason: MismatchReason::NotBenchmark,
589 } => {
590 skipped_tests_non_benchmark += 1;
591 true
592 }
593 FilterMatch::Mismatch {
594 reason: MismatchReason::DefaultFilter,
595 } => {
596 skipped_tests_default_filter += 1;
597 true
598 }
599 FilterMatch::Mismatch { .. } => true,
600 FilterMatch::Matches => false,
601 })
602 .count();
603
604 let mut skipped_binaries_default_filter = 0;
605 let skipped_binaries = self
606 .rust_suites
607 .iter()
608 .filter(|suite| match suite.status {
609 RustTestSuiteStatus::Skipped {
610 reason: BinaryMismatchReason::DefaultSet,
611 } => {
612 skipped_binaries_default_filter += 1;
613 true
614 }
615 RustTestSuiteStatus::Skipped { .. } => true,
616 RustTestSuiteStatus::Listed { .. } => false,
617 })
618 .count();
619
620 SkipCounts {
621 skipped_tests,
622 skipped_tests_rerun,
623 skipped_tests_non_benchmark,
624 skipped_tests_default_filter,
625 skipped_binaries,
626 skipped_binaries_default_filter,
627 }
628 })
629 }
630
631 pub fn run_count(&self) -> usize {
635 self.test_count - self.skip_counts().skipped_tests
636 }
637
638 pub fn binary_count(&self) -> usize {
640 self.rust_suites.len()
641 }
642
643 pub fn listed_binary_count(&self) -> usize {
645 self.binary_count() - self.skip_counts().skipped_binaries
646 }
647
648 pub fn workspace_root(&self) -> &Utf8Path {
650 &self.workspace_root
651 }
652
653 pub fn cargo_env(&self) -> &EnvironmentMap {
655 &self.env
656 }
657
658 pub fn updated_dylib_path(&self) -> &OsStr {
660 &self.updated_dylib_path
661 }
662
663 pub fn to_summary(&self) -> TestListSummary {
665 let rust_suites = self
666 .rust_suites
667 .iter()
668 .map(|test_suite| {
669 let (status, test_cases) = test_suite.status.to_summary();
670 let testsuite = RustTestSuiteSummary {
671 package_name: test_suite.package.name().to_owned(),
672 binary: RustTestBinarySummary {
673 binary_name: test_suite.binary_name.clone(),
674 package_id: test_suite.package.id().repr().to_owned(),
675 kind: test_suite.kind.clone(),
676 binary_path: test_suite.binary_path.clone(),
677 binary_id: test_suite.binary_id.clone(),
678 build_platform: test_suite.build_platform,
679 },
680 cwd: test_suite.cwd.clone(),
681 status,
682 test_cases,
683 };
684 (test_suite.binary_id.clone(), testsuite)
685 })
686 .collect();
687 let mut summary = TestListSummary::new(self.rust_build_meta.to_summary());
688 summary.test_count = self.test_count;
689 summary.rust_suites = rust_suites;
690 summary
691 }
692
693 pub fn write(
695 &self,
696 output_format: OutputFormat,
697 writer: &mut dyn WriteStr,
698 colorize: bool,
699 ) -> Result<(), WriteTestListError> {
700 match output_format {
701 OutputFormat::Human { verbose } => self
702 .write_human(writer, verbose, colorize)
703 .map_err(WriteTestListError::Io),
704 OutputFormat::Oneline { verbose } => self
705 .write_oneline(writer, verbose, colorize)
706 .map_err(WriteTestListError::Io),
707 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
708 }
709 }
710
711 pub fn iter(&self) -> impl Iterator<Item = &RustTestSuite<'_>> + '_ {
713 self.rust_suites.iter()
714 }
715
716 pub fn get_suite(&self, binary_id: &RustBinaryId) -> Option<&RustTestSuite<'_>> {
718 self.rust_suites.get(binary_id)
719 }
720
721 pub fn iter_tests(&self) -> impl Iterator<Item = TestInstance<'_>> + '_ {
723 self.rust_suites.iter().flat_map(|test_suite| {
724 test_suite
725 .status
726 .test_cases()
727 .map(move |case| TestInstance::new(case, test_suite))
728 })
729 }
730
731 pub fn to_priority_queue(
733 &'g self,
734 profile: &'g EvaluatableProfile<'g>,
735 ) -> TestPriorityQueue<'g> {
736 TestPriorityQueue::new(self, profile)
737 }
738
739 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
741 let mut s = String::with_capacity(1024);
742 self.write(output_format, &mut s, false)?;
743 Ok(s)
744 }
745
746 pub fn empty() -> Self {
754 Self {
755 test_count: 0,
756 mode: NextestRunMode::Test,
757 workspace_root: Utf8PathBuf::new(),
758 rust_build_meta: RustBuildMeta::empty(),
759 env: EnvironmentMap::empty(),
760 updated_dylib_path: OsString::new(),
761 rust_suites: IdOrdMap::new(),
762 skip_counts: OnceLock::new(),
763 }
764 }
765
766 pub(crate) fn create_dylib_path(
767 rust_build_meta: &RustBuildMeta<TestListState>,
768 ) -> Result<OsString, CreateTestListError> {
769 let dylib_path = dylib_path();
770 let dylib_path_is_empty = dylib_path.is_empty();
771 let new_paths = rust_build_meta.dylib_paths();
772
773 let mut updated_dylib_path: Vec<PathBuf> =
774 Vec::with_capacity(dylib_path.len() + new_paths.len());
775 updated_dylib_path.extend(
776 new_paths
777 .iter()
778 .map(|path| path.clone().into_std_path_buf()),
779 );
780 updated_dylib_path.extend(dylib_path);
781
782 if cfg!(target_os = "macos") && dylib_path_is_empty {
789 if let Some(home) = home::home_dir() {
790 updated_dylib_path.push(home.join("lib"));
791 }
792 updated_dylib_path.push("/usr/local/lib".into());
793 updated_dylib_path.push("/usr/lib".into());
794 }
795
796 std::env::join_paths(updated_dylib_path)
797 .map_err(move |error| CreateTestListError::dylib_join_paths(new_paths, error))
798 }
799
800 fn parse_output(
803 test_binary: RustTestArtifact<'g>,
804 non_ignored: impl AsRef<str>,
805 ignored: impl AsRef<str>,
806 ) -> Result<ParsedTestBinary<'g>, CreateTestListError> {
807 let mut test_cases = Vec::new();
808
809 for (test_name, kind) in Self::parse(&test_binary.binary_id, non_ignored.as_ref())? {
810 test_cases.push(ParsedTestCase {
811 name: TestCaseName::new(test_name),
812 kind,
813 ignored: false,
814 });
815 }
816
817 for (test_name, kind) in Self::parse(&test_binary.binary_id, ignored.as_ref())? {
818 test_cases.push(ParsedTestCase {
823 name: TestCaseName::new(test_name),
824 kind,
825 ignored: true,
826 });
827 }
828
829 Ok(ParsedTestBinary::Listed {
830 artifact: test_binary,
831 test_cases,
832 })
833 }
834
835 fn build_suites(
846 parsed: impl IntoIterator<Item = ParsedTestBinary<'g>>,
847 filter: &TestFilter,
848 ecx: &EvalContext<'_>,
849 bound: FilterBound,
850 groups: Option<&dyn GroupLookup>,
851 ) -> IdOrdMap<RustTestSuite<'g>> {
852 parsed
853 .into_iter()
854 .map(|binary| match binary {
855 ParsedTestBinary::Listed {
856 artifact,
857 test_cases,
858 } => {
859 let filtered = {
860 let query = artifact.to_binary_query();
861 let mut map = IdOrdMap::new();
862 for tc in test_cases {
863 let filter_match = filter.filter_match(
864 query, &tc.name, &tc.kind, ecx, bound, tc.ignored, groups,
865 );
866 map.insert_overwrite(RustTestCase {
871 name: tc.name,
872 test_info: RustTestCaseSummary {
873 kind: Some(tc.kind),
874 ignored: tc.ignored,
875 filter_match,
876 },
877 });
878 }
879 map
880 };
881 artifact.into_test_suite(RustTestSuiteStatus::Listed {
882 test_cases: filtered.into(),
883 })
884 }
885 ParsedTestBinary::Skipped { artifact, reason } => {
886 artifact.into_test_suite(RustTestSuiteStatus::Skipped { reason })
887 }
888 })
889 .collect()
890 }
891
892 fn make_skipped(
893 test_binary: RustTestArtifact<'g>,
894 reason: BinaryMismatchReason,
895 ) -> ParsedTestBinary<'g> {
896 ParsedTestBinary::Skipped {
897 artifact: test_binary,
898 reason,
899 }
900 }
901
902 fn collect_test_queries_from_parsed<'a>(
909 parsed_binaries: &'a [ParsedTestBinary<'g>],
910 ) -> Vec<TestQuery<'a>> {
911 parsed_binaries
912 .iter()
913 .filter_map(|binary| match binary {
914 ParsedTestBinary::Listed {
915 artifact,
916 test_cases,
917 } => Some((artifact, test_cases)),
918 ParsedTestBinary::Skipped { .. } => None,
919 })
920 .flat_map(|(artifact, test_cases)| {
921 let binary_query = artifact.to_binary_query();
922 test_cases.iter().map(move |tc| TestQuery {
923 binary_query,
924 test_name: &tc.name,
925 })
926 })
927 .collect()
928 }
929
930 fn apply_partitioning(
936 rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
937 partitioner_builder: Option<&PartitionerBuilder>,
938 ) {
939 let Some(partitioner_builder) = partitioner_builder else {
940 return;
941 };
942
943 match partitioner_builder.scope() {
944 PartitionerScope::PerBinary => {
945 Self::apply_per_binary_partitioning(rust_suites, partitioner_builder);
946 }
947 PartitionerScope::CrossBinary => {
948 Self::apply_cross_binary_partitioning(rust_suites, partitioner_builder);
949 }
950 }
951 }
952
953 fn apply_per_binary_partitioning(
956 rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
957 partitioner_builder: &PartitionerBuilder,
958 ) {
959 for mut suite in rust_suites.iter_mut() {
960 let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
961 continue;
962 };
963
964 let mut non_ignored_partitioner = partitioner_builder.build();
966 apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
967
968 let mut ignored_partitioner = partitioner_builder.build();
969 apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
970 }
971 }
972
973 fn apply_cross_binary_partitioning(
977 rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
978 partitioner_builder: &PartitionerBuilder,
979 ) {
980 let mut non_ignored_partitioner = partitioner_builder.build();
982 for mut suite in rust_suites.iter_mut() {
983 let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
984 continue;
985 };
986 apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
987 }
988
989 let mut ignored_partitioner = partitioner_builder.build();
991 for mut suite in rust_suites.iter_mut() {
992 let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
993 continue;
994 };
995 apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
996 }
997 }
998
999 fn parse<'a>(
1001 binary_id: &'a RustBinaryId,
1002 list_output: &'a str,
1003 ) -> Result<Vec<(&'a str, RustTestKind)>, CreateTestListError> {
1004 let mut list = parse_list_lines(binary_id, list_output).collect::<Result<Vec<_>, _>>()?;
1005 list.sort_unstable();
1006 Ok(list)
1007 }
1008
1009 pub fn write_human(
1011 &self,
1012 writer: &mut dyn WriteStr,
1013 verbose: bool,
1014 colorize: bool,
1015 ) -> io::Result<()> {
1016 self.write_human_impl(None, writer, verbose, colorize)
1017 }
1018
1019 pub(crate) fn write_human_with_filter(
1021 &self,
1022 filter: &TestListDisplayFilter<'_>,
1023 writer: &mut dyn WriteStr,
1024 verbose: bool,
1025 colorize: bool,
1026 ) -> io::Result<()> {
1027 self.write_human_impl(Some(filter), writer, verbose, colorize)
1028 }
1029
1030 fn write_human_impl(
1031 &self,
1032 filter: Option<&TestListDisplayFilter<'_>>,
1033 mut writer: &mut dyn WriteStr,
1034 verbose: bool,
1035 colorize: bool,
1036 ) -> io::Result<()> {
1037 let mut styles = Styles::default();
1038 if colorize {
1039 styles.colorize();
1040 }
1041
1042 for info in &self.rust_suites {
1043 let matcher = match filter {
1044 Some(filter) => match filter.matcher_for(&info.binary_id) {
1045 Some(matcher) => matcher,
1046 None => continue,
1047 },
1048 None => DisplayFilterMatcher::All,
1049 };
1050
1051 if !verbose
1054 && info
1055 .status
1056 .test_cases()
1057 .all(|case| !case.test_info.filter_match.is_match())
1058 {
1059 continue;
1060 }
1061
1062 writeln!(writer, "{}:", info.binary_id.style(styles.binary_id))?;
1063 if verbose {
1064 writeln!(
1065 writer,
1066 " {} {}",
1067 "bin:".style(styles.field),
1068 info.binary_path
1069 )?;
1070 writeln!(writer, " {} {}", "cwd:".style(styles.field), info.cwd)?;
1071 writeln!(
1072 writer,
1073 " {} {}",
1074 "build platform:".style(styles.field),
1075 info.build_platform,
1076 )?;
1077 }
1078
1079 let mut indented = indented(writer).with_str(" ");
1080
1081 match &info.status {
1082 RustTestSuiteStatus::Listed { test_cases } => {
1083 let matching_tests: Vec<_> = test_cases
1084 .iter()
1085 .filter(|case| matcher.is_match(&case.name))
1086 .collect();
1087 if matching_tests.is_empty() {
1088 writeln!(indented, "(no tests)")?;
1089 } else {
1090 for case in matching_tests {
1091 match (verbose, case.test_info.filter_match.is_match()) {
1092 (_, true) => {
1093 write_test_name(&case.name, &styles, &mut indented)?;
1094 writeln!(indented)?;
1095 }
1096 (true, false) => {
1097 write_test_name(&case.name, &styles, &mut indented)?;
1098 writeln!(indented, " (skipped)")?;
1099 }
1100 (false, false) => {
1101 }
1103 }
1104 }
1105 }
1106 }
1107 RustTestSuiteStatus::Skipped { reason } => {
1108 writeln!(indented, "(test binary {reason}, skipped)")?;
1109 }
1110 }
1111
1112 writer = indented.into_inner();
1113 }
1114 Ok(())
1115 }
1116
1117 pub fn write_oneline(
1119 &self,
1120 writer: &mut dyn WriteStr,
1121 verbose: bool,
1122 colorize: bool,
1123 ) -> io::Result<()> {
1124 let mut styles = Styles::default();
1125 if colorize {
1126 styles.colorize();
1127 }
1128
1129 for info in &self.rust_suites {
1130 match &info.status {
1131 RustTestSuiteStatus::Listed { test_cases } => {
1132 for case in test_cases.iter() {
1133 let is_match = case.test_info.filter_match.is_match();
1134 if !verbose && !is_match {
1136 continue;
1137 }
1138
1139 write!(writer, "{} ", info.binary_id.style(styles.binary_id))?;
1140 write_test_name(&case.name, &styles, writer)?;
1141
1142 if verbose {
1143 write!(
1144 writer,
1145 " [{}{}] [{}{}] [{}{}]{}",
1146 "bin: ".style(styles.field),
1147 info.binary_path,
1148 "cwd: ".style(styles.field),
1149 info.cwd,
1150 "build platform: ".style(styles.field),
1151 info.build_platform,
1152 if is_match { "" } else { " (skipped)" },
1153 )?;
1154 }
1155
1156 writeln!(writer)?;
1157 }
1158 }
1159 RustTestSuiteStatus::Skipped { .. } => {
1160 }
1162 }
1163 }
1164
1165 Ok(())
1166 }
1167}
1168
1169fn apply_partitioner_to_tests(
1171 test_cases: &mut IdOrdMap<RustTestCase>,
1172 partitioner: &mut dyn Partitioner,
1173 ignored: bool,
1174) {
1175 for mut test_case in test_cases.iter_mut() {
1176 if test_case.test_info.ignored == ignored {
1177 apply_partition_to_test(&mut test_case, partitioner);
1178 }
1179 }
1180}
1181
1182fn apply_partition_to_test(test_case: &mut RustTestCase, partitioner: &mut dyn Partitioner) {
1190 match test_case.test_info.filter_match {
1191 FilterMatch::Matches => {
1192 if !partitioner.test_matches(test_case.name.as_str()) {
1193 test_case.test_info.filter_match = FilterMatch::Mismatch {
1194 reason: MismatchReason::Partition,
1195 };
1196 }
1197 }
1198 FilterMatch::Mismatch {
1199 reason: MismatchReason::RerunAlreadyPassed,
1200 } => {
1201 let _ = partitioner.test_matches(test_case.name.as_str());
1203 }
1204 FilterMatch::Mismatch { .. } => {
1205 }
1207 }
1208}
1209
1210fn parse_list_lines<'a>(
1211 binary_id: &'a RustBinaryId,
1212 list_output: &'a str,
1213) -> impl Iterator<Item = Result<(&'a str, RustTestKind), CreateTestListError>> + 'a + use<'a> {
1214 list_output
1220 .lines()
1221 .map(move |line| match line.strip_suffix(": test") {
1222 Some(test_name) => Ok((test_name, RustTestKind::TEST)),
1223 None => match line.strip_suffix(": benchmark") {
1224 Some(test_name) => Ok((test_name, RustTestKind::BENCH)),
1225 None => Err(CreateTestListError::parse_line(
1226 binary_id.clone(),
1227 format!(
1228 "line {line:?} did not end with the string \": test\" or \": benchmark\""
1229 ),
1230 list_output,
1231 )),
1232 },
1233 })
1234}
1235
1236pub trait ListProfile {
1238 fn filterset_ecx(&self) -> EvalContext<'_>;
1240
1241 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_>;
1243
1244 fn precompute_group_memberships<'a>(
1246 &self,
1247 _tests: impl Iterator<Item = TestQuery<'a>>,
1248 ) -> PrecomputedGroupMembership;
1249}
1250
1251impl<'g> ListProfile for EvaluatableProfile<'g> {
1252 fn filterset_ecx(&self) -> EvalContext<'_> {
1253 self.filterset_ecx()
1254 }
1255
1256 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1257 self.list_settings_for(query)
1258 }
1259
1260 fn precompute_group_memberships<'a>(
1261 &self,
1262 tests: impl Iterator<Item = TestQuery<'a>>,
1263 ) -> PrecomputedGroupMembership {
1264 EvaluatableProfile::precompute_group_memberships(self, tests)
1265 }
1266}
1267
1268pub struct TestPriorityQueue<'a> {
1270 tests: Vec<TestInstanceWithSettings<'a>>,
1271}
1272
1273impl<'a> TestPriorityQueue<'a> {
1274 fn new(test_list: &'a TestList<'a>, profile: &'a EvaluatableProfile<'a>) -> Self {
1275 let mode = test_list.mode();
1276 let mut tests = test_list
1277 .iter_tests()
1278 .map(|instance| {
1279 let settings = profile.settings_for(mode, &instance.to_test_query());
1280 TestInstanceWithSettings { instance, settings }
1281 })
1282 .collect::<Vec<_>>();
1283 tests.sort_by_key(|test| test.settings.priority());
1286
1287 Self { tests }
1288 }
1289}
1290
1291impl<'a> IntoIterator for TestPriorityQueue<'a> {
1292 type Item = TestInstanceWithSettings<'a>;
1293 type IntoIter = std::vec::IntoIter<Self::Item>;
1294
1295 fn into_iter(self) -> Self::IntoIter {
1296 self.tests.into_iter()
1297 }
1298}
1299
1300#[derive(Debug)]
1304pub struct TestInstanceWithSettings<'a> {
1305 pub instance: TestInstance<'a>,
1307
1308 pub settings: TestSettings<'a>,
1310}
1311
1312#[derive(Clone, Debug, Eq, PartialEq)]
1316pub struct RustTestSuite<'g> {
1317 pub binary_id: RustBinaryId,
1319
1320 pub binary_path: Utf8PathBuf,
1322
1323 pub package: PackageMetadata<'g>,
1325
1326 pub binary_name: String,
1328
1329 pub kind: RustTestBinaryKind,
1331
1332 pub cwd: Utf8PathBuf,
1335
1336 pub build_platform: BuildPlatform,
1338
1339 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
1341
1342 pub status: RustTestSuiteStatus,
1344}
1345
1346impl<'g> RustTestSuite<'g> {
1347 pub fn to_binary_query(&self) -> BinaryQuery<'_> {
1349 BinaryQuery {
1350 package_id: self.package.id(),
1351 binary_id: &self.binary_id,
1352 kind: &self.kind,
1353 binary_name: &self.binary_name,
1354 platform: convert_build_platform(self.build_platform),
1355 }
1356 }
1357}
1358
1359impl IdOrdItem for RustTestSuite<'_> {
1360 type Key<'a>
1361 = &'a RustBinaryId
1362 where
1363 Self: 'a;
1364
1365 fn key(&self) -> Self::Key<'_> {
1366 &self.binary_id
1367 }
1368
1369 id_upcast!();
1370}
1371
1372impl RustTestArtifact<'_> {
1373 async fn exec(
1375 &self,
1376 lctx: &LocalExecuteContext<'_>,
1377 list_settings: &ListSettings<'_>,
1378 target_runner: &TargetRunner,
1379 ) -> Result<(String, String), CreateTestListError> {
1380 if !self.cwd.is_dir() {
1383 return Err(CreateTestListError::CwdIsNotDir {
1384 binary_id: self.binary_id.clone(),
1385 cwd: self.cwd.clone(),
1386 });
1387 }
1388 let platform_runner = target_runner.for_build_platform(self.build_platform);
1389
1390 let non_ignored = self.exec_single(false, lctx, list_settings, platform_runner);
1391 let ignored = self.exec_single(true, lctx, list_settings, platform_runner);
1392
1393 let (non_ignored_out, ignored_out) = futures::future::join(non_ignored, ignored).await;
1394 Ok((non_ignored_out?, ignored_out?))
1395 }
1396
1397 async fn exec_single(
1398 &self,
1399 ignored: bool,
1400 lctx: &LocalExecuteContext<'_>,
1401 list_settings: &ListSettings<'_>,
1402 runner: Option<&PlatformRunner>,
1403 ) -> Result<String, CreateTestListError> {
1404 let mut cli = TestCommandCli::default();
1405 cli.apply_wrappers(
1406 list_settings.list_wrapper(),
1407 runner,
1408 lctx.workspace_root,
1409 &lctx.rust_build_meta.target_directory,
1410 );
1411 cli.push(self.binary_path.as_str());
1412
1413 cli.extend(["--list", "--format", "terse"]);
1414 if ignored {
1415 cli.push("--ignored");
1416 }
1417
1418 let mut cmd = TestCommand::new(
1419 lctx,
1420 cli.program
1421 .clone()
1422 .expect("at least one argument passed in")
1423 .into_owned(),
1424 &cli.args,
1425 cli.env,
1426 &self.cwd,
1427 &self.package,
1428 &self.non_test_binaries,
1429 &Interceptor::None, );
1431
1432 cmd.command_mut()
1434 .env("NEXTEST_RUN_ID", lctx.run_id.to_string())
1435 .env("NEXTEST_BINARY_ID", self.binary_id.as_str())
1436 .env("NEXTEST_WORKSPACE_ROOT", lctx.workspace_root.as_str());
1437 lctx.version_env_vars.apply_env(cmd.command_mut());
1438
1439 let output =
1440 cmd.wait_with_output()
1441 .await
1442 .map_err(|error| CreateTestListError::CommandExecFail {
1443 binary_id: self.binary_id.clone(),
1444 command: cli.to_owned_cli(),
1445 error,
1446 })?;
1447
1448 if output.status.success() {
1449 String::from_utf8(output.stdout).map_err(|err| CreateTestListError::CommandNonUtf8 {
1450 binary_id: self.binary_id.clone(),
1451 command: cli.to_owned_cli(),
1452 stdout: err.into_bytes(),
1453 stderr: output.stderr,
1454 })
1455 } else {
1456 Err(CreateTestListError::CommandFail {
1457 binary_id: self.binary_id.clone(),
1458 command: cli.to_owned_cli(),
1459 exit_status: output.status,
1460 stdout: output.stdout,
1461 stderr: output.stderr,
1462 })
1463 }
1464 }
1465}
1466
1467enum ParsedTestBinary<'g> {
1473 Listed {
1475 artifact: RustTestArtifact<'g>,
1477
1478 test_cases: Vec<ParsedTestCase>,
1480 },
1481
1482 Skipped {
1484 artifact: RustTestArtifact<'g>,
1486
1487 reason: BinaryMismatchReason,
1489 },
1490}
1491
1492struct ParsedTestCase {
1497 name: TestCaseName,
1498 kind: RustTestKind,
1499 ignored: bool,
1500}
1501
1502#[derive(Clone, Debug, Eq, PartialEq)]
1506pub enum RustTestSuiteStatus {
1507 Listed {
1509 test_cases: DebugIgnore<IdOrdMap<RustTestCase>>,
1511 },
1512
1513 Skipped {
1515 reason: BinaryMismatchReason,
1517 },
1518}
1519
1520static EMPTY_TEST_CASE_MAP: IdOrdMap<RustTestCase> = IdOrdMap::new();
1521
1522impl RustTestSuiteStatus {
1523 pub fn test_count(&self) -> usize {
1525 match self {
1526 RustTestSuiteStatus::Listed { test_cases } => test_cases.len(),
1527 RustTestSuiteStatus::Skipped { .. } => 0,
1528 }
1529 }
1530
1531 pub fn get(&self, name: &TestCaseName) -> Option<&RustTestCase> {
1533 match self {
1534 RustTestSuiteStatus::Listed { test_cases } => test_cases.get(name),
1535 RustTestSuiteStatus::Skipped { .. } => None,
1536 }
1537 }
1538
1539 pub fn test_cases(&self) -> impl Iterator<Item = &RustTestCase> + '_ {
1541 match self {
1542 RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
1543 RustTestSuiteStatus::Skipped { .. } => {
1544 EMPTY_TEST_CASE_MAP.iter()
1546 }
1547 }
1548 }
1549
1550 pub fn to_summary(
1552 &self,
1553 ) -> (
1554 RustTestSuiteStatusSummary,
1555 BTreeMap<TestCaseName, RustTestCaseSummary>,
1556 ) {
1557 match self {
1558 Self::Listed { test_cases } => (
1559 RustTestSuiteStatusSummary::LISTED,
1560 test_cases
1561 .iter()
1562 .cloned()
1563 .map(|case| (case.name, case.test_info))
1564 .collect(),
1565 ),
1566 Self::Skipped {
1567 reason: BinaryMismatchReason::Expression,
1568 } => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
1569 Self::Skipped {
1570 reason: BinaryMismatchReason::DefaultSet,
1571 } => (
1572 RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER,
1573 BTreeMap::new(),
1574 ),
1575 }
1576 }
1577}
1578
1579#[derive(Clone, Debug, Eq, PartialEq)]
1581pub struct RustTestCase {
1582 pub name: TestCaseName,
1584
1585 pub test_info: RustTestCaseSummary,
1587}
1588
1589impl IdOrdItem for RustTestCase {
1590 type Key<'a> = &'a TestCaseName;
1591 fn key(&self) -> Self::Key<'_> {
1592 &self.name
1593 }
1594 id_upcast!();
1595}
1596
1597#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1599pub struct TestInstance<'a> {
1600 pub name: &'a TestCaseName,
1602
1603 pub suite_info: &'a RustTestSuite<'a>,
1605
1606 pub test_info: &'a RustTestCaseSummary,
1608}
1609
1610impl<'a> TestInstance<'a> {
1611 pub(crate) fn new(case: &'a RustTestCase, suite_info: &'a RustTestSuite) -> Self {
1613 Self {
1614 name: &case.name,
1615 suite_info,
1616 test_info: &case.test_info,
1617 }
1618 }
1619
1620 #[inline]
1623 pub fn id(&self) -> TestInstanceId<'a> {
1624 TestInstanceId {
1625 binary_id: &self.suite_info.binary_id,
1626 test_name: self.name,
1627 }
1628 }
1629
1630 pub fn to_test_query(&self) -> TestQuery<'a> {
1632 TestQuery {
1633 binary_query: BinaryQuery {
1634 package_id: self.suite_info.package.id(),
1635 binary_id: &self.suite_info.binary_id,
1636 kind: &self.suite_info.kind,
1637 binary_name: &self.suite_info.binary_name,
1638 platform: convert_build_platform(self.suite_info.build_platform),
1639 },
1640 test_name: self.name,
1641 }
1642 }
1643
1644 pub(crate) fn make_command(
1646 &self,
1647 ctx: &TestExecuteContext<'_>,
1648 test_list: &TestList<'_>,
1649 wrapper_script: Option<&WrapperScriptConfig>,
1650 extra_args: &[String],
1651 interceptor: &Interceptor,
1652 ) -> TestCommand {
1653 let cli = self.compute_cli(ctx, test_list, wrapper_script, extra_args);
1655
1656 let lctx = LocalExecuteContext {
1657 phase: TestCommandPhase::Run,
1658 run_id: ctx.run_id,
1659 version_env_vars: ctx.version_env_vars,
1660 workspace_root: test_list.workspace_root(),
1661 rust_build_meta: &test_list.rust_build_meta,
1662 double_spawn: ctx.double_spawn,
1663 dylib_path: test_list.updated_dylib_path(),
1664 profile_name: ctx.profile_name,
1665 env: &test_list.env,
1666 };
1667
1668 TestCommand::new(
1669 &lctx,
1670 cli.program
1671 .expect("at least one argument is guaranteed")
1672 .into_owned(),
1673 &cli.args,
1674 cli.env,
1675 &self.suite_info.cwd,
1676 &self.suite_info.package,
1677 &self.suite_info.non_test_binaries,
1678 interceptor,
1679 )
1680 }
1681
1682 pub(crate) fn command_line(
1683 &self,
1684 ctx: &TestExecuteContext<'_>,
1685 test_list: &TestList<'_>,
1686 wrapper_script: Option<&WrapperScriptConfig>,
1687 extra_args: &[String],
1688 ) -> Vec<String> {
1689 self.compute_cli(ctx, test_list, wrapper_script, extra_args)
1690 .to_owned_cli()
1691 }
1692
1693 fn compute_cli(
1694 &self,
1695 ctx: &'a TestExecuteContext<'_>,
1696 test_list: &TestList<'_>,
1697 wrapper_script: Option<&'a WrapperScriptConfig>,
1698 extra_args: &'a [String],
1699 ) -> TestCommandCli<'a> {
1700 let platform_runner = ctx
1701 .target_runner
1702 .for_build_platform(self.suite_info.build_platform);
1703
1704 let mut cli = TestCommandCli::default();
1705 cli.apply_wrappers(
1706 wrapper_script,
1707 platform_runner,
1708 test_list.workspace_root(),
1709 &test_list.rust_build_meta().target_directory,
1710 );
1711 cli.push(self.suite_info.binary_path.as_str());
1712
1713 cli.extend(["--exact", self.name.as_str(), "--nocapture"]);
1714 if self.test_info.ignored {
1715 cli.push("--ignored");
1716 }
1717 match test_list.mode() {
1718 NextestRunMode::Test => {}
1719 NextestRunMode::Benchmark => {
1720 cli.push("--bench");
1721 }
1722 }
1723 cli.extend(extra_args.iter().map(String::as_str));
1724
1725 cli
1726 }
1727}
1728
1729#[derive(Clone, Debug, Default)]
1730struct TestCommandCli<'a> {
1731 program: Option<Cow<'a, str>>,
1732 args: Vec<Cow<'a, str>>,
1733 env: Option<&'a ScriptCommandEnvMap>,
1734}
1735
1736impl<'a> TestCommandCli<'a> {
1737 fn apply_wrappers(
1738 &mut self,
1739 wrapper_script: Option<&'a WrapperScriptConfig>,
1740 platform_runner: Option<&'a PlatformRunner>,
1741 workspace_root: &Utf8Path,
1742 target_dir: &Utf8Path,
1743 ) {
1744 if let Some(wrapper) = wrapper_script {
1746 match wrapper.target_runner {
1747 WrapperScriptTargetRunner::Ignore => {
1748 self.env = Some(&wrapper.command.env);
1750 self.push(wrapper.command.program(workspace_root, target_dir));
1751 self.extend(wrapper.command.args.iter().map(String::as_str));
1752 }
1753 WrapperScriptTargetRunner::AroundWrapper => {
1754 self.env = Some(&wrapper.command.env);
1756 if let Some(runner) = platform_runner {
1757 self.push(runner.binary());
1758 self.extend(runner.args());
1759 }
1760 self.push(wrapper.command.program(workspace_root, target_dir));
1761 self.extend(wrapper.command.args.iter().map(String::as_str));
1762 }
1763 WrapperScriptTargetRunner::WithinWrapper => {
1764 self.env = Some(&wrapper.command.env);
1766 self.push(wrapper.command.program(workspace_root, target_dir));
1767 self.extend(wrapper.command.args.iter().map(String::as_str));
1768 if let Some(runner) = platform_runner {
1769 self.push(runner.binary());
1770 self.extend(runner.args());
1771 }
1772 }
1773 WrapperScriptTargetRunner::OverridesWrapper => {
1774 if let Some(runner) = platform_runner {
1775 self.push(runner.binary());
1778 self.extend(runner.args());
1779 } else {
1780 self.env = Some(&wrapper.command.env);
1782 self.push(wrapper.command.program(workspace_root, target_dir));
1783 self.extend(wrapper.command.args.iter().map(String::as_str));
1784 }
1785 }
1786 }
1787 } else {
1788 if let Some(runner) = platform_runner {
1790 self.push(runner.binary());
1791 self.extend(runner.args());
1792 }
1793 }
1794 }
1795
1796 fn push(&mut self, arg: impl Into<Cow<'a, str>>) {
1797 if self.program.is_none() {
1798 self.program = Some(arg.into());
1799 } else {
1800 self.args.push(arg.into());
1801 }
1802 }
1803
1804 fn extend(&mut self, args: impl IntoIterator<Item = &'a str>) {
1805 for arg in args {
1806 self.push(arg);
1807 }
1808 }
1809
1810 fn to_owned_cli(&self) -> Vec<String> {
1811 let mut owned_cli = Vec::new();
1812 if let Some(program) = &self.program {
1813 owned_cli.push(program.to_string());
1814 }
1815 owned_cli.extend(self.args.iter().map(|arg| arg.to_string()));
1816 owned_cli
1817 }
1818}
1819
1820#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize)]
1824pub struct TestInstanceId<'a> {
1825 pub binary_id: &'a RustBinaryId,
1827
1828 pub test_name: &'a TestCaseName,
1830}
1831
1832impl TestInstanceId<'_> {
1833 pub fn attempt_id(
1837 &self,
1838 run_id: ReportUuid,
1839 stress_index: Option<u32>,
1840 attempt: u32,
1841 ) -> String {
1842 let mut out = String::new();
1843 swrite!(out, "{run_id}:{}", self.binary_id);
1844 if let Some(stress_index) = stress_index {
1845 swrite!(out, "@stress-{}", stress_index);
1846 }
1847 swrite!(out, "${}", self.test_name);
1848 if attempt > 1 {
1849 swrite!(out, "#{attempt}");
1850 }
1851
1852 out
1853 }
1854}
1855
1856impl fmt::Display for TestInstanceId<'_> {
1857 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1858 write!(f, "{} {}", self.binary_id, self.test_name)
1859 }
1860}
1861
1862#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
1864#[serde(rename_all = "kebab-case")]
1865#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1866pub struct OwnedTestInstanceId {
1867 pub binary_id: RustBinaryId,
1869
1870 #[serde(rename = "name")]
1872 pub test_name: TestCaseName,
1873}
1874
1875impl OwnedTestInstanceId {
1876 pub fn as_ref(&self) -> TestInstanceId<'_> {
1878 TestInstanceId {
1879 binary_id: &self.binary_id,
1880 test_name: &self.test_name,
1881 }
1882 }
1883}
1884
1885impl TestInstanceId<'_> {
1886 pub fn to_owned(&self) -> OwnedTestInstanceId {
1888 OwnedTestInstanceId {
1889 binary_id: self.binary_id.clone(),
1890 test_name: self.test_name.clone(),
1891 }
1892 }
1893}
1894
1895pub trait TestInstanceIdKey {
1901 fn key<'k>(&'k self) -> TestInstanceId<'k>;
1903}
1904
1905impl TestInstanceIdKey for OwnedTestInstanceId {
1906 fn key<'k>(&'k self) -> TestInstanceId<'k> {
1907 TestInstanceId {
1908 binary_id: &self.binary_id,
1909 test_name: &self.test_name,
1910 }
1911 }
1912}
1913
1914impl<'a> TestInstanceIdKey for TestInstanceId<'a> {
1915 fn key<'k>(&'k self) -> TestInstanceId<'k> {
1916 *self
1917 }
1918}
1919
1920impl<'a> Borrow<dyn TestInstanceIdKey + 'a> for OwnedTestInstanceId {
1921 fn borrow(&self) -> &(dyn TestInstanceIdKey + 'a) {
1922 self
1923 }
1924}
1925
1926impl<'a> PartialEq for dyn TestInstanceIdKey + 'a {
1927 fn eq(&self, other: &(dyn TestInstanceIdKey + 'a)) -> bool {
1928 self.key() == other.key()
1929 }
1930}
1931
1932impl<'a> Eq for dyn TestInstanceIdKey + 'a {}
1933
1934impl<'a> PartialOrd for dyn TestInstanceIdKey + 'a {
1935 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1936 Some(self.cmp(other))
1937 }
1938}
1939
1940impl<'a> Ord for dyn TestInstanceIdKey + 'a {
1941 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1942 self.key().cmp(&other.key())
1943 }
1944}
1945
1946impl<'a> Hash for dyn TestInstanceIdKey + 'a {
1947 fn hash<H: Hasher>(&self, state: &mut H) {
1948 self.key().hash(state);
1949 }
1950}
1951
1952#[derive(Clone, Debug)]
1954pub struct TestExecuteContext<'a> {
1955 pub run_id: ReportUuid,
1957
1958 pub version_env_vars: &'a VersionEnvVars,
1960
1961 pub profile_name: &'a str,
1963
1964 pub double_spawn: &'a DoubleSpawnInfo,
1966
1967 pub target_runner: &'a TargetRunner,
1969}
1970
1971#[cfg(test)]
1972mod tests {
1973 use super::*;
1974 use crate::{
1975 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
1976 config::scripts::{ScriptCommand, ScriptCommandEnvMap, ScriptCommandRelativeTo},
1977 list::{
1978 SerializableFormat,
1979 test_helpers::{PACKAGE_GRAPH_FIXTURE, package_metadata},
1980 },
1981 platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
1982 target_runner::PlatformRunnerSource,
1983 test_filter::{RunIgnored, TestFilterPatterns},
1984 };
1985 use iddqd::id_ord_map;
1986 use indoc::indoc;
1987 use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, KnownGroups, ParseContext};
1988 use nextest_metadata::{FilterMatch, MismatchReason, PlatformLibdirUnavailable, RustTestKind};
1989 use pretty_assertions::assert_eq;
1990 use std::{
1991 collections::{BTreeMap, HashSet},
1992 hash::DefaultHasher,
1993 };
1994 use target_spec::Platform;
1995 use test_strategy::proptest;
1996
1997 #[test]
1998 fn test_parse_test_list() {
1999 let non_ignored_output = indoc! {"
2001 tests::foo::test_bar: test
2002 tests::baz::test_quux: test
2003 benches::bench_foo: benchmark
2004 "};
2005 let ignored_output = indoc! {"
2006 tests::ignored::test_bar: test
2007 tests::baz::test_ignored: test
2008 benches::ignored_bench_foo: benchmark
2009 "};
2010
2011 let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
2012
2013 let test_filter = TestFilter::new(
2014 NextestRunMode::Test,
2015 RunIgnored::Default,
2016 TestFilterPatterns::default(),
2017 vec![
2019 Filterset::parse(
2020 "platform(target)".to_owned(),
2021 &cx,
2022 FiltersetKind::Test,
2023 &KnownGroups::Known {
2024 custom_groups: HashSet::new(),
2025 },
2026 )
2027 .unwrap(),
2028 ],
2029 )
2030 .unwrap();
2031 let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
2032 let fake_binary_name = "fake-binary".to_owned();
2033 let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
2034
2035 let test_binary = RustTestArtifact {
2036 binary_path: "/fake/binary".into(),
2037 cwd: fake_cwd.clone(),
2038 package: package_metadata(),
2039 binary_name: fake_binary_name.clone(),
2040 binary_id: fake_binary_id.clone(),
2041 kind: RustTestBinaryKind::LIB,
2042 non_test_binaries: BTreeSet::new(),
2043 build_platform: BuildPlatform::Target,
2044 };
2045
2046 let skipped_binary_name = "skipped-binary".to_owned();
2047 let skipped_binary_id = RustBinaryId::new("fake-package::skipped-binary");
2048 let skipped_binary = RustTestArtifact {
2049 binary_path: "/fake/skipped-binary".into(),
2050 cwd: fake_cwd.clone(),
2051 package: package_metadata(),
2052 binary_name: skipped_binary_name.clone(),
2053 binary_id: skipped_binary_id.clone(),
2054 kind: RustTestBinaryKind::PROC_MACRO,
2055 non_test_binaries: BTreeSet::new(),
2056 build_platform: BuildPlatform::Host,
2057 };
2058
2059 let fake_triple = TargetTriple {
2060 platform: Platform::new(
2061 "aarch64-unknown-linux-gnu",
2062 target_spec::TargetFeatures::Unknown,
2063 )
2064 .unwrap(),
2065 source: TargetTripleSource::CliOption,
2066 location: TargetDefinitionLocation::Builtin,
2067 };
2068 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
2069 let build_platforms = BuildPlatforms {
2070 host: HostPlatform {
2071 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
2072 libdir: PlatformLibdir::Available(fake_host_libdir.into()),
2073 },
2074 target: Some(TargetPlatform {
2075 triple: fake_triple,
2076 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
2078 }),
2079 };
2080
2081 let fake_env = EnvironmentMap::empty();
2082 let rust_build_meta =
2083 RustBuildMeta::new("/fake", "/fake", build_platforms).map_paths(&PathMapper::noop());
2084 let ecx = EvalContext {
2085 default_filter: &CompiledExpr::ALL,
2086 };
2087 let test_list = TestList::new_with_outputs(
2088 [
2089 (test_binary, &non_ignored_output, &ignored_output),
2090 (
2091 skipped_binary,
2092 &"should-not-show-up-stdout",
2093 &"should-not-show-up-stderr",
2094 ),
2095 ],
2096 Utf8PathBuf::from("/fake/path"),
2097 rust_build_meta,
2098 &test_filter,
2099 None,
2100 fake_env,
2101 &ecx,
2102 FilterBound::All,
2103 )
2104 .expect("valid output");
2105 assert_eq!(
2106 test_list.rust_suites,
2107 id_ord_map! {
2108 RustTestSuite {
2109 status: RustTestSuiteStatus::Listed {
2110 test_cases: id_ord_map! {
2111 RustTestCase {
2112 name: TestCaseName::new("tests::foo::test_bar"),
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("tests::baz::test_quux"),
2121 test_info: RustTestCaseSummary {
2122 kind: Some(RustTestKind::TEST),
2123 ignored: false,
2124 filter_match: FilterMatch::Matches,
2125 },
2126 },
2127 RustTestCase {
2128 name: TestCaseName::new("benches::bench_foo"),
2129 test_info: RustTestCaseSummary {
2130 kind: Some(RustTestKind::BENCH),
2131 ignored: false,
2132 filter_match: FilterMatch::Matches,
2133 },
2134 },
2135 RustTestCase {
2136 name: TestCaseName::new("tests::ignored::test_bar"),
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("tests::baz::test_ignored"),
2145 test_info: RustTestCaseSummary {
2146 kind: Some(RustTestKind::TEST),
2147 ignored: true,
2148 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
2149 },
2150 },
2151 RustTestCase {
2152 name: TestCaseName::new("benches::ignored_bench_foo"),
2153 test_info: RustTestCaseSummary {
2154 kind: Some(RustTestKind::BENCH),
2155 ignored: true,
2156 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
2157 },
2158 },
2159 }.into(),
2160 },
2161 cwd: fake_cwd.clone(),
2162 build_platform: BuildPlatform::Target,
2163 package: package_metadata(),
2164 binary_name: fake_binary_name,
2165 binary_id: fake_binary_id,
2166 binary_path: "/fake/binary".into(),
2167 kind: RustTestBinaryKind::LIB,
2168 non_test_binaries: BTreeSet::new(),
2169 },
2170 RustTestSuite {
2171 status: RustTestSuiteStatus::Skipped {
2172 reason: BinaryMismatchReason::Expression,
2173 },
2174 cwd: fake_cwd,
2175 build_platform: BuildPlatform::Host,
2176 package: package_metadata(),
2177 binary_name: skipped_binary_name,
2178 binary_id: skipped_binary_id,
2179 binary_path: "/fake/skipped-binary".into(),
2180 kind: RustTestBinaryKind::PROC_MACRO,
2181 non_test_binaries: BTreeSet::new(),
2182 },
2183 }
2184 );
2185
2186 static EXPECTED_HUMAN: &str = indoc! {"
2188 fake-package::fake-binary:
2189 benches::bench_foo
2190 tests::baz::test_quux
2191 tests::foo::test_bar
2192 "};
2193 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {"
2194 fake-package::fake-binary:
2195 bin: /fake/binary
2196 cwd: /fake/cwd
2197 build platform: target
2198 benches::bench_foo
2199 benches::ignored_bench_foo (skipped)
2200 tests::baz::test_ignored (skipped)
2201 tests::baz::test_quux
2202 tests::foo::test_bar
2203 tests::ignored::test_bar (skipped)
2204 fake-package::skipped-binary:
2205 bin: /fake/skipped-binary
2206 cwd: /fake/cwd
2207 build platform: host
2208 (test binary didn't match filtersets, skipped)
2209 "};
2210 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
2211 {
2212 "rust-build-meta": {
2213 "target-directory": "/fake",
2214 "build-directory": "/fake",
2215 "base-output-directories": [],
2216 "non-test-binaries": {},
2217 "build-script-out-dirs": {},
2218 "build-script-info": {},
2219 "linked-paths": [],
2220 "platforms": {
2221 "host": {
2222 "platform": {
2223 "triple": "x86_64-unknown-linux-gnu",
2224 "target-features": "unknown"
2225 },
2226 "libdir": {
2227 "status": "available",
2228 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
2229 }
2230 },
2231 "targets": [
2232 {
2233 "platform": {
2234 "triple": "aarch64-unknown-linux-gnu",
2235 "target-features": "unknown"
2236 },
2237 "libdir": {
2238 "status": "unavailable",
2239 "reason": "test"
2240 }
2241 }
2242 ]
2243 },
2244 "target-platforms": [
2245 {
2246 "triple": "aarch64-unknown-linux-gnu",
2247 "target-features": "unknown"
2248 }
2249 ],
2250 "target-platform": "aarch64-unknown-linux-gnu"
2251 },
2252 "test-count": 6,
2253 "rust-suites": {
2254 "fake-package::fake-binary": {
2255 "package-name": "metadata-helper",
2256 "binary-id": "fake-package::fake-binary",
2257 "binary-name": "fake-binary",
2258 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
2259 "kind": "lib",
2260 "binary-path": "/fake/binary",
2261 "build-platform": "target",
2262 "cwd": "/fake/cwd",
2263 "status": "listed",
2264 "testcases": {
2265 "benches::bench_foo": {
2266 "kind": "bench",
2267 "ignored": false,
2268 "filter-match": {
2269 "status": "matches"
2270 }
2271 },
2272 "benches::ignored_bench_foo": {
2273 "kind": "bench",
2274 "ignored": true,
2275 "filter-match": {
2276 "status": "mismatch",
2277 "reason": "ignored"
2278 }
2279 },
2280 "tests::baz::test_ignored": {
2281 "kind": "test",
2282 "ignored": true,
2283 "filter-match": {
2284 "status": "mismatch",
2285 "reason": "ignored"
2286 }
2287 },
2288 "tests::baz::test_quux": {
2289 "kind": "test",
2290 "ignored": false,
2291 "filter-match": {
2292 "status": "matches"
2293 }
2294 },
2295 "tests::foo::test_bar": {
2296 "kind": "test",
2297 "ignored": false,
2298 "filter-match": {
2299 "status": "matches"
2300 }
2301 },
2302 "tests::ignored::test_bar": {
2303 "kind": "test",
2304 "ignored": true,
2305 "filter-match": {
2306 "status": "mismatch",
2307 "reason": "ignored"
2308 }
2309 }
2310 }
2311 },
2312 "fake-package::skipped-binary": {
2313 "package-name": "metadata-helper",
2314 "binary-id": "fake-package::skipped-binary",
2315 "binary-name": "skipped-binary",
2316 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
2317 "kind": "proc-macro",
2318 "binary-path": "/fake/skipped-binary",
2319 "build-platform": "host",
2320 "cwd": "/fake/cwd",
2321 "status": "skipped",
2322 "testcases": {}
2323 }
2324 }
2325 }"#};
2326 static EXPECTED_ONELINE: &str = indoc! {"
2327 fake-package::fake-binary benches::bench_foo
2328 fake-package::fake-binary tests::baz::test_quux
2329 fake-package::fake-binary tests::foo::test_bar
2330 "};
2331 static EXPECTED_ONELINE_VERBOSE: &str = indoc! {"
2332 fake-package::fake-binary benches::bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2333 fake-package::fake-binary benches::ignored_bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2334 fake-package::fake-binary tests::baz::test_ignored [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2335 fake-package::fake-binary tests::baz::test_quux [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2336 fake-package::fake-binary tests::foo::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2337 fake-package::fake-binary tests::ignored::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2338 "};
2339
2340 assert_eq!(
2341 test_list
2342 .to_string(OutputFormat::Human { verbose: false })
2343 .expect("human succeeded"),
2344 EXPECTED_HUMAN
2345 );
2346 assert_eq!(
2347 test_list
2348 .to_string(OutputFormat::Human { verbose: true })
2349 .expect("human succeeded"),
2350 EXPECTED_HUMAN_VERBOSE
2351 );
2352 println!(
2353 "{}",
2354 test_list
2355 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
2356 .expect("json-pretty succeeded")
2357 );
2358 assert_eq!(
2359 test_list
2360 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
2361 .expect("json-pretty succeeded"),
2362 EXPECTED_JSON_PRETTY
2363 );
2364 assert_eq!(
2365 test_list
2366 .to_string(OutputFormat::Oneline { verbose: false })
2367 .expect("oneline succeeded"),
2368 EXPECTED_ONELINE
2369 );
2370 assert_eq!(
2371 test_list
2372 .to_string(OutputFormat::Oneline { verbose: true })
2373 .expect("oneline verbose succeeded"),
2374 EXPECTED_ONELINE_VERBOSE
2375 );
2376 }
2377
2378 #[test]
2382 fn test_ignored_overrides_non_ignored() {
2383 let non_ignored_output = indoc! {"
2386 tests::unique_non_ignored: test
2387 tests::overlap_test: test
2388 "};
2389 let ignored_output = indoc! {"
2390 tests::unique_ignored: test
2391 tests::overlap_test: test
2392 "};
2393
2394 let test_filter = TestFilter::new(
2395 NextestRunMode::Test,
2396 RunIgnored::All,
2397 TestFilterPatterns::default(),
2398 Vec::new(),
2399 )
2400 .unwrap();
2401 let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
2402 let fake_binary_id = RustBinaryId::new("fake-package::overlap-binary");
2403
2404 let test_binary = RustTestArtifact {
2405 binary_path: "/fake/binary".into(),
2406 cwd: fake_cwd.clone(),
2407 package: package_metadata(),
2408 binary_name: "overlap-binary".to_owned(),
2409 binary_id: fake_binary_id.clone(),
2410 kind: RustTestBinaryKind::LIB,
2411 non_test_binaries: BTreeSet::new(),
2412 build_platform: BuildPlatform::Target,
2413 };
2414
2415 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
2416 let build_platforms = BuildPlatforms {
2417 host: HostPlatform {
2418 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
2419 libdir: PlatformLibdir::Available(fake_host_libdir.into()),
2420 },
2421 target: None,
2422 };
2423
2424 let fake_env = EnvironmentMap::empty();
2425 let rust_build_meta =
2426 RustBuildMeta::new("/fake", "/fake", build_platforms).map_paths(&PathMapper::noop());
2427 let ecx = EvalContext {
2428 default_filter: &CompiledExpr::ALL,
2429 };
2430 let test_list = TestList::new_with_outputs(
2431 [(test_binary, &non_ignored_output, &ignored_output)],
2432 Utf8PathBuf::from("/fake/path"),
2433 rust_build_meta,
2434 &test_filter,
2435 None,
2436 fake_env,
2437 &ecx,
2438 FilterBound::All,
2439 )
2440 .expect("valid output");
2441
2442 let suite = test_list
2444 .rust_suites
2445 .get(&fake_binary_id)
2446 .expect("suite exists");
2447 match &suite.status {
2448 RustTestSuiteStatus::Listed { test_cases } => {
2449 let overlap = test_cases
2450 .get(&TestCaseName::new("tests::overlap_test"))
2451 .expect("overlap_test exists");
2452 assert!(
2453 overlap.test_info.ignored,
2454 "overlapping test should be marked ignored"
2455 );
2456 }
2457 other => panic!("expected Listed status, got {other:?}"),
2458 }
2459 }
2460
2461 #[test]
2462 fn apply_wrappers_examples() {
2463 cfg_if::cfg_if! {
2464 if #[cfg(windows)]
2465 {
2466 let workspace_root = Utf8Path::new("D:\\workspace\\root");
2467 let target_dir = Utf8Path::new("C:\\foo\\bar");
2468 } else {
2469 let workspace_root = Utf8Path::new("/workspace/root");
2470 let target_dir = Utf8Path::new("/foo/bar");
2471 }
2472 };
2473
2474 {
2476 let mut cli_no_wrappers = TestCommandCli::default();
2477 cli_no_wrappers.apply_wrappers(None, None, workspace_root, target_dir);
2478 cli_no_wrappers.extend(["binary", "arg"]);
2479 assert!(cli_no_wrappers.env.is_none());
2480 assert_eq!(cli_no_wrappers.to_owned_cli(), vec!["binary", "arg"]);
2481 }
2482
2483 {
2485 let runner = PlatformRunner::debug_new(
2486 "runner".into(),
2487 Vec::new(),
2488 PlatformRunnerSource::Env("fake".to_owned()),
2489 );
2490 let mut cli_runner_only = TestCommandCli::default();
2491 cli_runner_only.apply_wrappers(None, Some(&runner), workspace_root, target_dir);
2492 cli_runner_only.extend(["binary", "arg"]);
2493 assert!(cli_runner_only.env.is_none());
2494 assert_eq!(
2495 cli_runner_only.to_owned_cli(),
2496 vec!["runner", "binary", "arg"],
2497 );
2498 }
2499
2500 {
2502 let runner = PlatformRunner::debug_new(
2503 "runner".into(),
2504 Vec::new(),
2505 PlatformRunnerSource::Env("fake".to_owned()),
2506 );
2507 let wrapper_ignore = WrapperScriptConfig {
2508 command: ScriptCommand {
2509 program: "wrapper".into(),
2510 args: Vec::new(),
2511 env: ScriptCommandEnvMap::default(),
2512 relative_to: ScriptCommandRelativeTo::None,
2513 },
2514 target_runner: WrapperScriptTargetRunner::Ignore,
2515 };
2516 let mut cli_wrapper_ignore = TestCommandCli::default();
2517 cli_wrapper_ignore.apply_wrappers(
2518 Some(&wrapper_ignore),
2519 Some(&runner),
2520 workspace_root,
2521 target_dir,
2522 );
2523 cli_wrapper_ignore.extend(["binary", "arg"]);
2524 assert_eq!(
2525 cli_wrapper_ignore.env,
2526 Some(&ScriptCommandEnvMap::default())
2527 );
2528 assert_eq!(
2529 cli_wrapper_ignore.to_owned_cli(),
2530 vec!["wrapper", "binary", "arg"],
2531 );
2532 }
2533
2534 {
2536 let runner = PlatformRunner::debug_new(
2537 "runner".into(),
2538 Vec::new(),
2539 PlatformRunnerSource::Env("fake".to_owned()),
2540 );
2541 let env = ScriptCommandEnvMap::new(BTreeMap::from([(
2542 String::from("MSG"),
2543 String::from("hello world"),
2544 )]))
2545 .expect("valid env var keys");
2546 let wrapper_around = WrapperScriptConfig {
2547 command: ScriptCommand {
2548 program: "wrapper".into(),
2549 args: Vec::new(),
2550 env: env.clone(),
2551 relative_to: ScriptCommandRelativeTo::None,
2552 },
2553 target_runner: WrapperScriptTargetRunner::AroundWrapper,
2554 };
2555 let mut cli_wrapper_around = TestCommandCli::default();
2556 cli_wrapper_around.apply_wrappers(
2557 Some(&wrapper_around),
2558 Some(&runner),
2559 workspace_root,
2560 target_dir,
2561 );
2562 cli_wrapper_around.extend(["binary", "arg"]);
2563 assert_eq!(cli_wrapper_around.env, Some(&env));
2564 assert_eq!(
2565 cli_wrapper_around.to_owned_cli(),
2566 vec!["runner", "wrapper", "binary", "arg"],
2567 );
2568 }
2569
2570 {
2572 let runner = PlatformRunner::debug_new(
2573 "runner".into(),
2574 Vec::new(),
2575 PlatformRunnerSource::Env("fake".to_owned()),
2576 );
2577 let wrapper_within = WrapperScriptConfig {
2578 command: ScriptCommand {
2579 program: "wrapper".into(),
2580 args: Vec::new(),
2581 env: ScriptCommandEnvMap::default(),
2582 relative_to: ScriptCommandRelativeTo::None,
2583 },
2584 target_runner: WrapperScriptTargetRunner::WithinWrapper,
2585 };
2586 let mut cli_wrapper_within = TestCommandCli::default();
2587 cli_wrapper_within.apply_wrappers(
2588 Some(&wrapper_within),
2589 Some(&runner),
2590 workspace_root,
2591 target_dir,
2592 );
2593 cli_wrapper_within.extend(["binary", "arg"]);
2594 assert_eq!(
2595 cli_wrapper_within.env,
2596 Some(&ScriptCommandEnvMap::default())
2597 );
2598 assert_eq!(
2599 cli_wrapper_within.to_owned_cli(),
2600 vec!["wrapper", "runner", "binary", "arg"],
2601 );
2602 }
2603
2604 {
2607 let runner = PlatformRunner::debug_new(
2608 "runner".into(),
2609 Vec::new(),
2610 PlatformRunnerSource::Env("fake".to_owned()),
2611 );
2612 let wrapper_overrides = WrapperScriptConfig {
2613 command: ScriptCommand {
2614 program: "wrapper".into(),
2615 args: Vec::new(),
2616 env: ScriptCommandEnvMap::default(),
2617 relative_to: ScriptCommandRelativeTo::None,
2618 },
2619 target_runner: WrapperScriptTargetRunner::OverridesWrapper,
2620 };
2621 let mut cli_wrapper_overrides = TestCommandCli::default();
2622 cli_wrapper_overrides.apply_wrappers(
2623 Some(&wrapper_overrides),
2624 Some(&runner),
2625 workspace_root,
2626 target_dir,
2627 );
2628 cli_wrapper_overrides.extend(["binary", "arg"]);
2629 assert!(
2630 cli_wrapper_overrides.env.is_none(),
2631 "overrides-wrapper with runner should not apply wrapper env"
2632 );
2633 assert_eq!(
2634 cli_wrapper_overrides.to_owned_cli(),
2635 vec!["runner", "binary", "arg"],
2636 );
2637 }
2638
2639 {
2642 let wrapper_overrides = WrapperScriptConfig {
2643 command: ScriptCommand {
2644 program: "wrapper".into(),
2645 args: Vec::new(),
2646 env: ScriptCommandEnvMap::default(),
2647 relative_to: ScriptCommandRelativeTo::None,
2648 },
2649 target_runner: WrapperScriptTargetRunner::OverridesWrapper,
2650 };
2651 let mut cli_wrapper_overrides_no_runner = TestCommandCli::default();
2652 cli_wrapper_overrides_no_runner.apply_wrappers(
2653 Some(&wrapper_overrides),
2654 None,
2655 workspace_root,
2656 target_dir,
2657 );
2658 cli_wrapper_overrides_no_runner.extend(["binary", "arg"]);
2659 assert_eq!(
2660 cli_wrapper_overrides_no_runner.env,
2661 Some(&ScriptCommandEnvMap::default()),
2662 "overrides-wrapper without runner should apply wrapper env"
2663 );
2664 assert_eq!(
2665 cli_wrapper_overrides_no_runner.to_owned_cli(),
2666 vec!["wrapper", "binary", "arg"],
2667 );
2668 }
2669
2670 {
2672 let wrapper_with_args = WrapperScriptConfig {
2673 command: ScriptCommand {
2674 program: "wrapper".into(),
2675 args: vec!["--flag".to_string(), "value".to_string()],
2676 env: ScriptCommandEnvMap::default(),
2677 relative_to: ScriptCommandRelativeTo::None,
2678 },
2679 target_runner: WrapperScriptTargetRunner::Ignore,
2680 };
2681 let mut cli_wrapper_args = TestCommandCli::default();
2682 cli_wrapper_args.apply_wrappers(
2683 Some(&wrapper_with_args),
2684 None,
2685 workspace_root,
2686 target_dir,
2687 );
2688 cli_wrapper_args.extend(["binary", "arg"]);
2689 assert_eq!(cli_wrapper_args.env, Some(&ScriptCommandEnvMap::default()));
2690 assert_eq!(
2691 cli_wrapper_args.to_owned_cli(),
2692 vec!["wrapper", "--flag", "value", "binary", "arg"],
2693 );
2694 }
2695
2696 {
2698 let runner_with_args = PlatformRunner::debug_new(
2699 "runner".into(),
2700 vec!["--runner-flag".into(), "value".into()],
2701 PlatformRunnerSource::Env("fake".to_owned()),
2702 );
2703 let mut cli_runner_args = TestCommandCli::default();
2704 cli_runner_args.apply_wrappers(
2705 None,
2706 Some(&runner_with_args),
2707 workspace_root,
2708 target_dir,
2709 );
2710 cli_runner_args.extend(["binary", "arg"]);
2711 assert!(cli_runner_args.env.is_none());
2712 assert_eq!(
2713 cli_runner_args.to_owned_cli(),
2714 vec!["runner", "--runner-flag", "value", "binary", "arg"],
2715 );
2716 }
2717
2718 {
2720 let wrapper_relative_to_workspace_root = WrapperScriptConfig {
2721 command: ScriptCommand {
2722 program: "abc/def/my-wrapper".into(),
2723 args: vec!["--verbose".to_string()],
2724 env: ScriptCommandEnvMap::default(),
2725 relative_to: ScriptCommandRelativeTo::WorkspaceRoot,
2726 },
2727 target_runner: WrapperScriptTargetRunner::Ignore,
2728 };
2729 let mut cli_wrapper_relative = TestCommandCli::default();
2730 cli_wrapper_relative.apply_wrappers(
2731 Some(&wrapper_relative_to_workspace_root),
2732 None,
2733 workspace_root,
2734 target_dir,
2735 );
2736 cli_wrapper_relative.extend(["binary", "arg"]);
2737
2738 cfg_if::cfg_if! {
2739 if #[cfg(windows)] {
2740 let wrapper_path = "D:\\workspace\\root\\abc\\def\\my-wrapper";
2741 } else {
2742 let wrapper_path = "/workspace/root/abc/def/my-wrapper";
2743 }
2744 }
2745 assert_eq!(
2746 cli_wrapper_relative.env,
2747 Some(&ScriptCommandEnvMap::default())
2748 );
2749 assert_eq!(
2750 cli_wrapper_relative.to_owned_cli(),
2751 vec![wrapper_path, "--verbose", "binary", "arg"],
2752 );
2753 }
2754
2755 {
2757 let wrapper_relative_to_target = WrapperScriptConfig {
2758 command: ScriptCommand {
2759 program: "abc/def/my-wrapper".into(),
2760 args: vec!["--verbose".to_string()],
2761 env: ScriptCommandEnvMap::default(),
2762 relative_to: ScriptCommandRelativeTo::Target,
2763 },
2764 target_runner: WrapperScriptTargetRunner::Ignore,
2765 };
2766 let mut cli_wrapper_relative = TestCommandCli::default();
2767 cli_wrapper_relative.apply_wrappers(
2768 Some(&wrapper_relative_to_target),
2769 None,
2770 workspace_root,
2771 target_dir,
2772 );
2773 cli_wrapper_relative.extend(["binary", "arg"]);
2774 cfg_if::cfg_if! {
2775 if #[cfg(windows)] {
2776 let wrapper_path = "C:\\foo\\bar\\abc\\def\\my-wrapper";
2777 } else {
2778 let wrapper_path = "/foo/bar/abc/def/my-wrapper";
2779 }
2780 }
2781 assert_eq!(
2782 cli_wrapper_relative.env,
2783 Some(&ScriptCommandEnvMap::default())
2784 );
2785 assert_eq!(
2786 cli_wrapper_relative.to_owned_cli(),
2787 vec![wrapper_path, "--verbose", "binary", "arg"],
2788 );
2789 }
2790 }
2791
2792 #[test]
2793 fn test_parse_list_lines() {
2794 let binary_id = RustBinaryId::new("test-package::test-binary");
2795
2796 let input = indoc! {"
2798 simple_test: test
2799 module::nested_test: test
2800 deeply::nested::module::test_name: test
2801 "};
2802 let results: Vec<_> = parse_list_lines(&binary_id, input)
2803 .collect::<Result<_, _>>()
2804 .expect("parsed valid test output");
2805 insta::assert_debug_snapshot!("valid_tests", results);
2806
2807 let input = indoc! {"
2809 simple_bench: benchmark
2810 benches::module::my_benchmark: benchmark
2811 "};
2812 let results: Vec<_> = parse_list_lines(&binary_id, input)
2813 .collect::<Result<_, _>>()
2814 .expect("parsed valid benchmark output");
2815 insta::assert_debug_snapshot!("valid_benchmarks", results);
2816
2817 let input = indoc! {"
2819 test_one: test
2820 bench_one: benchmark
2821 test_two: test
2822 bench_two: benchmark
2823 "};
2824 let results: Vec<_> = parse_list_lines(&binary_id, input)
2825 .collect::<Result<_, _>>()
2826 .expect("parsed mixed output");
2827 insta::assert_debug_snapshot!("mixed_tests_and_benchmarks", results);
2828
2829 let input = indoc! {r#"
2831 test_with_underscore_123: test
2832 test::with::colons: test
2833 test_with_numbers_42: test
2834 "#};
2835 let results: Vec<_> = parse_list_lines(&binary_id, input)
2836 .collect::<Result<_, _>>()
2837 .expect("parsed tests with special characters");
2838 insta::assert_debug_snapshot!("special_characters", results);
2839
2840 let input = "";
2842 let results: Vec<_> = parse_list_lines(&binary_id, input)
2843 .collect::<Result<_, _>>()
2844 .expect("parsed empty output");
2845 insta::assert_debug_snapshot!("empty_input", results);
2846
2847 let input = "invalid_test: wrong_suffix";
2849 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2850 assert!(result.is_err());
2851 insta::assert_snapshot!("invalid_suffix_error", result.unwrap_err());
2852
2853 let input = "test_without_suffix";
2855 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2856 assert!(result.is_err());
2857 insta::assert_snapshot!("missing_suffix_error", result.unwrap_err());
2858
2859 let input = indoc! {"
2861 valid_test: test
2862 invalid_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!("partial_valid_error", result.unwrap_err());
2868
2869 let input = indoc! {"
2871 valid_test: test
2872 \rinvalid_line
2873 another_valid: benchmark
2874 "};
2875 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2876 assert!(result.is_err());
2877 insta::assert_snapshot!("control_character_error", result.unwrap_err());
2878 }
2879
2880 #[proptest]
2883 fn test_instance_id_key_borrow_consistency(
2884 owned1: OwnedTestInstanceId,
2885 owned2: OwnedTestInstanceId,
2886 ) {
2887 let borrowed1: &dyn TestInstanceIdKey = &owned1;
2889 let borrowed2: &dyn TestInstanceIdKey = &owned2;
2890
2891 assert_eq!(
2893 owned1 == owned2,
2894 borrowed1 == borrowed2,
2895 "Eq must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2896 );
2897
2898 assert_eq!(
2900 owned1.partial_cmp(&owned2),
2901 borrowed1.partial_cmp(borrowed2),
2902 "PartialOrd must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2903 );
2904
2905 assert_eq!(
2907 owned1.cmp(&owned2),
2908 borrowed1.cmp(borrowed2),
2909 "Ord must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2910 );
2911
2912 fn hash_value(x: &impl Hash) -> u64 {
2914 let mut hasher = DefaultHasher::new();
2915 x.hash(&mut hasher);
2916 hasher.finish()
2917 }
2918
2919 assert_eq!(
2920 hash_value(&owned1),
2921 hash_value(&borrowed1),
2922 "Hash must be consistent for owned1 and its borrowed form"
2923 );
2924 assert_eq!(
2925 hash_value(&owned2),
2926 hash_value(&borrowed2),
2927 "Hash must be consistent for owned2 and its borrowed form"
2928 );
2929 }
2930
2931 #[derive(Debug)]
2934 struct MockGroupLookup {
2935 group_name: String,
2936 }
2937
2938 impl GroupLookup for MockGroupLookup {
2939 fn is_member_test(
2940 &self,
2941 _test: &nextest_filtering::TestQuery<'_>,
2942 matcher: &nextest_filtering::NameMatcher,
2943 ) -> bool {
2944 matcher.is_match(&self.group_name)
2945 }
2946 }
2947
2948 #[test]
2951 fn test_build_suites_with_group_filter() {
2952 let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
2953
2954 let test_filter = TestFilter::new(
2957 NextestRunMode::Test,
2958 RunIgnored::Default,
2959 TestFilterPatterns::default(),
2960 vec![
2961 Filterset::parse(
2962 "group(serial)".to_owned(),
2963 &cx,
2964 FiltersetKind::Test,
2965 &KnownGroups::Known {
2966 custom_groups: HashSet::from(["serial".to_owned()]),
2967 },
2968 )
2969 .unwrap(),
2970 ],
2971 )
2972 .unwrap();
2973
2974 assert!(
2975 test_filter.has_group_predicates(),
2976 "filter with group() must report has_group_predicates"
2977 );
2978
2979 let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
2980
2981 let make_parsed = || {
2982 vec![ParsedTestBinary::Listed {
2983 artifact: RustTestArtifact {
2984 binary_path: "/fake/binary".into(),
2985 cwd: "/fake/cwd".into(),
2986 package: package_metadata(),
2987 binary_name: "fake-binary".to_owned(),
2988 binary_id: fake_binary_id.clone(),
2989 kind: RustTestBinaryKind::LIB,
2990 non_test_binaries: BTreeSet::new(),
2991 build_platform: BuildPlatform::Target,
2992 },
2993 test_cases: vec![
2994 ParsedTestCase {
2995 name: TestCaseName::new("serial_test"),
2996 kind: RustTestKind::TEST,
2997 ignored: false,
2998 },
2999 ParsedTestCase {
3000 name: TestCaseName::new("parallel_test"),
3001 kind: RustTestKind::TEST,
3002 ignored: false,
3003 },
3004 ],
3005 }]
3006 };
3007
3008 let ecx = EvalContext {
3009 default_filter: &CompiledExpr::ALL,
3010 };
3011
3012 let lookup = MockGroupLookup {
3014 group_name: "serial".to_owned(),
3015 };
3016 let suites = TestList::build_suites(
3017 make_parsed(),
3018 &test_filter,
3019 &ecx,
3020 FilterBound::All,
3021 Some(&lookup),
3022 );
3023 let suite = suites.get(&fake_binary_id).expect("suite exists");
3024 for case in suite.status.test_cases() {
3026 assert_eq!(
3027 case.test_info.filter_match,
3028 FilterMatch::Matches,
3029 "{} should match with serial group lookup",
3030 case.name,
3031 );
3032 }
3033
3034 let lookup_other = MockGroupLookup {
3036 group_name: "batch".to_owned(),
3037 };
3038 let suites = TestList::build_suites(
3039 make_parsed(),
3040 &test_filter,
3041 &ecx,
3042 FilterBound::All,
3043 Some(&lookup_other),
3044 );
3045 let suite = suites.get(&fake_binary_id).expect("suite exists");
3046 for case in suite.status.test_cases() {
3048 assert_eq!(
3049 case.test_info.filter_match,
3050 FilterMatch::Mismatch {
3051 reason: MismatchReason::Expression,
3052 },
3053 "{} should not match with batch group lookup",
3054 case.name,
3055 );
3056 }
3057 }
3058}