1use super::{DisplayFilterMatcher, TestListDisplayFilter};
5use crate::{
6 cargo_config::EnvironmentMap,
7 config::{
8 core::EvaluatableProfile,
9 overrides::{ListSettings, TestSettings},
10 scripts::{ScriptCommandEnvMap, WrapperScriptConfig, WrapperScriptTargetRunner},
11 },
12 double_spawn::DoubleSpawnInfo,
13 errors::{
14 CreateTestListError, FromMessagesError, TestListFromSummaryError, WriteTestListError,
15 },
16 helpers::{convert_build_platform, dylib_path, dylib_path_envvar, write_test_name},
17 indenter::indented,
18 list::{BinaryList, OutputFormat, RustBuildMeta, Styles, TestListState},
19 partition::{Partitioner, PartitionerBuilder, PartitionerScope},
20 reuse_build::PathMapper,
21 run_mode::NextestRunMode,
22 runner::Interceptor,
23 target_runner::{PlatformRunner, TargetRunner},
24 test_command::{LocalExecuteContext, TestCommand, TestCommandPhase},
25 test_filter::{BinaryMismatchReason, FilterBinaryMatch, FilterBound, TestFilter},
26 write_str::WriteStr,
27};
28use camino::{Utf8Path, Utf8PathBuf};
29use debug_ignore::DebugIgnore;
30use futures::prelude::*;
31use guppy::{
32 PackageId,
33 graph::{PackageGraph, PackageMetadata},
34};
35use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
36use nextest_filtering::{BinaryQuery, EvalContext, TestQuery};
37use nextest_metadata::{
38 BuildPlatform, FilterMatch, MismatchReason, RustBinaryId, RustNonTestBinaryKind,
39 RustTestBinaryKind, RustTestBinarySummary, RustTestCaseSummary, RustTestKind,
40 RustTestSuiteStatusSummary, RustTestSuiteSummary, TestCaseName, TestListSummary,
41};
42use owo_colors::OwoColorize;
43use quick_junit::ReportUuid;
44use serde::{Deserialize, Serialize};
45use std::{
46 borrow::{Borrow, Cow},
47 collections::{BTreeMap, BTreeSet},
48 ffi::{OsStr, OsString},
49 fmt,
50 hash::{Hash, Hasher},
51 io,
52 path::PathBuf,
53 sync::{Arc, OnceLock},
54};
55use swrite::{SWrite, swrite};
56use tokio::runtime::Runtime;
57use tracing::debug;
58
59#[derive(Clone, Debug)]
64pub struct RustTestArtifact<'g> {
65 pub binary_id: RustBinaryId,
67
68 pub package: PackageMetadata<'g>,
71
72 pub binary_path: Utf8PathBuf,
74
75 pub binary_name: String,
77
78 pub kind: RustTestBinaryKind,
80
81 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
83
84 pub cwd: Utf8PathBuf,
86
87 pub build_platform: BuildPlatform,
89}
90
91impl<'g> RustTestArtifact<'g> {
92 pub fn from_binary_list(
94 graph: &'g PackageGraph,
95 binary_list: Arc<BinaryList>,
96 rust_build_meta: &RustBuildMeta<TestListState>,
97 path_mapper: &PathMapper,
98 platform_filter: Option<BuildPlatform>,
99 ) -> Result<Vec<Self>, FromMessagesError> {
100 let mut binaries = vec![];
101
102 for binary in &binary_list.rust_binaries {
103 if platform_filter.is_some() && platform_filter != Some(binary.build_platform) {
104 continue;
105 }
106
107 let package_id = PackageId::new(binary.package_id.clone());
109 let package = graph
110 .metadata(&package_id)
111 .map_err(FromMessagesError::PackageGraph)?;
112
113 let cwd = package
115 .manifest_path()
116 .parent()
117 .unwrap_or_else(|| {
118 panic!(
119 "manifest path {} doesn't have a parent",
120 package.manifest_path()
121 )
122 })
123 .to_path_buf();
124
125 let binary_path = path_mapper.map_build_path(binary.path.clone());
127 let cwd = path_mapper.map_cwd(cwd);
128
129 let non_test_binaries = if binary.kind == RustTestBinaryKind::TEST
131 || binary.kind == RustTestBinaryKind::BENCH
132 {
133 match rust_build_meta.non_test_binaries.get(package_id.repr()) {
136 Some(binaries) => binaries
137 .iter()
138 .filter(|binary| {
139 binary.kind == RustNonTestBinaryKind::BIN_EXE
141 })
142 .map(|binary| {
143 let abs_path = rust_build_meta.target_directory.join(&binary.path);
145 (binary.name.clone(), abs_path)
146 })
147 .collect(),
148 None => BTreeSet::new(),
149 }
150 } else {
151 BTreeSet::new()
152 };
153
154 binaries.push(RustTestArtifact {
155 binary_id: binary.id.clone(),
156 package,
157 binary_path,
158 binary_name: binary.name.clone(),
159 kind: binary.kind.clone(),
160 cwd,
161 non_test_binaries,
162 build_platform: binary.build_platform,
163 })
164 }
165
166 Ok(binaries)
167 }
168
169 pub fn to_binary_query(&self) -> BinaryQuery<'_> {
171 BinaryQuery {
172 package_id: self.package.id(),
173 binary_id: &self.binary_id,
174 kind: &self.kind,
175 binary_name: &self.binary_name,
176 platform: convert_build_platform(self.build_platform),
177 }
178 }
179
180 fn into_test_suite(self, status: RustTestSuiteStatus) -> RustTestSuite<'g> {
184 let Self {
185 binary_id,
186 package,
187 binary_path,
188 binary_name,
189 kind,
190 non_test_binaries,
191 cwd,
192 build_platform,
193 } = self;
194
195 RustTestSuite {
196 binary_id,
197 binary_path,
198 package,
199 binary_name,
200 kind,
201 non_test_binaries,
202 cwd,
203 build_platform,
204 status,
205 }
206 }
207}
208
209#[derive(Clone, Debug, Eq, PartialEq)]
211pub struct SkipCounts {
212 pub skipped_tests: usize,
214
215 pub skipped_tests_rerun: usize,
218
219 pub skipped_tests_non_benchmark: usize,
223
224 pub skipped_tests_default_filter: usize,
226
227 pub skipped_binaries: usize,
229
230 pub skipped_binaries_default_filter: usize,
232}
233
234#[derive(Clone, Debug)]
236pub struct TestList<'g> {
237 test_count: usize,
238 mode: NextestRunMode,
239 rust_build_meta: RustBuildMeta<TestListState>,
240 rust_suites: IdOrdMap<RustTestSuite<'g>>,
241 workspace_root: Utf8PathBuf,
242 env: EnvironmentMap,
243 updated_dylib_path: OsString,
244 skip_counts: OnceLock<SkipCounts>,
246}
247
248impl<'g> TestList<'g> {
249 #[expect(clippy::too_many_arguments)]
251 pub fn new<I>(
252 ctx: &TestExecuteContext<'_>,
253 test_artifacts: I,
254 rust_build_meta: RustBuildMeta<TestListState>,
255 filter: &TestFilter,
256 partitioner_builder: Option<&PartitionerBuilder>,
257 workspace_root: Utf8PathBuf,
258 env: EnvironmentMap,
259 profile: &impl ListProfile,
260 bound: FilterBound,
261 list_threads: usize,
262 ) -> Result<Self, CreateTestListError>
263 where
264 I: IntoIterator<Item = RustTestArtifact<'g>>,
265 I::IntoIter: Send,
266 {
267 let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
268 debug!(
269 "updated {}: {}",
270 dylib_path_envvar(),
271 updated_dylib_path.to_string_lossy(),
272 );
273 let lctx = LocalExecuteContext {
274 phase: TestCommandPhase::List,
275 workspace_root: &workspace_root,
278 rust_build_meta: &rust_build_meta,
279 double_spawn: ctx.double_spawn,
280 dylib_path: &updated_dylib_path,
281 profile_name: ctx.profile_name,
282 env: &env,
283 };
284
285 let ecx = profile.filterset_ecx();
286
287 let runtime = Runtime::new().map_err(CreateTestListError::TokioRuntimeCreate)?;
288
289 let stream = futures::stream::iter(test_artifacts).map(|test_binary| {
290 async {
291 let binary_query = test_binary.to_binary_query();
292 let binary_match = filter.filter_binary_match(&test_binary, &ecx, bound);
293 match binary_match {
294 FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
295 debug!(
296 "executing test binary to obtain test list \
297 (match result is {binary_match:?}): {}",
298 test_binary.binary_id,
299 );
300 let list_settings = profile.list_settings_for(&binary_query);
302 let (non_ignored, ignored) = test_binary
303 .exec(&lctx, &list_settings, ctx.target_runner)
304 .await?;
305 let info = Self::process_output(
306 test_binary,
307 filter,
308 &ecx,
309 bound,
310 non_ignored.as_str(),
311 ignored.as_str(),
312 )?;
313 Ok::<_, CreateTestListError>(info)
314 }
315 FilterBinaryMatch::Mismatch { reason } => {
316 debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
317 Ok(Self::process_skipped(test_binary, reason))
318 }
319 }
320 }
321 });
322 let fut = stream.buffer_unordered(list_threads).try_collect();
323
324 let mut rust_suites: IdOrdMap<_> = runtime.block_on(fut)?;
325
326 runtime.shutdown_background();
329
330 Self::apply_partitioning(&mut rust_suites, partitioner_builder);
331
332 let test_count = rust_suites
333 .iter()
334 .map(|suite| suite.status.test_count())
335 .sum();
336
337 Ok(Self {
338 rust_suites,
339 mode: filter.mode(),
340 workspace_root,
341 env,
342 rust_build_meta,
343 updated_dylib_path,
344 test_count,
345 skip_counts: OnceLock::new(),
346 })
347 }
348
349 #[cfg(test)]
351 #[expect(clippy::too_many_arguments)]
352 pub(crate) fn new_with_outputs(
353 test_bin_outputs: impl IntoIterator<
354 Item = (RustTestArtifact<'g>, impl AsRef<str>, impl AsRef<str>),
355 >,
356 workspace_root: Utf8PathBuf,
357 rust_build_meta: RustBuildMeta<TestListState>,
358 filter: &TestFilter,
359 partitioner_builder: Option<&PartitionerBuilder>,
360 env: EnvironmentMap,
361 ecx: &EvalContext<'_>,
362 bound: FilterBound,
363 ) -> Result<Self, CreateTestListError> {
364 let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
365
366 let mut rust_suites = test_bin_outputs
367 .into_iter()
368 .map(|(test_binary, non_ignored, ignored)| {
369 let binary_match = filter.filter_binary_match(&test_binary, ecx, bound);
370 match binary_match {
371 FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
372 debug!(
373 "processing output for binary \
374 (match result is {binary_match:?}): {}",
375 test_binary.binary_id,
376 );
377 let info = Self::process_output(
378 test_binary,
379 filter,
380 ecx,
381 bound,
382 non_ignored.as_ref(),
383 ignored.as_ref(),
384 )?;
385 Ok(info)
386 }
387 FilterBinaryMatch::Mismatch { reason } => {
388 debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
389 Ok(Self::process_skipped(test_binary, reason))
390 }
391 }
392 })
393 .collect::<Result<IdOrdMap<_>, _>>()?;
394
395 Self::apply_partitioning(&mut rust_suites, partitioner_builder);
396
397 let test_count = rust_suites
398 .iter()
399 .map(|suite| suite.status.test_count())
400 .sum();
401
402 Ok(Self {
403 rust_suites,
404 mode: filter.mode(),
405 workspace_root,
406 env,
407 rust_build_meta,
408 updated_dylib_path,
409 test_count,
410 skip_counts: OnceLock::new(),
411 })
412 }
413
414 pub fn from_summary(
420 graph: &'g PackageGraph,
421 summary: &TestListSummary,
422 mode: NextestRunMode,
423 ) -> Result<Self, TestListFromSummaryError> {
424 let rust_build_meta = RustBuildMeta::from_summary(summary.rust_build_meta.clone())
426 .map_err(TestListFromSummaryError::RustBuildMeta)?;
427
428 let workspace_root = graph.workspace().root().to_path_buf();
430
431 let env = EnvironmentMap::empty();
433
434 let updated_dylib_path = OsString::new();
436
437 let mut rust_suites = IdOrdMap::new();
439 let mut test_count = 0;
440
441 for (binary_id, suite_summary) in &summary.rust_suites {
442 let package_id = PackageId::new(suite_summary.binary.package_id.clone());
444 let package = graph.metadata(&package_id).map_err(|_| {
445 TestListFromSummaryError::PackageNotFound {
446 name: suite_summary.package_name.clone(),
447 package_id: suite_summary.binary.package_id.clone(),
448 }
449 })?;
450
451 let status = if suite_summary.status == RustTestSuiteStatusSummary::SKIPPED {
453 RustTestSuiteStatus::Skipped {
454 reason: BinaryMismatchReason::Expression,
455 }
456 } else if suite_summary.status == RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER {
457 RustTestSuiteStatus::Skipped {
458 reason: BinaryMismatchReason::DefaultSet,
459 }
460 } else {
461 let test_cases: IdOrdMap<RustTestCase> = suite_summary
463 .test_cases
464 .iter()
465 .map(|(name, info)| RustTestCase {
466 name: name.clone(),
467 test_info: info.clone(),
468 })
469 .collect();
470
471 test_count += test_cases.len();
472
473 RustTestSuiteStatus::Listed {
475 test_cases: DebugIgnore(test_cases),
476 }
477 };
478
479 let suite = RustTestSuite {
480 binary_id: binary_id.clone(),
481 binary_path: suite_summary.binary.binary_path.clone(),
482 package,
483 binary_name: suite_summary.binary.binary_name.clone(),
484 kind: suite_summary.binary.kind.clone(),
485 non_test_binaries: BTreeSet::new(), cwd: suite_summary.cwd.clone(),
487 build_platform: suite_summary.binary.build_platform,
488 status,
489 };
490
491 let _ = rust_suites.insert_unique(suite);
492 }
493
494 Ok(Self {
495 rust_suites,
496 mode,
497 workspace_root,
498 env,
499 rust_build_meta,
500 updated_dylib_path,
501 test_count,
502 skip_counts: OnceLock::new(),
503 })
504 }
505
506 pub fn test_count(&self) -> usize {
508 self.test_count
509 }
510
511 pub fn mode(&self) -> NextestRunMode {
513 self.mode
514 }
515
516 pub fn rust_build_meta(&self) -> &RustBuildMeta<TestListState> {
518 &self.rust_build_meta
519 }
520
521 pub fn skip_counts(&self) -> &SkipCounts {
523 self.skip_counts.get_or_init(|| {
524 let mut skipped_tests_rerun = 0;
525 let mut skipped_tests_non_benchmark = 0;
526 let mut skipped_tests_default_filter = 0;
527 let skipped_tests = self
528 .iter_tests()
529 .filter(|instance| match instance.test_info.filter_match {
530 FilterMatch::Mismatch {
531 reason: MismatchReason::RerunAlreadyPassed,
532 } => {
533 skipped_tests_rerun += 1;
534 true
535 }
536 FilterMatch::Mismatch {
537 reason: MismatchReason::NotBenchmark,
538 } => {
539 skipped_tests_non_benchmark += 1;
540 true
541 }
542 FilterMatch::Mismatch {
543 reason: MismatchReason::DefaultFilter,
544 } => {
545 skipped_tests_default_filter += 1;
546 true
547 }
548 FilterMatch::Mismatch { .. } => true,
549 FilterMatch::Matches => false,
550 })
551 .count();
552
553 let mut skipped_binaries_default_filter = 0;
554 let skipped_binaries = self
555 .rust_suites
556 .iter()
557 .filter(|suite| match suite.status {
558 RustTestSuiteStatus::Skipped {
559 reason: BinaryMismatchReason::DefaultSet,
560 } => {
561 skipped_binaries_default_filter += 1;
562 true
563 }
564 RustTestSuiteStatus::Skipped { .. } => true,
565 RustTestSuiteStatus::Listed { .. } => false,
566 })
567 .count();
568
569 SkipCounts {
570 skipped_tests,
571 skipped_tests_rerun,
572 skipped_tests_non_benchmark,
573 skipped_tests_default_filter,
574 skipped_binaries,
575 skipped_binaries_default_filter,
576 }
577 })
578 }
579
580 pub fn run_count(&self) -> usize {
584 self.test_count - self.skip_counts().skipped_tests
585 }
586
587 pub fn binary_count(&self) -> usize {
589 self.rust_suites.len()
590 }
591
592 pub fn listed_binary_count(&self) -> usize {
594 self.binary_count() - self.skip_counts().skipped_binaries
595 }
596
597 pub fn workspace_root(&self) -> &Utf8Path {
599 &self.workspace_root
600 }
601
602 pub fn cargo_env(&self) -> &EnvironmentMap {
604 &self.env
605 }
606
607 pub fn updated_dylib_path(&self) -> &OsStr {
609 &self.updated_dylib_path
610 }
611
612 pub fn to_summary(&self) -> TestListSummary {
614 let rust_suites = self
615 .rust_suites
616 .iter()
617 .map(|test_suite| {
618 let (status, test_cases) = test_suite.status.to_summary();
619 let testsuite = RustTestSuiteSummary {
620 package_name: test_suite.package.name().to_owned(),
621 binary: RustTestBinarySummary {
622 binary_name: test_suite.binary_name.clone(),
623 package_id: test_suite.package.id().repr().to_owned(),
624 kind: test_suite.kind.clone(),
625 binary_path: test_suite.binary_path.clone(),
626 binary_id: test_suite.binary_id.clone(),
627 build_platform: test_suite.build_platform,
628 },
629 cwd: test_suite.cwd.clone(),
630 status,
631 test_cases,
632 };
633 (test_suite.binary_id.clone(), testsuite)
634 })
635 .collect();
636 let mut summary = TestListSummary::new(self.rust_build_meta.to_summary());
637 summary.test_count = self.test_count;
638 summary.rust_suites = rust_suites;
639 summary
640 }
641
642 pub fn write(
644 &self,
645 output_format: OutputFormat,
646 writer: &mut dyn WriteStr,
647 colorize: bool,
648 ) -> Result<(), WriteTestListError> {
649 match output_format {
650 OutputFormat::Human { verbose } => self
651 .write_human(writer, verbose, colorize)
652 .map_err(WriteTestListError::Io),
653 OutputFormat::Oneline { verbose } => self
654 .write_oneline(writer, verbose, colorize)
655 .map_err(WriteTestListError::Io),
656 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
657 }
658 }
659
660 pub fn iter(&self) -> impl Iterator<Item = &RustTestSuite<'_>> + '_ {
662 self.rust_suites.iter()
663 }
664
665 pub fn get_suite(&self, binary_id: &RustBinaryId) -> Option<&RustTestSuite<'_>> {
667 self.rust_suites.get(binary_id)
668 }
669
670 pub fn iter_tests(&self) -> impl Iterator<Item = TestInstance<'_>> + '_ {
672 self.rust_suites.iter().flat_map(|test_suite| {
673 test_suite
674 .status
675 .test_cases()
676 .map(move |case| TestInstance::new(case, test_suite))
677 })
678 }
679
680 pub fn to_priority_queue(
682 &'g self,
683 profile: &'g EvaluatableProfile<'g>,
684 ) -> TestPriorityQueue<'g> {
685 TestPriorityQueue::new(self, profile)
686 }
687
688 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
690 let mut s = String::with_capacity(1024);
691 self.write(output_format, &mut s, false)?;
692 Ok(s)
693 }
694
695 pub fn empty() -> Self {
703 Self {
704 test_count: 0,
705 mode: NextestRunMode::Test,
706 workspace_root: Utf8PathBuf::new(),
707 rust_build_meta: RustBuildMeta::empty(),
708 env: EnvironmentMap::empty(),
709 updated_dylib_path: OsString::new(),
710 rust_suites: IdOrdMap::new(),
711 skip_counts: OnceLock::new(),
712 }
713 }
714
715 pub(crate) fn create_dylib_path(
716 rust_build_meta: &RustBuildMeta<TestListState>,
717 ) -> Result<OsString, CreateTestListError> {
718 let dylib_path = dylib_path();
719 let dylib_path_is_empty = dylib_path.is_empty();
720 let new_paths = rust_build_meta.dylib_paths();
721
722 let mut updated_dylib_path: Vec<PathBuf> =
723 Vec::with_capacity(dylib_path.len() + new_paths.len());
724 updated_dylib_path.extend(
725 new_paths
726 .iter()
727 .map(|path| path.clone().into_std_path_buf()),
728 );
729 updated_dylib_path.extend(dylib_path);
730
731 if cfg!(target_os = "macos") && dylib_path_is_empty {
738 if let Some(home) = home::home_dir() {
739 updated_dylib_path.push(home.join("lib"));
740 }
741 updated_dylib_path.push("/usr/local/lib".into());
742 updated_dylib_path.push("/usr/lib".into());
743 }
744
745 std::env::join_paths(updated_dylib_path)
746 .map_err(move |error| CreateTestListError::dylib_join_paths(new_paths, error))
747 }
748
749 fn process_output(
750 test_binary: RustTestArtifact<'g>,
751 filter: &TestFilter,
752 ecx: &EvalContext<'_>,
753 bound: FilterBound,
754 non_ignored: impl AsRef<str>,
755 ignored: impl AsRef<str>,
756 ) -> Result<RustTestSuite<'g>, CreateTestListError> {
757 let mut test_cases = IdOrdMap::new();
758
759 for (test_name, kind) in Self::parse(&test_binary.binary_id, non_ignored.as_ref())? {
760 let name = TestCaseName::new(test_name);
761 let filter_match = filter.filter_match(&test_binary, &name, &kind, ecx, bound, false);
762 test_cases.insert_overwrite(RustTestCase {
763 name,
764 test_info: RustTestCaseSummary {
765 kind: Some(kind),
766 ignored: false,
767 filter_match,
768 },
769 });
770 }
771
772 for (test_name, kind) in Self::parse(&test_binary.binary_id, ignored.as_ref())? {
773 let name = TestCaseName::new(test_name);
778 let filter_match = filter.filter_match(&test_binary, &name, &kind, ecx, bound, true);
779 test_cases.insert_overwrite(RustTestCase {
780 name,
781 test_info: RustTestCaseSummary {
782 kind: Some(kind),
783 ignored: true,
784 filter_match,
785 },
786 });
787 }
788
789 Ok(test_binary.into_test_suite(RustTestSuiteStatus::Listed {
790 test_cases: test_cases.into(),
791 }))
792 }
793
794 fn process_skipped(
795 test_binary: RustTestArtifact<'g>,
796 reason: BinaryMismatchReason,
797 ) -> RustTestSuite<'g> {
798 test_binary.into_test_suite(RustTestSuiteStatus::Skipped { reason })
799 }
800
801 fn apply_partitioning(
807 rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
808 partitioner_builder: Option<&PartitionerBuilder>,
809 ) {
810 let Some(partitioner_builder) = partitioner_builder else {
811 return;
812 };
813
814 match partitioner_builder.scope() {
815 PartitionerScope::PerBinary => {
816 Self::apply_per_binary_partitioning(rust_suites, partitioner_builder);
817 }
818 PartitionerScope::CrossBinary => {
819 Self::apply_cross_binary_partitioning(rust_suites, partitioner_builder);
820 }
821 }
822 }
823
824 fn apply_per_binary_partitioning(
827 rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
828 partitioner_builder: &PartitionerBuilder,
829 ) {
830 for mut suite in rust_suites.iter_mut() {
831 let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
832 continue;
833 };
834
835 let mut non_ignored_partitioner = partitioner_builder.build();
837 apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
838
839 let mut ignored_partitioner = partitioner_builder.build();
840 apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
841 }
842 }
843
844 fn apply_cross_binary_partitioning(
848 rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
849 partitioner_builder: &PartitionerBuilder,
850 ) {
851 let mut non_ignored_partitioner = partitioner_builder.build();
853 for mut suite in rust_suites.iter_mut() {
854 let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
855 continue;
856 };
857 apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
858 }
859
860 let mut ignored_partitioner = partitioner_builder.build();
862 for mut suite in rust_suites.iter_mut() {
863 let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
864 continue;
865 };
866 apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
867 }
868 }
869
870 fn parse<'a>(
872 binary_id: &'a RustBinaryId,
873 list_output: &'a str,
874 ) -> Result<Vec<(&'a str, RustTestKind)>, CreateTestListError> {
875 let mut list = parse_list_lines(binary_id, list_output).collect::<Result<Vec<_>, _>>()?;
876 list.sort_unstable();
877 Ok(list)
878 }
879
880 pub fn write_human(
882 &self,
883 writer: &mut dyn WriteStr,
884 verbose: bool,
885 colorize: bool,
886 ) -> io::Result<()> {
887 self.write_human_impl(None, writer, verbose, colorize)
888 }
889
890 pub(crate) fn write_human_with_filter(
892 &self,
893 filter: &TestListDisplayFilter<'_>,
894 writer: &mut dyn WriteStr,
895 verbose: bool,
896 colorize: bool,
897 ) -> io::Result<()> {
898 self.write_human_impl(Some(filter), writer, verbose, colorize)
899 }
900
901 fn write_human_impl(
902 &self,
903 filter: Option<&TestListDisplayFilter<'_>>,
904 mut writer: &mut dyn WriteStr,
905 verbose: bool,
906 colorize: bool,
907 ) -> io::Result<()> {
908 let mut styles = Styles::default();
909 if colorize {
910 styles.colorize();
911 }
912
913 for info in &self.rust_suites {
914 let matcher = match filter {
915 Some(filter) => match filter.matcher_for(&info.binary_id) {
916 Some(matcher) => matcher,
917 None => continue,
918 },
919 None => DisplayFilterMatcher::All,
920 };
921
922 if !verbose
925 && info
926 .status
927 .test_cases()
928 .all(|case| !case.test_info.filter_match.is_match())
929 {
930 continue;
931 }
932
933 writeln!(writer, "{}:", info.binary_id.style(styles.binary_id))?;
934 if verbose {
935 writeln!(
936 writer,
937 " {} {}",
938 "bin:".style(styles.field),
939 info.binary_path
940 )?;
941 writeln!(writer, " {} {}", "cwd:".style(styles.field), info.cwd)?;
942 writeln!(
943 writer,
944 " {} {}",
945 "build platform:".style(styles.field),
946 info.build_platform,
947 )?;
948 }
949
950 let mut indented = indented(writer).with_str(" ");
951
952 match &info.status {
953 RustTestSuiteStatus::Listed { test_cases } => {
954 let matching_tests: Vec<_> = test_cases
955 .iter()
956 .filter(|case| matcher.is_match(&case.name))
957 .collect();
958 if matching_tests.is_empty() {
959 writeln!(indented, "(no tests)")?;
960 } else {
961 for case in matching_tests {
962 match (verbose, case.test_info.filter_match.is_match()) {
963 (_, true) => {
964 write_test_name(&case.name, &styles, &mut indented)?;
965 writeln!(indented)?;
966 }
967 (true, false) => {
968 write_test_name(&case.name, &styles, &mut indented)?;
969 writeln!(indented, " (skipped)")?;
970 }
971 (false, false) => {
972 }
974 }
975 }
976 }
977 }
978 RustTestSuiteStatus::Skipped { reason } => {
979 writeln!(indented, "(test binary {reason}, skipped)")?;
980 }
981 }
982
983 writer = indented.into_inner();
984 }
985 Ok(())
986 }
987
988 pub fn write_oneline(
990 &self,
991 writer: &mut dyn WriteStr,
992 verbose: bool,
993 colorize: bool,
994 ) -> io::Result<()> {
995 let mut styles = Styles::default();
996 if colorize {
997 styles.colorize();
998 }
999
1000 for info in &self.rust_suites {
1001 match &info.status {
1002 RustTestSuiteStatus::Listed { test_cases } => {
1003 for case in test_cases.iter() {
1004 let is_match = case.test_info.filter_match.is_match();
1005 if !verbose && !is_match {
1007 continue;
1008 }
1009
1010 write!(writer, "{} ", info.binary_id.style(styles.binary_id))?;
1011 write_test_name(&case.name, &styles, writer)?;
1012
1013 if verbose {
1014 write!(
1015 writer,
1016 " [{}{}] [{}{}] [{}{}]{}",
1017 "bin: ".style(styles.field),
1018 info.binary_path,
1019 "cwd: ".style(styles.field),
1020 info.cwd,
1021 "build platform: ".style(styles.field),
1022 info.build_platform,
1023 if is_match { "" } else { " (skipped)" },
1024 )?;
1025 }
1026
1027 writeln!(writer)?;
1028 }
1029 }
1030 RustTestSuiteStatus::Skipped { .. } => {
1031 }
1033 }
1034 }
1035
1036 Ok(())
1037 }
1038}
1039
1040fn apply_partitioner_to_tests(
1042 test_cases: &mut IdOrdMap<RustTestCase>,
1043 partitioner: &mut dyn Partitioner,
1044 ignored: bool,
1045) {
1046 for mut test_case in test_cases.iter_mut() {
1047 if test_case.test_info.ignored == ignored {
1048 apply_partition_to_test(&mut test_case, partitioner);
1049 }
1050 }
1051}
1052
1053fn apply_partition_to_test(test_case: &mut RustTestCase, partitioner: &mut dyn Partitioner) {
1061 match test_case.test_info.filter_match {
1062 FilterMatch::Matches => {
1063 if !partitioner.test_matches(test_case.name.as_str()) {
1064 test_case.test_info.filter_match = FilterMatch::Mismatch {
1065 reason: MismatchReason::Partition,
1066 };
1067 }
1068 }
1069 FilterMatch::Mismatch {
1070 reason: MismatchReason::RerunAlreadyPassed,
1071 } => {
1072 let _ = partitioner.test_matches(test_case.name.as_str());
1074 }
1075 FilterMatch::Mismatch { .. } => {
1076 }
1078 }
1079}
1080
1081fn parse_list_lines<'a>(
1082 binary_id: &'a RustBinaryId,
1083 list_output: &'a str,
1084) -> impl Iterator<Item = Result<(&'a str, RustTestKind), CreateTestListError>> + 'a + use<'a> {
1085 list_output
1091 .lines()
1092 .map(move |line| match line.strip_suffix(": test") {
1093 Some(test_name) => Ok((test_name, RustTestKind::TEST)),
1094 None => match line.strip_suffix(": benchmark") {
1095 Some(test_name) => Ok((test_name, RustTestKind::BENCH)),
1096 None => Err(CreateTestListError::parse_line(
1097 binary_id.clone(),
1098 format!(
1099 "line {line:?} did not end with the string \": test\" or \": benchmark\""
1100 ),
1101 list_output,
1102 )),
1103 },
1104 })
1105}
1106
1107pub trait ListProfile {
1109 fn filterset_ecx(&self) -> EvalContext<'_>;
1111
1112 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_>;
1114}
1115
1116impl<'g> ListProfile for EvaluatableProfile<'g> {
1117 fn filterset_ecx(&self) -> EvalContext<'_> {
1118 self.filterset_ecx()
1119 }
1120
1121 fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1122 self.list_settings_for(query)
1123 }
1124}
1125
1126pub struct TestPriorityQueue<'a> {
1128 tests: Vec<TestInstanceWithSettings<'a>>,
1129}
1130
1131impl<'a> TestPriorityQueue<'a> {
1132 fn new(test_list: &'a TestList<'a>, profile: &'a EvaluatableProfile<'a>) -> Self {
1133 let mode = test_list.mode();
1134 let mut tests = test_list
1135 .iter_tests()
1136 .map(|instance| {
1137 let settings = profile.settings_for(mode, &instance.to_test_query());
1138 TestInstanceWithSettings { instance, settings }
1139 })
1140 .collect::<Vec<_>>();
1141 tests.sort_by_key(|test| test.settings.priority());
1144
1145 Self { tests }
1146 }
1147}
1148
1149impl<'a> IntoIterator for TestPriorityQueue<'a> {
1150 type Item = TestInstanceWithSettings<'a>;
1151 type IntoIter = std::vec::IntoIter<Self::Item>;
1152
1153 fn into_iter(self) -> Self::IntoIter {
1154 self.tests.into_iter()
1155 }
1156}
1157
1158#[derive(Debug)]
1162pub struct TestInstanceWithSettings<'a> {
1163 pub instance: TestInstance<'a>,
1165
1166 pub settings: TestSettings<'a>,
1168}
1169
1170#[derive(Clone, Debug, Eq, PartialEq)]
1174pub struct RustTestSuite<'g> {
1175 pub binary_id: RustBinaryId,
1177
1178 pub binary_path: Utf8PathBuf,
1180
1181 pub package: PackageMetadata<'g>,
1183
1184 pub binary_name: String,
1186
1187 pub kind: RustTestBinaryKind,
1189
1190 pub cwd: Utf8PathBuf,
1193
1194 pub build_platform: BuildPlatform,
1196
1197 pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
1199
1200 pub status: RustTestSuiteStatus,
1202}
1203
1204impl IdOrdItem for RustTestSuite<'_> {
1205 type Key<'a>
1206 = &'a RustBinaryId
1207 where
1208 Self: 'a;
1209
1210 fn key(&self) -> Self::Key<'_> {
1211 &self.binary_id
1212 }
1213
1214 id_upcast!();
1215}
1216
1217impl RustTestArtifact<'_> {
1218 async fn exec(
1220 &self,
1221 lctx: &LocalExecuteContext<'_>,
1222 list_settings: &ListSettings<'_>,
1223 target_runner: &TargetRunner,
1224 ) -> Result<(String, String), CreateTestListError> {
1225 if !self.cwd.is_dir() {
1228 return Err(CreateTestListError::CwdIsNotDir {
1229 binary_id: self.binary_id.clone(),
1230 cwd: self.cwd.clone(),
1231 });
1232 }
1233 let platform_runner = target_runner.for_build_platform(self.build_platform);
1234
1235 let non_ignored = self.exec_single(false, lctx, list_settings, platform_runner);
1236 let ignored = self.exec_single(true, lctx, list_settings, platform_runner);
1237
1238 let (non_ignored_out, ignored_out) = futures::future::join(non_ignored, ignored).await;
1239 Ok((non_ignored_out?, ignored_out?))
1240 }
1241
1242 async fn exec_single(
1243 &self,
1244 ignored: bool,
1245 lctx: &LocalExecuteContext<'_>,
1246 list_settings: &ListSettings<'_>,
1247 runner: Option<&PlatformRunner>,
1248 ) -> Result<String, CreateTestListError> {
1249 let mut cli = TestCommandCli::default();
1250 cli.apply_wrappers(
1251 list_settings.list_wrapper(),
1252 runner,
1253 lctx.workspace_root,
1254 &lctx.rust_build_meta.target_directory,
1255 );
1256 cli.push(self.binary_path.as_str());
1257
1258 cli.extend(["--list", "--format", "terse"]);
1259 if ignored {
1260 cli.push("--ignored");
1261 }
1262
1263 let cmd = TestCommand::new(
1264 lctx,
1265 cli.program
1266 .clone()
1267 .expect("at least one argument passed in")
1268 .into_owned(),
1269 &cli.args,
1270 cli.env,
1271 &self.cwd,
1272 &self.package,
1273 &self.non_test_binaries,
1274 &Interceptor::None, );
1276
1277 let output =
1278 cmd.wait_with_output()
1279 .await
1280 .map_err(|error| CreateTestListError::CommandExecFail {
1281 binary_id: self.binary_id.clone(),
1282 command: cli.to_owned_cli(),
1283 error,
1284 })?;
1285
1286 if output.status.success() {
1287 String::from_utf8(output.stdout).map_err(|err| CreateTestListError::CommandNonUtf8 {
1288 binary_id: self.binary_id.clone(),
1289 command: cli.to_owned_cli(),
1290 stdout: err.into_bytes(),
1291 stderr: output.stderr,
1292 })
1293 } else {
1294 Err(CreateTestListError::CommandFail {
1295 binary_id: self.binary_id.clone(),
1296 command: cli.to_owned_cli(),
1297 exit_status: output.status,
1298 stdout: output.stdout,
1299 stderr: output.stderr,
1300 })
1301 }
1302 }
1303}
1304
1305#[derive(Clone, Debug, Eq, PartialEq)]
1309pub enum RustTestSuiteStatus {
1310 Listed {
1312 test_cases: DebugIgnore<IdOrdMap<RustTestCase>>,
1314 },
1315
1316 Skipped {
1318 reason: BinaryMismatchReason,
1320 },
1321}
1322
1323static EMPTY_TEST_CASE_MAP: IdOrdMap<RustTestCase> = IdOrdMap::new();
1324
1325impl RustTestSuiteStatus {
1326 pub fn test_count(&self) -> usize {
1328 match self {
1329 RustTestSuiteStatus::Listed { test_cases } => test_cases.len(),
1330 RustTestSuiteStatus::Skipped { .. } => 0,
1331 }
1332 }
1333
1334 pub fn get(&self, name: &TestCaseName) -> Option<&RustTestCase> {
1336 match self {
1337 RustTestSuiteStatus::Listed { test_cases } => test_cases.get(name),
1338 RustTestSuiteStatus::Skipped { .. } => None,
1339 }
1340 }
1341
1342 pub fn test_cases(&self) -> impl Iterator<Item = &RustTestCase> + '_ {
1344 match self {
1345 RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
1346 RustTestSuiteStatus::Skipped { .. } => {
1347 EMPTY_TEST_CASE_MAP.iter()
1349 }
1350 }
1351 }
1352
1353 pub fn to_summary(
1355 &self,
1356 ) -> (
1357 RustTestSuiteStatusSummary,
1358 BTreeMap<TestCaseName, RustTestCaseSummary>,
1359 ) {
1360 match self {
1361 Self::Listed { test_cases } => (
1362 RustTestSuiteStatusSummary::LISTED,
1363 test_cases
1364 .iter()
1365 .cloned()
1366 .map(|case| (case.name, case.test_info))
1367 .collect(),
1368 ),
1369 Self::Skipped {
1370 reason: BinaryMismatchReason::Expression,
1371 } => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
1372 Self::Skipped {
1373 reason: BinaryMismatchReason::DefaultSet,
1374 } => (
1375 RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER,
1376 BTreeMap::new(),
1377 ),
1378 }
1379 }
1380}
1381
1382#[derive(Clone, Debug, Eq, PartialEq)]
1384pub struct RustTestCase {
1385 pub name: TestCaseName,
1387
1388 pub test_info: RustTestCaseSummary,
1390}
1391
1392impl IdOrdItem for RustTestCase {
1393 type Key<'a> = &'a TestCaseName;
1394 fn key(&self) -> Self::Key<'_> {
1395 &self.name
1396 }
1397 id_upcast!();
1398}
1399
1400#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1402pub struct TestInstance<'a> {
1403 pub name: &'a TestCaseName,
1405
1406 pub suite_info: &'a RustTestSuite<'a>,
1408
1409 pub test_info: &'a RustTestCaseSummary,
1411}
1412
1413impl<'a> TestInstance<'a> {
1414 pub(crate) fn new(case: &'a RustTestCase, suite_info: &'a RustTestSuite) -> Self {
1416 Self {
1417 name: &case.name,
1418 suite_info,
1419 test_info: &case.test_info,
1420 }
1421 }
1422
1423 #[inline]
1426 pub fn id(&self) -> TestInstanceId<'a> {
1427 TestInstanceId {
1428 binary_id: &self.suite_info.binary_id,
1429 test_name: self.name,
1430 }
1431 }
1432
1433 pub fn to_test_query(&self) -> TestQuery<'a> {
1435 TestQuery {
1436 binary_query: BinaryQuery {
1437 package_id: self.suite_info.package.id(),
1438 binary_id: &self.suite_info.binary_id,
1439 kind: &self.suite_info.kind,
1440 binary_name: &self.suite_info.binary_name,
1441 platform: convert_build_platform(self.suite_info.build_platform),
1442 },
1443 test_name: self.name,
1444 }
1445 }
1446
1447 pub(crate) fn make_command(
1449 &self,
1450 ctx: &TestExecuteContext<'_>,
1451 test_list: &TestList<'_>,
1452 wrapper_script: Option<&WrapperScriptConfig>,
1453 extra_args: &[String],
1454 interceptor: &Interceptor,
1455 ) -> TestCommand {
1456 let cli = self.compute_cli(ctx, test_list, wrapper_script, extra_args);
1458
1459 let lctx = LocalExecuteContext {
1460 phase: TestCommandPhase::Run,
1461 workspace_root: test_list.workspace_root(),
1462 rust_build_meta: &test_list.rust_build_meta,
1463 double_spawn: ctx.double_spawn,
1464 dylib_path: test_list.updated_dylib_path(),
1465 profile_name: ctx.profile_name,
1466 env: &test_list.env,
1467 };
1468
1469 TestCommand::new(
1470 &lctx,
1471 cli.program
1472 .expect("at least one argument is guaranteed")
1473 .into_owned(),
1474 &cli.args,
1475 cli.env,
1476 &self.suite_info.cwd,
1477 &self.suite_info.package,
1478 &self.suite_info.non_test_binaries,
1479 interceptor,
1480 )
1481 }
1482
1483 pub(crate) fn command_line(
1484 &self,
1485 ctx: &TestExecuteContext<'_>,
1486 test_list: &TestList<'_>,
1487 wrapper_script: Option<&WrapperScriptConfig>,
1488 extra_args: &[String],
1489 ) -> Vec<String> {
1490 self.compute_cli(ctx, test_list, wrapper_script, extra_args)
1491 .to_owned_cli()
1492 }
1493
1494 fn compute_cli(
1495 &self,
1496 ctx: &'a TestExecuteContext<'_>,
1497 test_list: &TestList<'_>,
1498 wrapper_script: Option<&'a WrapperScriptConfig>,
1499 extra_args: &'a [String],
1500 ) -> TestCommandCli<'a> {
1501 let platform_runner = ctx
1502 .target_runner
1503 .for_build_platform(self.suite_info.build_platform);
1504
1505 let mut cli = TestCommandCli::default();
1506 cli.apply_wrappers(
1507 wrapper_script,
1508 platform_runner,
1509 test_list.workspace_root(),
1510 &test_list.rust_build_meta().target_directory,
1511 );
1512 cli.push(self.suite_info.binary_path.as_str());
1513
1514 cli.extend(["--exact", self.name.as_str(), "--nocapture"]);
1515 if self.test_info.ignored {
1516 cli.push("--ignored");
1517 }
1518 match test_list.mode() {
1519 NextestRunMode::Test => {}
1520 NextestRunMode::Benchmark => {
1521 cli.push("--bench");
1522 }
1523 }
1524 cli.extend(extra_args.iter().map(String::as_str));
1525
1526 cli
1527 }
1528}
1529
1530#[derive(Clone, Debug, Default)]
1531struct TestCommandCli<'a> {
1532 program: Option<Cow<'a, str>>,
1533 args: Vec<Cow<'a, str>>,
1534 env: Option<&'a ScriptCommandEnvMap>,
1535}
1536
1537impl<'a> TestCommandCli<'a> {
1538 fn apply_wrappers(
1539 &mut self,
1540 wrapper_script: Option<&'a WrapperScriptConfig>,
1541 platform_runner: Option<&'a PlatformRunner>,
1542 workspace_root: &Utf8Path,
1543 target_dir: &Utf8Path,
1544 ) {
1545 if let Some(wrapper) = wrapper_script {
1547 match wrapper.target_runner {
1548 WrapperScriptTargetRunner::Ignore => {
1549 self.env = Some(&wrapper.command.env);
1551 self.push(wrapper.command.program(workspace_root, target_dir));
1552 self.extend(wrapper.command.args.iter().map(String::as_str));
1553 }
1554 WrapperScriptTargetRunner::AroundWrapper => {
1555 self.env = Some(&wrapper.command.env);
1557 if let Some(runner) = platform_runner {
1558 self.push(runner.binary());
1559 self.extend(runner.args());
1560 }
1561 self.push(wrapper.command.program(workspace_root, target_dir));
1562 self.extend(wrapper.command.args.iter().map(String::as_str));
1563 }
1564 WrapperScriptTargetRunner::WithinWrapper => {
1565 self.env = Some(&wrapper.command.env);
1567 self.push(wrapper.command.program(workspace_root, target_dir));
1568 self.extend(wrapper.command.args.iter().map(String::as_str));
1569 if let Some(runner) = platform_runner {
1570 self.push(runner.binary());
1571 self.extend(runner.args());
1572 }
1573 }
1574 WrapperScriptTargetRunner::OverridesWrapper => {
1575 if let Some(runner) = platform_runner {
1576 self.push(runner.binary());
1579 self.extend(runner.args());
1580 } else {
1581 self.env = Some(&wrapper.command.env);
1583 self.push(wrapper.command.program(workspace_root, target_dir));
1584 self.extend(wrapper.command.args.iter().map(String::as_str));
1585 }
1586 }
1587 }
1588 } else {
1589 if let Some(runner) = platform_runner {
1591 self.push(runner.binary());
1592 self.extend(runner.args());
1593 }
1594 }
1595 }
1596
1597 fn push(&mut self, arg: impl Into<Cow<'a, str>>) {
1598 if self.program.is_none() {
1599 self.program = Some(arg.into());
1600 } else {
1601 self.args.push(arg.into());
1602 }
1603 }
1604
1605 fn extend(&mut self, args: impl IntoIterator<Item = &'a str>) {
1606 for arg in args {
1607 self.push(arg);
1608 }
1609 }
1610
1611 fn to_owned_cli(&self) -> Vec<String> {
1612 let mut owned_cli = Vec::new();
1613 if let Some(program) = &self.program {
1614 owned_cli.push(program.to_string());
1615 }
1616 owned_cli.extend(self.args.iter().map(|arg| arg.to_string()));
1617 owned_cli
1618 }
1619}
1620
1621#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize)]
1625pub struct TestInstanceId<'a> {
1626 pub binary_id: &'a RustBinaryId,
1628
1629 pub test_name: &'a TestCaseName,
1631}
1632
1633impl TestInstanceId<'_> {
1634 pub fn attempt_id(
1638 &self,
1639 run_id: ReportUuid,
1640 stress_index: Option<u32>,
1641 attempt: u32,
1642 ) -> String {
1643 let mut out = String::new();
1644 swrite!(out, "{run_id}:{}", self.binary_id);
1645 if let Some(stress_index) = stress_index {
1646 swrite!(out, "@stress-{}", stress_index);
1647 }
1648 swrite!(out, "${}", self.test_name);
1649 if attempt > 1 {
1650 swrite!(out, "#{attempt}");
1651 }
1652
1653 out
1654 }
1655}
1656
1657impl fmt::Display for TestInstanceId<'_> {
1658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1659 write!(f, "{} {}", self.binary_id, self.test_name)
1660 }
1661}
1662
1663#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
1665#[serde(rename_all = "kebab-case")]
1666#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1667pub struct OwnedTestInstanceId {
1668 pub binary_id: RustBinaryId,
1670
1671 #[serde(rename = "name")]
1673 pub test_name: TestCaseName,
1674}
1675
1676impl OwnedTestInstanceId {
1677 pub fn as_ref(&self) -> TestInstanceId<'_> {
1679 TestInstanceId {
1680 binary_id: &self.binary_id,
1681 test_name: &self.test_name,
1682 }
1683 }
1684}
1685
1686impl TestInstanceId<'_> {
1687 pub fn to_owned(&self) -> OwnedTestInstanceId {
1689 OwnedTestInstanceId {
1690 binary_id: self.binary_id.clone(),
1691 test_name: self.test_name.clone(),
1692 }
1693 }
1694}
1695
1696pub trait TestInstanceIdKey {
1702 fn key<'k>(&'k self) -> TestInstanceId<'k>;
1704}
1705
1706impl TestInstanceIdKey for OwnedTestInstanceId {
1707 fn key<'k>(&'k self) -> TestInstanceId<'k> {
1708 TestInstanceId {
1709 binary_id: &self.binary_id,
1710 test_name: &self.test_name,
1711 }
1712 }
1713}
1714
1715impl<'a> TestInstanceIdKey for TestInstanceId<'a> {
1716 fn key<'k>(&'k self) -> TestInstanceId<'k> {
1717 *self
1718 }
1719}
1720
1721impl<'a> Borrow<dyn TestInstanceIdKey + 'a> for OwnedTestInstanceId {
1722 fn borrow(&self) -> &(dyn TestInstanceIdKey + 'a) {
1723 self
1724 }
1725}
1726
1727impl<'a> PartialEq for dyn TestInstanceIdKey + 'a {
1728 fn eq(&self, other: &(dyn TestInstanceIdKey + 'a)) -> bool {
1729 self.key() == other.key()
1730 }
1731}
1732
1733impl<'a> Eq for dyn TestInstanceIdKey + 'a {}
1734
1735impl<'a> PartialOrd for dyn TestInstanceIdKey + 'a {
1736 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1737 Some(self.cmp(other))
1738 }
1739}
1740
1741impl<'a> Ord for dyn TestInstanceIdKey + 'a {
1742 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1743 self.key().cmp(&other.key())
1744 }
1745}
1746
1747impl<'a> Hash for dyn TestInstanceIdKey + 'a {
1748 fn hash<H: Hasher>(&self, state: &mut H) {
1749 self.key().hash(state);
1750 }
1751}
1752
1753#[derive(Clone, Debug)]
1755pub struct TestExecuteContext<'a> {
1756 pub profile_name: &'a str,
1758
1759 pub double_spawn: &'a DoubleSpawnInfo,
1761
1762 pub target_runner: &'a TargetRunner,
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768 use super::*;
1769 use crate::{
1770 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
1771 config::scripts::{ScriptCommand, ScriptCommandEnvMap, ScriptCommandRelativeTo},
1772 list::{
1773 SerializableFormat,
1774 test_helpers::{PACKAGE_GRAPH_FIXTURE, package_metadata},
1775 },
1776 platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
1777 target_runner::PlatformRunnerSource,
1778 test_filter::{RunIgnored, TestFilterPatterns},
1779 };
1780 use iddqd::id_ord_map;
1781 use indoc::indoc;
1782 use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, ParseContext};
1783 use nextest_metadata::{FilterMatch, MismatchReason, PlatformLibdirUnavailable, RustTestKind};
1784 use pretty_assertions::assert_eq;
1785 use std::{collections::BTreeMap, hash::DefaultHasher};
1786 use target_spec::Platform;
1787 use test_strategy::proptest;
1788
1789 #[test]
1790 fn test_parse_test_list() {
1791 let non_ignored_output = indoc! {"
1793 tests::foo::test_bar: test
1794 tests::baz::test_quux: test
1795 benches::bench_foo: benchmark
1796 "};
1797 let ignored_output = indoc! {"
1798 tests::ignored::test_bar: test
1799 tests::baz::test_ignored: test
1800 benches::ignored_bench_foo: benchmark
1801 "};
1802
1803 let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
1804
1805 let test_filter = TestFilter::new(
1806 NextestRunMode::Test,
1807 RunIgnored::Default,
1808 TestFilterPatterns::default(),
1809 vec![
1811 Filterset::parse("platform(target)".to_owned(), &cx, FiltersetKind::Test).unwrap(),
1812 ],
1813 )
1814 .unwrap();
1815 let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
1816 let fake_binary_name = "fake-binary".to_owned();
1817 let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
1818
1819 let test_binary = RustTestArtifact {
1820 binary_path: "/fake/binary".into(),
1821 cwd: fake_cwd.clone(),
1822 package: package_metadata(),
1823 binary_name: fake_binary_name.clone(),
1824 binary_id: fake_binary_id.clone(),
1825 kind: RustTestBinaryKind::LIB,
1826 non_test_binaries: BTreeSet::new(),
1827 build_platform: BuildPlatform::Target,
1828 };
1829
1830 let skipped_binary_name = "skipped-binary".to_owned();
1831 let skipped_binary_id = RustBinaryId::new("fake-package::skipped-binary");
1832 let skipped_binary = RustTestArtifact {
1833 binary_path: "/fake/skipped-binary".into(),
1834 cwd: fake_cwd.clone(),
1835 package: package_metadata(),
1836 binary_name: skipped_binary_name.clone(),
1837 binary_id: skipped_binary_id.clone(),
1838 kind: RustTestBinaryKind::PROC_MACRO,
1839 non_test_binaries: BTreeSet::new(),
1840 build_platform: BuildPlatform::Host,
1841 };
1842
1843 let fake_triple = TargetTriple {
1844 platform: Platform::new(
1845 "aarch64-unknown-linux-gnu",
1846 target_spec::TargetFeatures::Unknown,
1847 )
1848 .unwrap(),
1849 source: TargetTripleSource::CliOption,
1850 location: TargetDefinitionLocation::Builtin,
1851 };
1852 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
1853 let build_platforms = BuildPlatforms {
1854 host: HostPlatform {
1855 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
1856 libdir: PlatformLibdir::Available(fake_host_libdir.into()),
1857 },
1858 target: Some(TargetPlatform {
1859 triple: fake_triple,
1860 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
1862 }),
1863 };
1864
1865 let fake_env = EnvironmentMap::empty();
1866 let rust_build_meta =
1867 RustBuildMeta::new("/fake", "/fake", build_platforms).map_paths(&PathMapper::noop());
1868 let ecx = EvalContext {
1869 default_filter: &CompiledExpr::ALL,
1870 };
1871 let test_list = TestList::new_with_outputs(
1872 [
1873 (test_binary, &non_ignored_output, &ignored_output),
1874 (
1875 skipped_binary,
1876 &"should-not-show-up-stdout",
1877 &"should-not-show-up-stderr",
1878 ),
1879 ],
1880 Utf8PathBuf::from("/fake/path"),
1881 rust_build_meta,
1882 &test_filter,
1883 None,
1884 fake_env,
1885 &ecx,
1886 FilterBound::All,
1887 )
1888 .expect("valid output");
1889 assert_eq!(
1890 test_list.rust_suites,
1891 id_ord_map! {
1892 RustTestSuite {
1893 status: RustTestSuiteStatus::Listed {
1894 test_cases: id_ord_map! {
1895 RustTestCase {
1896 name: TestCaseName::new("tests::foo::test_bar"),
1897 test_info: RustTestCaseSummary {
1898 kind: Some(RustTestKind::TEST),
1899 ignored: false,
1900 filter_match: FilterMatch::Matches,
1901 },
1902 },
1903 RustTestCase {
1904 name: TestCaseName::new("tests::baz::test_quux"),
1905 test_info: RustTestCaseSummary {
1906 kind: Some(RustTestKind::TEST),
1907 ignored: false,
1908 filter_match: FilterMatch::Matches,
1909 },
1910 },
1911 RustTestCase {
1912 name: TestCaseName::new("benches::bench_foo"),
1913 test_info: RustTestCaseSummary {
1914 kind: Some(RustTestKind::BENCH),
1915 ignored: false,
1916 filter_match: FilterMatch::Matches,
1917 },
1918 },
1919 RustTestCase {
1920 name: TestCaseName::new("tests::ignored::test_bar"),
1921 test_info: RustTestCaseSummary {
1922 kind: Some(RustTestKind::TEST),
1923 ignored: true,
1924 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1925 },
1926 },
1927 RustTestCase {
1928 name: TestCaseName::new("tests::baz::test_ignored"),
1929 test_info: RustTestCaseSummary {
1930 kind: Some(RustTestKind::TEST),
1931 ignored: true,
1932 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1933 },
1934 },
1935 RustTestCase {
1936 name: TestCaseName::new("benches::ignored_bench_foo"),
1937 test_info: RustTestCaseSummary {
1938 kind: Some(RustTestKind::BENCH),
1939 ignored: true,
1940 filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1941 },
1942 },
1943 }.into(),
1944 },
1945 cwd: fake_cwd.clone(),
1946 build_platform: BuildPlatform::Target,
1947 package: package_metadata(),
1948 binary_name: fake_binary_name,
1949 binary_id: fake_binary_id,
1950 binary_path: "/fake/binary".into(),
1951 kind: RustTestBinaryKind::LIB,
1952 non_test_binaries: BTreeSet::new(),
1953 },
1954 RustTestSuite {
1955 status: RustTestSuiteStatus::Skipped {
1956 reason: BinaryMismatchReason::Expression,
1957 },
1958 cwd: fake_cwd,
1959 build_platform: BuildPlatform::Host,
1960 package: package_metadata(),
1961 binary_name: skipped_binary_name,
1962 binary_id: skipped_binary_id,
1963 binary_path: "/fake/skipped-binary".into(),
1964 kind: RustTestBinaryKind::PROC_MACRO,
1965 non_test_binaries: BTreeSet::new(),
1966 },
1967 }
1968 );
1969
1970 static EXPECTED_HUMAN: &str = indoc! {"
1972 fake-package::fake-binary:
1973 benches::bench_foo
1974 tests::baz::test_quux
1975 tests::foo::test_bar
1976 "};
1977 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {"
1978 fake-package::fake-binary:
1979 bin: /fake/binary
1980 cwd: /fake/cwd
1981 build platform: target
1982 benches::bench_foo
1983 benches::ignored_bench_foo (skipped)
1984 tests::baz::test_ignored (skipped)
1985 tests::baz::test_quux
1986 tests::foo::test_bar
1987 tests::ignored::test_bar (skipped)
1988 fake-package::skipped-binary:
1989 bin: /fake/skipped-binary
1990 cwd: /fake/cwd
1991 build platform: host
1992 (test binary didn't match filtersets, skipped)
1993 "};
1994 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
1995 {
1996 "rust-build-meta": {
1997 "target-directory": "/fake",
1998 "build-directory": "/fake",
1999 "base-output-directories": [],
2000 "non-test-binaries": {},
2001 "build-script-out-dirs": {},
2002 "build-script-info": {},
2003 "linked-paths": [],
2004 "platforms": {
2005 "host": {
2006 "platform": {
2007 "triple": "x86_64-unknown-linux-gnu",
2008 "target-features": "unknown"
2009 },
2010 "libdir": {
2011 "status": "available",
2012 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
2013 }
2014 },
2015 "targets": [
2016 {
2017 "platform": {
2018 "triple": "aarch64-unknown-linux-gnu",
2019 "target-features": "unknown"
2020 },
2021 "libdir": {
2022 "status": "unavailable",
2023 "reason": "test"
2024 }
2025 }
2026 ]
2027 },
2028 "target-platforms": [
2029 {
2030 "triple": "aarch64-unknown-linux-gnu",
2031 "target-features": "unknown"
2032 }
2033 ],
2034 "target-platform": "aarch64-unknown-linux-gnu"
2035 },
2036 "test-count": 6,
2037 "rust-suites": {
2038 "fake-package::fake-binary": {
2039 "package-name": "metadata-helper",
2040 "binary-id": "fake-package::fake-binary",
2041 "binary-name": "fake-binary",
2042 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
2043 "kind": "lib",
2044 "binary-path": "/fake/binary",
2045 "build-platform": "target",
2046 "cwd": "/fake/cwd",
2047 "status": "listed",
2048 "testcases": {
2049 "benches::bench_foo": {
2050 "kind": "bench",
2051 "ignored": false,
2052 "filter-match": {
2053 "status": "matches"
2054 }
2055 },
2056 "benches::ignored_bench_foo": {
2057 "kind": "bench",
2058 "ignored": true,
2059 "filter-match": {
2060 "status": "mismatch",
2061 "reason": "ignored"
2062 }
2063 },
2064 "tests::baz::test_ignored": {
2065 "kind": "test",
2066 "ignored": true,
2067 "filter-match": {
2068 "status": "mismatch",
2069 "reason": "ignored"
2070 }
2071 },
2072 "tests::baz::test_quux": {
2073 "kind": "test",
2074 "ignored": false,
2075 "filter-match": {
2076 "status": "matches"
2077 }
2078 },
2079 "tests::foo::test_bar": {
2080 "kind": "test",
2081 "ignored": false,
2082 "filter-match": {
2083 "status": "matches"
2084 }
2085 },
2086 "tests::ignored::test_bar": {
2087 "kind": "test",
2088 "ignored": true,
2089 "filter-match": {
2090 "status": "mismatch",
2091 "reason": "ignored"
2092 }
2093 }
2094 }
2095 },
2096 "fake-package::skipped-binary": {
2097 "package-name": "metadata-helper",
2098 "binary-id": "fake-package::skipped-binary",
2099 "binary-name": "skipped-binary",
2100 "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
2101 "kind": "proc-macro",
2102 "binary-path": "/fake/skipped-binary",
2103 "build-platform": "host",
2104 "cwd": "/fake/cwd",
2105 "status": "skipped",
2106 "testcases": {}
2107 }
2108 }
2109 }"#};
2110 static EXPECTED_ONELINE: &str = indoc! {"
2111 fake-package::fake-binary benches::bench_foo
2112 fake-package::fake-binary tests::baz::test_quux
2113 fake-package::fake-binary tests::foo::test_bar
2114 "};
2115 static EXPECTED_ONELINE_VERBOSE: &str = indoc! {"
2116 fake-package::fake-binary benches::bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2117 fake-package::fake-binary benches::ignored_bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2118 fake-package::fake-binary tests::baz::test_ignored [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2119 fake-package::fake-binary tests::baz::test_quux [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2120 fake-package::fake-binary tests::foo::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2121 fake-package::fake-binary tests::ignored::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2122 "};
2123
2124 assert_eq!(
2125 test_list
2126 .to_string(OutputFormat::Human { verbose: false })
2127 .expect("human succeeded"),
2128 EXPECTED_HUMAN
2129 );
2130 assert_eq!(
2131 test_list
2132 .to_string(OutputFormat::Human { verbose: true })
2133 .expect("human succeeded"),
2134 EXPECTED_HUMAN_VERBOSE
2135 );
2136 println!(
2137 "{}",
2138 test_list
2139 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
2140 .expect("json-pretty succeeded")
2141 );
2142 assert_eq!(
2143 test_list
2144 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
2145 .expect("json-pretty succeeded"),
2146 EXPECTED_JSON_PRETTY
2147 );
2148 assert_eq!(
2149 test_list
2150 .to_string(OutputFormat::Oneline { verbose: false })
2151 .expect("oneline succeeded"),
2152 EXPECTED_ONELINE
2153 );
2154 assert_eq!(
2155 test_list
2156 .to_string(OutputFormat::Oneline { verbose: true })
2157 .expect("oneline verbose succeeded"),
2158 EXPECTED_ONELINE_VERBOSE
2159 );
2160 }
2161
2162 #[test]
2163 fn apply_wrappers_examples() {
2164 cfg_if::cfg_if! {
2165 if #[cfg(windows)]
2166 {
2167 let workspace_root = Utf8Path::new("D:\\workspace\\root");
2168 let target_dir = Utf8Path::new("C:\\foo\\bar");
2169 } else {
2170 let workspace_root = Utf8Path::new("/workspace/root");
2171 let target_dir = Utf8Path::new("/foo/bar");
2172 }
2173 };
2174
2175 {
2177 let mut cli_no_wrappers = TestCommandCli::default();
2178 cli_no_wrappers.apply_wrappers(None, None, workspace_root, target_dir);
2179 cli_no_wrappers.extend(["binary", "arg"]);
2180 assert!(cli_no_wrappers.env.is_none());
2181 assert_eq!(cli_no_wrappers.to_owned_cli(), vec!["binary", "arg"]);
2182 }
2183
2184 {
2186 let runner = PlatformRunner::debug_new(
2187 "runner".into(),
2188 Vec::new(),
2189 PlatformRunnerSource::Env("fake".to_owned()),
2190 );
2191 let mut cli_runner_only = TestCommandCli::default();
2192 cli_runner_only.apply_wrappers(None, Some(&runner), workspace_root, target_dir);
2193 cli_runner_only.extend(["binary", "arg"]);
2194 assert!(cli_runner_only.env.is_none());
2195 assert_eq!(
2196 cli_runner_only.to_owned_cli(),
2197 vec!["runner", "binary", "arg"],
2198 );
2199 }
2200
2201 {
2203 let runner = PlatformRunner::debug_new(
2204 "runner".into(),
2205 Vec::new(),
2206 PlatformRunnerSource::Env("fake".to_owned()),
2207 );
2208 let wrapper_ignore = WrapperScriptConfig {
2209 command: ScriptCommand {
2210 program: "wrapper".into(),
2211 args: Vec::new(),
2212 env: ScriptCommandEnvMap::default(),
2213 relative_to: ScriptCommandRelativeTo::None,
2214 },
2215 target_runner: WrapperScriptTargetRunner::Ignore,
2216 };
2217 let mut cli_wrapper_ignore = TestCommandCli::default();
2218 cli_wrapper_ignore.apply_wrappers(
2219 Some(&wrapper_ignore),
2220 Some(&runner),
2221 workspace_root,
2222 target_dir,
2223 );
2224 cli_wrapper_ignore.extend(["binary", "arg"]);
2225 assert_eq!(
2226 cli_wrapper_ignore.env,
2227 Some(&ScriptCommandEnvMap::default())
2228 );
2229 assert_eq!(
2230 cli_wrapper_ignore.to_owned_cli(),
2231 vec!["wrapper", "binary", "arg"],
2232 );
2233 }
2234
2235 {
2237 let runner = PlatformRunner::debug_new(
2238 "runner".into(),
2239 Vec::new(),
2240 PlatformRunnerSource::Env("fake".to_owned()),
2241 );
2242 let env = ScriptCommandEnvMap::new(BTreeMap::from([(
2243 String::from("MSG"),
2244 String::from("hello world"),
2245 )]))
2246 .expect("valid env var keys");
2247 let wrapper_around = WrapperScriptConfig {
2248 command: ScriptCommand {
2249 program: "wrapper".into(),
2250 args: Vec::new(),
2251 env: env.clone(),
2252 relative_to: ScriptCommandRelativeTo::None,
2253 },
2254 target_runner: WrapperScriptTargetRunner::AroundWrapper,
2255 };
2256 let mut cli_wrapper_around = TestCommandCli::default();
2257 cli_wrapper_around.apply_wrappers(
2258 Some(&wrapper_around),
2259 Some(&runner),
2260 workspace_root,
2261 target_dir,
2262 );
2263 cli_wrapper_around.extend(["binary", "arg"]);
2264 assert_eq!(cli_wrapper_around.env, Some(&env));
2265 assert_eq!(
2266 cli_wrapper_around.to_owned_cli(),
2267 vec!["runner", "wrapper", "binary", "arg"],
2268 );
2269 }
2270
2271 {
2273 let runner = PlatformRunner::debug_new(
2274 "runner".into(),
2275 Vec::new(),
2276 PlatformRunnerSource::Env("fake".to_owned()),
2277 );
2278 let wrapper_within = WrapperScriptConfig {
2279 command: ScriptCommand {
2280 program: "wrapper".into(),
2281 args: Vec::new(),
2282 env: ScriptCommandEnvMap::default(),
2283 relative_to: ScriptCommandRelativeTo::None,
2284 },
2285 target_runner: WrapperScriptTargetRunner::WithinWrapper,
2286 };
2287 let mut cli_wrapper_within = TestCommandCli::default();
2288 cli_wrapper_within.apply_wrappers(
2289 Some(&wrapper_within),
2290 Some(&runner),
2291 workspace_root,
2292 target_dir,
2293 );
2294 cli_wrapper_within.extend(["binary", "arg"]);
2295 assert_eq!(
2296 cli_wrapper_within.env,
2297 Some(&ScriptCommandEnvMap::default())
2298 );
2299 assert_eq!(
2300 cli_wrapper_within.to_owned_cli(),
2301 vec!["wrapper", "runner", "binary", "arg"],
2302 );
2303 }
2304
2305 {
2308 let runner = PlatformRunner::debug_new(
2309 "runner".into(),
2310 Vec::new(),
2311 PlatformRunnerSource::Env("fake".to_owned()),
2312 );
2313 let wrapper_overrides = WrapperScriptConfig {
2314 command: ScriptCommand {
2315 program: "wrapper".into(),
2316 args: Vec::new(),
2317 env: ScriptCommandEnvMap::default(),
2318 relative_to: ScriptCommandRelativeTo::None,
2319 },
2320 target_runner: WrapperScriptTargetRunner::OverridesWrapper,
2321 };
2322 let mut cli_wrapper_overrides = TestCommandCli::default();
2323 cli_wrapper_overrides.apply_wrappers(
2324 Some(&wrapper_overrides),
2325 Some(&runner),
2326 workspace_root,
2327 target_dir,
2328 );
2329 cli_wrapper_overrides.extend(["binary", "arg"]);
2330 assert!(
2331 cli_wrapper_overrides.env.is_none(),
2332 "overrides-wrapper with runner should not apply wrapper env"
2333 );
2334 assert_eq!(
2335 cli_wrapper_overrides.to_owned_cli(),
2336 vec!["runner", "binary", "arg"],
2337 );
2338 }
2339
2340 {
2343 let wrapper_overrides = WrapperScriptConfig {
2344 command: ScriptCommand {
2345 program: "wrapper".into(),
2346 args: Vec::new(),
2347 env: ScriptCommandEnvMap::default(),
2348 relative_to: ScriptCommandRelativeTo::None,
2349 },
2350 target_runner: WrapperScriptTargetRunner::OverridesWrapper,
2351 };
2352 let mut cli_wrapper_overrides_no_runner = TestCommandCli::default();
2353 cli_wrapper_overrides_no_runner.apply_wrappers(
2354 Some(&wrapper_overrides),
2355 None,
2356 workspace_root,
2357 target_dir,
2358 );
2359 cli_wrapper_overrides_no_runner.extend(["binary", "arg"]);
2360 assert_eq!(
2361 cli_wrapper_overrides_no_runner.env,
2362 Some(&ScriptCommandEnvMap::default()),
2363 "overrides-wrapper without runner should apply wrapper env"
2364 );
2365 assert_eq!(
2366 cli_wrapper_overrides_no_runner.to_owned_cli(),
2367 vec!["wrapper", "binary", "arg"],
2368 );
2369 }
2370
2371 {
2373 let wrapper_with_args = WrapperScriptConfig {
2374 command: ScriptCommand {
2375 program: "wrapper".into(),
2376 args: vec!["--flag".to_string(), "value".to_string()],
2377 env: ScriptCommandEnvMap::default(),
2378 relative_to: ScriptCommandRelativeTo::None,
2379 },
2380 target_runner: WrapperScriptTargetRunner::Ignore,
2381 };
2382 let mut cli_wrapper_args = TestCommandCli::default();
2383 cli_wrapper_args.apply_wrappers(
2384 Some(&wrapper_with_args),
2385 None,
2386 workspace_root,
2387 target_dir,
2388 );
2389 cli_wrapper_args.extend(["binary", "arg"]);
2390 assert_eq!(cli_wrapper_args.env, Some(&ScriptCommandEnvMap::default()));
2391 assert_eq!(
2392 cli_wrapper_args.to_owned_cli(),
2393 vec!["wrapper", "--flag", "value", "binary", "arg"],
2394 );
2395 }
2396
2397 {
2399 let runner_with_args = PlatformRunner::debug_new(
2400 "runner".into(),
2401 vec!["--runner-flag".into(), "value".into()],
2402 PlatformRunnerSource::Env("fake".to_owned()),
2403 );
2404 let mut cli_runner_args = TestCommandCli::default();
2405 cli_runner_args.apply_wrappers(
2406 None,
2407 Some(&runner_with_args),
2408 workspace_root,
2409 target_dir,
2410 );
2411 cli_runner_args.extend(["binary", "arg"]);
2412 assert!(cli_runner_args.env.is_none());
2413 assert_eq!(
2414 cli_runner_args.to_owned_cli(),
2415 vec!["runner", "--runner-flag", "value", "binary", "arg"],
2416 );
2417 }
2418
2419 {
2421 let wrapper_relative_to_workspace_root = WrapperScriptConfig {
2422 command: ScriptCommand {
2423 program: "abc/def/my-wrapper".into(),
2424 args: vec!["--verbose".to_string()],
2425 env: ScriptCommandEnvMap::default(),
2426 relative_to: ScriptCommandRelativeTo::WorkspaceRoot,
2427 },
2428 target_runner: WrapperScriptTargetRunner::Ignore,
2429 };
2430 let mut cli_wrapper_relative = TestCommandCli::default();
2431 cli_wrapper_relative.apply_wrappers(
2432 Some(&wrapper_relative_to_workspace_root),
2433 None,
2434 workspace_root,
2435 target_dir,
2436 );
2437 cli_wrapper_relative.extend(["binary", "arg"]);
2438
2439 cfg_if::cfg_if! {
2440 if #[cfg(windows)] {
2441 let wrapper_path = "D:\\workspace\\root\\abc\\def\\my-wrapper";
2442 } else {
2443 let wrapper_path = "/workspace/root/abc/def/my-wrapper";
2444 }
2445 }
2446 assert_eq!(
2447 cli_wrapper_relative.env,
2448 Some(&ScriptCommandEnvMap::default())
2449 );
2450 assert_eq!(
2451 cli_wrapper_relative.to_owned_cli(),
2452 vec![wrapper_path, "--verbose", "binary", "arg"],
2453 );
2454 }
2455
2456 {
2458 let wrapper_relative_to_target = WrapperScriptConfig {
2459 command: ScriptCommand {
2460 program: "abc/def/my-wrapper".into(),
2461 args: vec!["--verbose".to_string()],
2462 env: ScriptCommandEnvMap::default(),
2463 relative_to: ScriptCommandRelativeTo::Target,
2464 },
2465 target_runner: WrapperScriptTargetRunner::Ignore,
2466 };
2467 let mut cli_wrapper_relative = TestCommandCli::default();
2468 cli_wrapper_relative.apply_wrappers(
2469 Some(&wrapper_relative_to_target),
2470 None,
2471 workspace_root,
2472 target_dir,
2473 );
2474 cli_wrapper_relative.extend(["binary", "arg"]);
2475 cfg_if::cfg_if! {
2476 if #[cfg(windows)] {
2477 let wrapper_path = "C:\\foo\\bar\\abc\\def\\my-wrapper";
2478 } else {
2479 let wrapper_path = "/foo/bar/abc/def/my-wrapper";
2480 }
2481 }
2482 assert_eq!(
2483 cli_wrapper_relative.env,
2484 Some(&ScriptCommandEnvMap::default())
2485 );
2486 assert_eq!(
2487 cli_wrapper_relative.to_owned_cli(),
2488 vec![wrapper_path, "--verbose", "binary", "arg"],
2489 );
2490 }
2491 }
2492
2493 #[test]
2494 fn test_parse_list_lines() {
2495 let binary_id = RustBinaryId::new("test-package::test-binary");
2496
2497 let input = indoc! {"
2499 simple_test: test
2500 module::nested_test: test
2501 deeply::nested::module::test_name: test
2502 "};
2503 let results: Vec<_> = parse_list_lines(&binary_id, input)
2504 .collect::<Result<_, _>>()
2505 .expect("parsed valid test output");
2506 insta::assert_debug_snapshot!("valid_tests", results);
2507
2508 let input = indoc! {"
2510 simple_bench: benchmark
2511 benches::module::my_benchmark: benchmark
2512 "};
2513 let results: Vec<_> = parse_list_lines(&binary_id, input)
2514 .collect::<Result<_, _>>()
2515 .expect("parsed valid benchmark output");
2516 insta::assert_debug_snapshot!("valid_benchmarks", results);
2517
2518 let input = indoc! {"
2520 test_one: test
2521 bench_one: benchmark
2522 test_two: test
2523 bench_two: benchmark
2524 "};
2525 let results: Vec<_> = parse_list_lines(&binary_id, input)
2526 .collect::<Result<_, _>>()
2527 .expect("parsed mixed output");
2528 insta::assert_debug_snapshot!("mixed_tests_and_benchmarks", results);
2529
2530 let input = indoc! {r#"
2532 test_with_underscore_123: test
2533 test::with::colons: test
2534 test_with_numbers_42: test
2535 "#};
2536 let results: Vec<_> = parse_list_lines(&binary_id, input)
2537 .collect::<Result<_, _>>()
2538 .expect("parsed tests with special characters");
2539 insta::assert_debug_snapshot!("special_characters", results);
2540
2541 let input = "";
2543 let results: Vec<_> = parse_list_lines(&binary_id, input)
2544 .collect::<Result<_, _>>()
2545 .expect("parsed empty output");
2546 insta::assert_debug_snapshot!("empty_input", results);
2547
2548 let input = "invalid_test: wrong_suffix";
2550 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2551 assert!(result.is_err());
2552 insta::assert_snapshot!("invalid_suffix_error", result.unwrap_err());
2553
2554 let input = "test_without_suffix";
2556 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2557 assert!(result.is_err());
2558 insta::assert_snapshot!("missing_suffix_error", result.unwrap_err());
2559
2560 let input = indoc! {"
2562 valid_test: test
2563 invalid_line
2564 another_valid: benchmark
2565 "};
2566 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2567 assert!(result.is_err());
2568 insta::assert_snapshot!("partial_valid_error", result.unwrap_err());
2569
2570 let input = indoc! {"
2572 valid_test: test
2573 \rinvalid_line
2574 another_valid: benchmark
2575 "};
2576 let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2577 assert!(result.is_err());
2578 insta::assert_snapshot!("control_character_error", result.unwrap_err());
2579 }
2580
2581 #[proptest]
2584 fn test_instance_id_key_borrow_consistency(
2585 owned1: OwnedTestInstanceId,
2586 owned2: OwnedTestInstanceId,
2587 ) {
2588 let borrowed1: &dyn TestInstanceIdKey = &owned1;
2590 let borrowed2: &dyn TestInstanceIdKey = &owned2;
2591
2592 assert_eq!(
2594 owned1 == owned2,
2595 borrowed1 == borrowed2,
2596 "Eq must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2597 );
2598
2599 assert_eq!(
2601 owned1.partial_cmp(&owned2),
2602 borrowed1.partial_cmp(borrowed2),
2603 "PartialOrd must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2604 );
2605
2606 assert_eq!(
2608 owned1.cmp(&owned2),
2609 borrowed1.cmp(borrowed2),
2610 "Ord must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2611 );
2612
2613 fn hash_value(x: &impl Hash) -> u64 {
2615 let mut hasher = DefaultHasher::new();
2616 x.hash(&mut hasher);
2617 hasher.finish()
2618 }
2619
2620 assert_eq!(
2621 hash_value(&owned1),
2622 hash_value(&borrowed1),
2623 "Hash must be consistent for owned1 and its borrowed form"
2624 );
2625 assert_eq!(
2626 hash_value(&owned2),
2627 hash_value(&borrowed2),
2628 "Hash must be consistent for owned2 and its borrowed form"
2629 );
2630 }
2631}