nextest_runner/list/
test_list.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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/// A Rust test binary built by Cargo. This artifact hasn't been run yet so there's no information
60/// about the tests within it.
61///
62/// Accepted as input to [`TestList::new`].
63#[derive(Clone, Debug)]
64pub struct RustTestArtifact<'g> {
65    /// A unique identifier for this test artifact.
66    pub binary_id: RustBinaryId,
67
68    /// Metadata for the package this artifact is a part of. This is used to set the correct
69    /// environment variables.
70    pub package: PackageMetadata<'g>,
71
72    /// The path to the binary artifact.
73    pub binary_path: Utf8PathBuf,
74
75    /// The unique binary name defined in `Cargo.toml` or inferred by the filename.
76    pub binary_name: String,
77
78    /// The kind of Rust test binary this is.
79    pub kind: RustTestBinaryKind,
80
81    /// Non-test binaries to be exposed to this artifact at runtime (name, path).
82    pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
83
84    /// The working directory that this test should be executed in.
85    pub cwd: Utf8PathBuf,
86
87    /// The platform for which this test artifact was built.
88    pub build_platform: BuildPlatform,
89}
90
91impl<'g> RustTestArtifact<'g> {
92    /// Constructs a list of test binaries from the list of built binaries.
93    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            // Look up the executable by package ID.
108            let package_id = PackageId::new(binary.package_id.clone());
109            let package = graph
110                .metadata(&package_id)
111                .map_err(FromMessagesError::PackageGraph)?;
112
113            // Tests are run in the directory containing Cargo.toml
114            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            // Test binaries live under the build directory (never uplifted).
126            let binary_path = path_mapper.map_build_path(binary.path.clone());
127            let cwd = path_mapper.map_cwd(cwd);
128
129            // Non-test binaries are only exposed to integration tests and benchmarks.
130            let non_test_binaries = if binary.kind == RustTestBinaryKind::TEST
131                || binary.kind == RustTestBinaryKind::BENCH
132            {
133                // Note we must use the TestListState rust_build_meta here to ensure we get remapped
134                // paths.
135                match rust_build_meta.non_test_binaries.get(package_id.repr()) {
136                    Some(binaries) => binaries
137                        .iter()
138                        .filter(|binary| {
139                            // Only expose BIN_EXE non-test files.
140                            binary.kind == RustNonTestBinaryKind::BIN_EXE
141                        })
142                        .map(|binary| {
143                            // Convert relative paths to absolute ones by joining with the target directory.
144                            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    /// Returns a [`BinaryQuery`] corresponding to this test artifact.
170    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    // ---
181    // Helper methods
182    // ---
183    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/// Information about skipped tests and binaries.
210#[derive(Clone, Debug, Eq, PartialEq)]
211pub struct SkipCounts {
212    /// The number of skipped tests.
213    pub skipped_tests: usize,
214
215    /// The number of skipped tests due to this being a rerun and the test was
216    /// already passing.
217    pub skipped_tests_rerun: usize,
218
219    /// The number of tests skipped because they are not benchmarks.
220    ///
221    /// This is used when running in benchmark mode.
222    pub skipped_tests_non_benchmark: usize,
223
224    /// The number of tests skipped due to not being in the default set.
225    pub skipped_tests_default_filter: usize,
226
227    /// The number of skipped binaries.
228    pub skipped_binaries: usize,
229
230    /// The number of binaries skipped due to not being in the default set.
231    pub skipped_binaries_default_filter: usize,
232}
233
234/// List of test instances, obtained by querying the [`RustTestArtifact`] instances generated by Cargo.
235#[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    // Computed on first access.
245    skip_counts: OnceLock<SkipCounts>,
246}
247
248impl<'g> TestList<'g> {
249    /// Creates a new test list by running the given command and applying the specified filter.
250    #[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            // Note: this is the remapped workspace root, not the original one.
276            // (We really should have newtypes for this.)
277            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                        // Run the binary to obtain the test list.
301                        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        // Ensure that the runtime doesn't stay hanging even if a custom test framework misbehaves
327        // (can be an issue on Windows).
328        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    /// Creates a new test list with the given binary names and outputs.
350    #[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    /// Reconstructs a TestList from archived summary data.
415    ///
416    /// This is used during replay to reconstruct a TestList without
417    /// executing test binaries. The reconstructed TestList provides the
418    /// data needed for display through the reporter infrastructure.
419    pub fn from_summary(
420        graph: &'g PackageGraph,
421        summary: &TestListSummary,
422        mode: NextestRunMode,
423    ) -> Result<Self, TestListFromSummaryError> {
424        // Build RustBuildMeta from summary.
425        let rust_build_meta = RustBuildMeta::from_summary(summary.rust_build_meta.clone())
426            .map_err(TestListFromSummaryError::RustBuildMeta)?;
427
428        // Get the workspace root from the graph.
429        let workspace_root = graph.workspace().root().to_path_buf();
430
431        // Construct an empty environment map - we don't need it for replay.
432        let env = EnvironmentMap::empty();
433
434        // For replay, we don't need the actual dylib path since we're not executing tests.
435        let updated_dylib_path = OsString::new();
436
437        // Build test suites from the summary.
438        let mut rust_suites = IdOrdMap::new();
439        let mut test_count = 0;
440
441        for (binary_id, suite_summary) in &summary.rust_suites {
442            // Look up the package in the graph by package_id.
443            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            // Determine the status based on the summary.
452            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                // Build test cases from the summary (only for listed suites).
462                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                // LISTED or any other status should be treated as listed.
474                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(), // Not stored in summary.
486                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    /// Returns the total number of tests across all binaries.
507    pub fn test_count(&self) -> usize {
508        self.test_count
509    }
510
511    /// Returns the mode nextest is running in.
512    pub fn mode(&self) -> NextestRunMode {
513        self.mode
514    }
515
516    /// Returns the Rust build-related metadata for this test list.
517    pub fn rust_build_meta(&self) -> &RustBuildMeta<TestListState> {
518        &self.rust_build_meta
519    }
520
521    /// Returns the total number of skipped tests.
522    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    /// Returns the total number of tests that aren't skipped.
581    ///
582    /// It is always the case that `run_count + skip_count == test_count`.
583    pub fn run_count(&self) -> usize {
584        self.test_count - self.skip_counts().skipped_tests
585    }
586
587    /// Returns the total number of binaries that contain tests.
588    pub fn binary_count(&self) -> usize {
589        self.rust_suites.len()
590    }
591
592    /// Returns the total number of binaries that were listed (not skipped).
593    pub fn listed_binary_count(&self) -> usize {
594        self.binary_count() - self.skip_counts().skipped_binaries
595    }
596
597    /// Returns the mapped workspace root.
598    pub fn workspace_root(&self) -> &Utf8Path {
599        &self.workspace_root
600    }
601
602    /// Returns the environment variables to be used when running tests.
603    pub fn cargo_env(&self) -> &EnvironmentMap {
604        &self.env
605    }
606
607    /// Returns the updated dynamic library path used for tests.
608    pub fn updated_dylib_path(&self) -> &OsStr {
609        &self.updated_dylib_path
610    }
611
612    /// Constructs a serializble summary for this test list.
613    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    /// Outputs this list to the given writer.
643    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    /// Iterates over all the test suites.
661    pub fn iter(&self) -> impl Iterator<Item = &RustTestSuite<'_>> + '_ {
662        self.rust_suites.iter()
663    }
664
665    /// Looks up a test suite by binary ID.
666    pub fn get_suite(&self, binary_id: &RustBinaryId) -> Option<&RustTestSuite<'_>> {
667        self.rust_suites.get(binary_id)
668    }
669
670    /// Iterates over the list of tests, returning the path and test name.
671    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    /// Produces a priority queue of tests based on the given profile.
681    pub fn to_priority_queue(
682        &'g self,
683        profile: &'g EvaluatableProfile<'g>,
684    ) -> TestPriorityQueue<'g> {
685        TestPriorityQueue::new(self, profile)
686    }
687
688    /// Outputs this list as a string with the given format.
689    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    // ---
696    // Helper methods
697    // ---
698
699    /// Creates an empty test list.
700    ///
701    /// This is primarily for use in tests where a placeholder test list is needed.
702    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        // On macOS, these are the defaults when DYLD_FALLBACK_LIBRARY_PATH isn't set or set to an
732        // empty string. (This is relevant if nextest is invoked as its own process and not
733        // a Cargo subcommand.)
734        //
735        // This copies the logic from
736        // https://cs.github.com/rust-lang/cargo/blob/7d289b171183578d45dcabc56db6db44b9accbff/src/cargo/core/compiler/compilation.rs#L292.
737        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            // Note that libtest prints out:
774            // * just ignored tests if --ignored is passed in
775            // * all tests, both ignored and non-ignored, if --ignored is not passed in
776            // Adding ignored tests after non-ignored ones makes everything resolve correctly.
777            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    /// Applies partitioning to the test suites as a post-filtering step.
802    ///
803    /// This is called after all other filtering (name, expression, ignored) has
804    /// been applied. Partitioning operates on the set of tests that matched all
805    /// other filters, distributing them across shards.
806    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    /// Applies per-binary partitioning: each binary gets its own independent
825    /// partitioner instance, matching the old inline behavior.
826    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            // Non-ignored and ignored tests get independent partitioner state.
836            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    /// Applies cross-binary partitioning: a single partitioner instance spans
845    /// all binaries, producing even shard sizes regardless of how tests are
846    /// distributed across binaries.
847    fn apply_cross_binary_partitioning(
848        rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
849        partitioner_builder: &PartitionerBuilder,
850    ) {
851        // Pass 1: non-ignored tests across all binaries.
852        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        // Pass 2: ignored tests across all binaries.
861        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    /// Parses the output of --list --message-format terse and returns a sorted list.
871    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    /// Writes this test list out in a human-friendly format.
881    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    /// Writes this test list out in a human-friendly format with the given filter.
891    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            // Skip this binary if there are no tests within it that will be run, and this isn't
923            // verbose output.
924            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                                    // Skip printing this test entirely if it isn't a match.
973                                }
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    /// Writes this test list out in a one-line-per-test format.
989    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                        // Skip tests that don't match the filter (unless verbose).
1006                        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                    // Skip binaries that were not listed.
1032                }
1033            }
1034        }
1035
1036        Ok(())
1037    }
1038}
1039
1040/// Applies a partitioner to all test cases with the given ignored status.
1041fn 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
1053/// Applies a partitioner to a single test case.
1054///
1055/// - If the test currently matches, the partitioner decides whether to keep or exclude it.
1056/// - If the test is `RerunAlreadyPassed`, the partitioner counts it (to maintain stable bucketing)
1057///   but preserves its status.
1058/// - All other mismatch reasons mean the test was already filtered out and should not be counted by
1059///   the partitioner.
1060fn 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            // Count the test to maintain consistent bucketing, but don't change its status.
1073            let _ = partitioner.test_matches(test_case.name.as_str());
1074        }
1075        FilterMatch::Mismatch { .. } => {
1076            // Already filtered out by another criterion; don't count it.
1077        }
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    // The output is in the form:
1086    // <test name>: test
1087    // <test name>: test
1088    // ...
1089
1090    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
1107/// Profile implementation for test lists.
1108pub trait ListProfile {
1109    /// Returns the evaluation context.
1110    fn filterset_ecx(&self) -> EvalContext<'_>;
1111
1112    /// Returns list-time settings for a test binary.
1113    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
1126/// A test list that has been sorted and has had priorities applied to it.
1127pub 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        // Note: this is a stable sort so that tests with the same priority are
1142        // sorted by what `iter_tests` produced.
1143        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/// A test instance, along with computed settings from a profile.
1159///
1160/// Returned from [`TestPriorityQueue`].
1161#[derive(Debug)]
1162pub struct TestInstanceWithSettings<'a> {
1163    /// The test instance.
1164    pub instance: TestInstance<'a>,
1165
1166    /// The settings for this test.
1167    pub settings: TestSettings<'a>,
1168}
1169
1170/// A suite of tests within a single Rust test binary.
1171///
1172/// This is a representation of [`nextest_metadata::RustTestSuiteSummary`] used internally by the runner.
1173#[derive(Clone, Debug, Eq, PartialEq)]
1174pub struct RustTestSuite<'g> {
1175    /// A unique identifier for this binary.
1176    pub binary_id: RustBinaryId,
1177
1178    /// The path to the binary.
1179    pub binary_path: Utf8PathBuf,
1180
1181    /// Package metadata.
1182    pub package: PackageMetadata<'g>,
1183
1184    /// The unique binary name defined in `Cargo.toml` or inferred by the filename.
1185    pub binary_name: String,
1186
1187    /// The kind of Rust test binary this is.
1188    pub kind: RustTestBinaryKind,
1189
1190    /// The working directory that this test binary will be executed in. If None, the current directory
1191    /// will not be changed.
1192    pub cwd: Utf8PathBuf,
1193
1194    /// The platform the test suite is for (host or target).
1195    pub build_platform: BuildPlatform,
1196
1197    /// Non-test binaries corresponding to this test suite (name, path).
1198    pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
1199
1200    /// Test suite status and test case names.
1201    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    /// Run this binary with and without --ignored and get the corresponding outputs.
1219    async fn exec(
1220        &self,
1221        lctx: &LocalExecuteContext<'_>,
1222        list_settings: &ListSettings<'_>,
1223        target_runner: &TargetRunner,
1224    ) -> Result<(String, String), CreateTestListError> {
1225        // This error situation has been known to happen with reused builds. It produces
1226        // a really terrible and confusing "file not found" message if allowed to prceed.
1227        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, // Interceptors are not used during the test list phase.
1275        );
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/// Serializable information about the status of and test cases within a test suite.
1306///
1307/// Part of a [`RustTestSuiteSummary`].
1308#[derive(Clone, Debug, Eq, PartialEq)]
1309pub enum RustTestSuiteStatus {
1310    /// The test suite was executed with `--list` and the list of test cases was obtained.
1311    Listed {
1312        /// The test cases contained within this test suite.
1313        test_cases: DebugIgnore<IdOrdMap<RustTestCase>>,
1314    },
1315
1316    /// The test suite was not executed.
1317    Skipped {
1318        /// The reason why the test suite was skipped.
1319        reason: BinaryMismatchReason,
1320    },
1321}
1322
1323static EMPTY_TEST_CASE_MAP: IdOrdMap<RustTestCase> = IdOrdMap::new();
1324
1325impl RustTestSuiteStatus {
1326    /// Returns the number of test cases within this suite.
1327    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    /// Returns a test case by name, or `None` if the suite was skipped or the test doesn't exist.
1335    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    /// Returns the list of test cases within this suite.
1343    pub fn test_cases(&self) -> impl Iterator<Item = &RustTestCase> + '_ {
1344        match self {
1345            RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
1346            RustTestSuiteStatus::Skipped { .. } => {
1347                // Return an empty test case.
1348                EMPTY_TEST_CASE_MAP.iter()
1349            }
1350        }
1351    }
1352
1353    /// Converts this status to its serializable form.
1354    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/// A single test case within a test suite.
1383#[derive(Clone, Debug, Eq, PartialEq)]
1384pub struct RustTestCase {
1385    /// The name of the test.
1386    pub name: TestCaseName,
1387
1388    /// Information about the test.
1389    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/// Represents a single test with its associated binary.
1401#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1402pub struct TestInstance<'a> {
1403    /// The name of the test.
1404    pub name: &'a TestCaseName,
1405
1406    /// Information about the test suite.
1407    pub suite_info: &'a RustTestSuite<'a>,
1408
1409    /// Information about the test.
1410    pub test_info: &'a RustTestCaseSummary,
1411}
1412
1413impl<'a> TestInstance<'a> {
1414    /// Creates a new `TestInstance`.
1415    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    /// Return an identifier for test instances, including being able to sort
1424    /// them.
1425    #[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    /// Returns the corresponding [`TestQuery`] for this `TestInstance`.
1434    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    /// Creates the command for this test instance.
1448    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        // TODO: non-rust tests
1457        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        // Apply the wrapper script if it's enabled.
1546        if let Some(wrapper) = wrapper_script {
1547            match wrapper.target_runner {
1548                WrapperScriptTargetRunner::Ignore => {
1549                    // Ignore the platform runner.
1550                    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                    // Platform runner goes first.
1556                    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                    // Wrapper script goes first.
1566                    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                        // Target runner overrides wrapper: wrapper's command
1577                        // and env are not used.
1578                        self.push(runner.binary());
1579                        self.extend(runner.args());
1580                    } else {
1581                        // No target runner: fall back to wrapper.
1582                        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 no wrapper script is enabled, use the platform runner.
1590            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/// A key for identifying and sorting test instances.
1622///
1623/// Returned by [`TestInstance::id`].
1624#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize)]
1625pub struct TestInstanceId<'a> {
1626    /// The binary ID.
1627    pub binary_id: &'a RustBinaryId,
1628
1629    /// The name of the test.
1630    pub test_name: &'a TestCaseName,
1631}
1632
1633impl TestInstanceId<'_> {
1634    /// Return the attempt ID corresponding to this test instance.
1635    ///
1636    /// This string uniquely identifies a single test attempt.
1637    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/// An owned version of [`TestInstanceId`].
1664#[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    /// The binary ID.
1669    pub binary_id: RustBinaryId,
1670
1671    /// The name of the test.
1672    #[serde(rename = "name")]
1673    pub test_name: TestCaseName,
1674}
1675
1676impl OwnedTestInstanceId {
1677    /// Borrow this as a [`TestInstanceId`].
1678    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    /// Convert this to an owned version.
1688    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
1696/// Trait to allow retrieving data from a set of [`OwnedTestInstanceId`] using a
1697/// [`TestInstanceId`].
1698///
1699/// This is an implementation of the [borrow-complex-key-example
1700/// pattern](https://github.com/sunshowers-code/borrow-complex-key-example).
1701pub trait TestInstanceIdKey {
1702    /// Converts self to a [`TestInstanceId`].
1703    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/// Context required for test execution.
1754#[derive(Clone, Debug)]
1755pub struct TestExecuteContext<'a> {
1756    /// The name of the profile.
1757    pub profile_name: &'a str,
1758
1759    /// Double-spawn info.
1760    pub double_spawn: &'a DoubleSpawnInfo,
1761
1762    /// Target runner.
1763    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        // Lines ending in ': benchmark' (output by the default Rust bencher) should be skipped.
1792        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            // Test against the platform() predicate because this is the most important one here.
1810            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                // Test an unavailable libdir.
1861                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        // Check that the expected outputs are valid.
1971        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        // Test with no wrappers
2176        {
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        // Test with platform runner only
2185        {
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        // Test wrapper with ignore target runner
2202        {
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        // Test wrapper with around wrapper (runner first)
2236        {
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        // Test wrapper with within wrapper (wrapper first)
2272        {
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        // Test wrapper with overrides-wrapper + runner present: runner wins,
2306        // wrapper env is not applied.
2307        {
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        // Test wrapper with overrides-wrapper + no runner: wrapper is used as
2341        // fallback, env is applied.
2342        {
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        // Test wrapper with args
2372        {
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        // Test platform runner with args
2398        {
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        // Test wrapper with ScriptCommandRelativeTo::WorkspaceRoot
2420        {
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        // Test wrapper with ScriptCommandRelativeTo::Target
2457        {
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        // Valid: tests only.
2498        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        // Valid: benchmarks only.
2509        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        // Valid: mixed tests and benchmarks.
2519        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        // Valid: special characters.
2531        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        // Valid: empty input.
2542        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        // Invalid: wrong suffix.
2549        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        // Invalid: missing suffix.
2555        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        // Invalid: partial valid (stops at first error).
2561        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        // Invalid: control character.
2571        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 to verify that the `Borrow<dyn TestInstanceIdKey>` implementation for
2582    // `OwnedTestInstanceId` is consistent with Eq, Ord, and Hash.
2583    #[proptest]
2584    fn test_instance_id_key_borrow_consistency(
2585        owned1: OwnedTestInstanceId,
2586        owned2: OwnedTestInstanceId,
2587    ) {
2588        // Create borrowed trait object references.
2589        let borrowed1: &dyn TestInstanceIdKey = &owned1;
2590        let borrowed2: &dyn TestInstanceIdKey = &owned2;
2591
2592        // Verify Eq consistency: owned equality must match borrowed equality.
2593        assert_eq!(
2594            owned1 == owned2,
2595            borrowed1 == borrowed2,
2596            "Eq must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2597        );
2598
2599        // Verify PartialOrd consistency.
2600        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        // Verify Ord consistency.
2607        assert_eq!(
2608            owned1.cmp(&owned2),
2609            borrowed1.cmp(borrowed2),
2610            "Ord must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2611        );
2612
2613        // Verify Hash consistency.
2614        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}