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::{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            let binary_path = path_mapper.map_binary(binary.path.clone());
126            let cwd = path_mapper.map_cwd(cwd);
127
128            // Non-test binaries are only exposed to integration tests and benchmarks.
129            let non_test_binaries = if binary.kind == RustTestBinaryKind::TEST
130                || binary.kind == RustTestBinaryKind::BENCH
131            {
132                // Note we must use the TestListState rust_build_meta here to ensure we get remapped
133                // paths.
134                match rust_build_meta.non_test_binaries.get(package_id.repr()) {
135                    Some(binaries) => binaries
136                        .iter()
137                        .filter(|binary| {
138                            // Only expose BIN_EXE non-test files.
139                            binary.kind == RustNonTestBinaryKind::BIN_EXE
140                        })
141                        .map(|binary| {
142                            // Convert relative paths to absolute ones by joining with the target directory.
143                            let abs_path = rust_build_meta.target_directory.join(&binary.path);
144                            (binary.name.clone(), abs_path)
145                        })
146                        .collect(),
147                    None => BTreeSet::new(),
148                }
149            } else {
150                BTreeSet::new()
151            };
152
153            binaries.push(RustTestArtifact {
154                binary_id: binary.id.clone(),
155                package,
156                binary_path,
157                binary_name: binary.name.clone(),
158                kind: binary.kind.clone(),
159                cwd,
160                non_test_binaries,
161                build_platform: binary.build_platform,
162            })
163        }
164
165        Ok(binaries)
166    }
167
168    /// Returns a [`BinaryQuery`] corresponding to this test artifact.
169    pub fn to_binary_query(&self) -> BinaryQuery<'_> {
170        BinaryQuery {
171            package_id: self.package.id(),
172            binary_id: &self.binary_id,
173            kind: &self.kind,
174            binary_name: &self.binary_name,
175            platform: convert_build_platform(self.build_platform),
176        }
177    }
178
179    // ---
180    // Helper methods
181    // ---
182    fn into_test_suite(self, status: RustTestSuiteStatus) -> RustTestSuite<'g> {
183        let Self {
184            binary_id,
185            package,
186            binary_path,
187            binary_name,
188            kind,
189            non_test_binaries,
190            cwd,
191            build_platform,
192        } = self;
193
194        RustTestSuite {
195            binary_id,
196            binary_path,
197            package,
198            binary_name,
199            kind,
200            non_test_binaries,
201            cwd,
202            build_platform,
203            status,
204        }
205    }
206}
207
208/// Information about skipped tests and binaries.
209#[derive(Clone, Debug, Eq, PartialEq)]
210pub struct SkipCounts {
211    /// The number of skipped tests.
212    pub skipped_tests: usize,
213
214    /// The number of skipped tests due to this being a rerun and the test was
215    /// already passing.
216    pub skipped_tests_rerun: usize,
217
218    /// The number of tests skipped because they are not benchmarks.
219    ///
220    /// This is used when running in benchmark mode.
221    pub skipped_tests_non_benchmark: usize,
222
223    /// The number of tests skipped due to not being in the default set.
224    pub skipped_tests_default_filter: usize,
225
226    /// The number of skipped binaries.
227    pub skipped_binaries: usize,
228
229    /// The number of binaries skipped due to not being in the default set.
230    pub skipped_binaries_default_filter: usize,
231}
232
233/// List of test instances, obtained by querying the [`RustTestArtifact`] instances generated by Cargo.
234#[derive(Clone, Debug)]
235pub struct TestList<'g> {
236    test_count: usize,
237    mode: NextestRunMode,
238    rust_build_meta: RustBuildMeta<TestListState>,
239    rust_suites: IdOrdMap<RustTestSuite<'g>>,
240    workspace_root: Utf8PathBuf,
241    env: EnvironmentMap,
242    updated_dylib_path: OsString,
243    // Computed on first access.
244    skip_counts: OnceLock<SkipCounts>,
245}
246
247impl<'g> TestList<'g> {
248    /// Creates a new test list by running the given command and applying the specified filter.
249    #[expect(clippy::too_many_arguments)]
250    pub fn new<I>(
251        ctx: &TestExecuteContext<'_>,
252        test_artifacts: I,
253        rust_build_meta: RustBuildMeta<TestListState>,
254        filter: &TestFilter,
255        partitioner_builder: Option<&PartitionerBuilder>,
256        workspace_root: Utf8PathBuf,
257        env: EnvironmentMap,
258        profile: &impl ListProfile,
259        bound: FilterBound,
260        list_threads: usize,
261    ) -> Result<Self, CreateTestListError>
262    where
263        I: IntoIterator<Item = RustTestArtifact<'g>>,
264        I::IntoIter: Send,
265    {
266        let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
267        debug!(
268            "updated {}: {}",
269            dylib_path_envvar(),
270            updated_dylib_path.to_string_lossy(),
271        );
272        let lctx = LocalExecuteContext {
273            phase: TestCommandPhase::List,
274            // Note: this is the remapped workspace root, not the original one.
275            // (We really should have newtypes for this.)
276            workspace_root: &workspace_root,
277            rust_build_meta: &rust_build_meta,
278            double_spawn: ctx.double_spawn,
279            dylib_path: &updated_dylib_path,
280            profile_name: ctx.profile_name,
281            env: &env,
282        };
283
284        let ecx = profile.filterset_ecx();
285
286        let runtime = Runtime::new().map_err(CreateTestListError::TokioRuntimeCreate)?;
287
288        let stream = futures::stream::iter(test_artifacts).map(|test_binary| {
289            async {
290                let binary_query = test_binary.to_binary_query();
291                let binary_match = filter.filter_binary_match(&test_binary, &ecx, bound);
292                match binary_match {
293                    FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
294                        debug!(
295                            "executing test binary to obtain test list \
296                            (match result is {binary_match:?}): {}",
297                            test_binary.binary_id,
298                        );
299                        // Run the binary to obtain the test list.
300                        let list_settings = profile.list_settings_for(&binary_query);
301                        let (non_ignored, ignored) = test_binary
302                            .exec(&lctx, &list_settings, ctx.target_runner)
303                            .await?;
304                        let info = Self::process_output(
305                            test_binary,
306                            filter,
307                            &ecx,
308                            bound,
309                            non_ignored.as_str(),
310                            ignored.as_str(),
311                        )?;
312                        Ok::<_, CreateTestListError>(info)
313                    }
314                    FilterBinaryMatch::Mismatch { reason } => {
315                        debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
316                        Ok(Self::process_skipped(test_binary, reason))
317                    }
318                }
319            }
320        });
321        let fut = stream.buffer_unordered(list_threads).try_collect();
322
323        let mut rust_suites: IdOrdMap<_> = runtime.block_on(fut)?;
324
325        // Ensure that the runtime doesn't stay hanging even if a custom test framework misbehaves
326        // (can be an issue on Windows).
327        runtime.shutdown_background();
328
329        Self::apply_partitioning(&mut rust_suites, partitioner_builder);
330
331        let test_count = rust_suites
332            .iter()
333            .map(|suite| suite.status.test_count())
334            .sum();
335
336        Ok(Self {
337            rust_suites,
338            mode: filter.mode(),
339            workspace_root,
340            env,
341            rust_build_meta,
342            updated_dylib_path,
343            test_count,
344            skip_counts: OnceLock::new(),
345        })
346    }
347
348    /// Creates a new test list with the given binary names and outputs.
349    #[cfg(test)]
350    #[expect(clippy::too_many_arguments)]
351    pub(crate) fn new_with_outputs(
352        test_bin_outputs: impl IntoIterator<
353            Item = (RustTestArtifact<'g>, impl AsRef<str>, impl AsRef<str>),
354        >,
355        workspace_root: Utf8PathBuf,
356        rust_build_meta: RustBuildMeta<TestListState>,
357        filter: &TestFilter,
358        partitioner_builder: Option<&PartitionerBuilder>,
359        env: EnvironmentMap,
360        ecx: &EvalContext<'_>,
361        bound: FilterBound,
362    ) -> Result<Self, CreateTestListError> {
363        let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
364
365        let mut rust_suites = test_bin_outputs
366            .into_iter()
367            .map(|(test_binary, non_ignored, ignored)| {
368                let binary_match = filter.filter_binary_match(&test_binary, ecx, bound);
369                match binary_match {
370                    FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
371                        debug!(
372                            "processing output for binary \
373                            (match result is {binary_match:?}): {}",
374                            test_binary.binary_id,
375                        );
376                        let info = Self::process_output(
377                            test_binary,
378                            filter,
379                            ecx,
380                            bound,
381                            non_ignored.as_ref(),
382                            ignored.as_ref(),
383                        )?;
384                        Ok(info)
385                    }
386                    FilterBinaryMatch::Mismatch { reason } => {
387                        debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
388                        Ok(Self::process_skipped(test_binary, reason))
389                    }
390                }
391            })
392            .collect::<Result<IdOrdMap<_>, _>>()?;
393
394        Self::apply_partitioning(&mut rust_suites, partitioner_builder);
395
396        let test_count = rust_suites
397            .iter()
398            .map(|suite| suite.status.test_count())
399            .sum();
400
401        Ok(Self {
402            rust_suites,
403            mode: filter.mode(),
404            workspace_root,
405            env,
406            rust_build_meta,
407            updated_dylib_path,
408            test_count,
409            skip_counts: OnceLock::new(),
410        })
411    }
412
413    /// Reconstructs a TestList from archived summary data.
414    ///
415    /// This is used during replay to reconstruct a TestList without
416    /// executing test binaries. The reconstructed TestList provides the
417    /// data needed for display through the reporter infrastructure.
418    pub fn from_summary(
419        graph: &'g PackageGraph,
420        summary: &TestListSummary,
421        mode: NextestRunMode,
422    ) -> Result<Self, TestListFromSummaryError> {
423        // Build RustBuildMeta from summary.
424        let rust_build_meta = RustBuildMeta::from_summary(summary.rust_build_meta.clone())
425            .map_err(TestListFromSummaryError::RustBuildMeta)?;
426
427        // Get the workspace root from the graph.
428        let workspace_root = graph.workspace().root().to_path_buf();
429
430        // Construct an empty environment map - we don't need it for replay.
431        let env = EnvironmentMap::empty();
432
433        // For replay, we don't need the actual dylib path since we're not executing tests.
434        let updated_dylib_path = OsString::new();
435
436        // Build test suites from the summary.
437        let mut rust_suites = IdOrdMap::new();
438        let mut test_count = 0;
439
440        for (binary_id, suite_summary) in &summary.rust_suites {
441            // Look up the package in the graph by package_id.
442            let package_id = PackageId::new(suite_summary.binary.package_id.clone());
443            let package = graph.metadata(&package_id).map_err(|_| {
444                TestListFromSummaryError::PackageNotFound {
445                    name: suite_summary.package_name.clone(),
446                    package_id: suite_summary.binary.package_id.clone(),
447                }
448            })?;
449
450            // Determine the status based on the summary.
451            let status = if suite_summary.status == RustTestSuiteStatusSummary::SKIPPED {
452                RustTestSuiteStatus::Skipped {
453                    reason: BinaryMismatchReason::Expression,
454                }
455            } else if suite_summary.status == RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER {
456                RustTestSuiteStatus::Skipped {
457                    reason: BinaryMismatchReason::DefaultSet,
458                }
459            } else {
460                // Build test cases from the summary (only for listed suites).
461                let test_cases: IdOrdMap<RustTestCase> = suite_summary
462                    .test_cases
463                    .iter()
464                    .map(|(name, info)| RustTestCase {
465                        name: name.clone(),
466                        test_info: info.clone(),
467                    })
468                    .collect();
469
470                test_count += test_cases.len();
471
472                // LISTED or any other status should be treated as listed.
473                RustTestSuiteStatus::Listed {
474                    test_cases: DebugIgnore(test_cases),
475                }
476            };
477
478            let suite = RustTestSuite {
479                binary_id: binary_id.clone(),
480                binary_path: suite_summary.binary.binary_path.clone(),
481                package,
482                binary_name: suite_summary.binary.binary_name.clone(),
483                kind: suite_summary.binary.kind.clone(),
484                non_test_binaries: BTreeSet::new(), // Not stored in summary.
485                cwd: suite_summary.cwd.clone(),
486                build_platform: suite_summary.binary.build_platform,
487                status,
488            };
489
490            let _ = rust_suites.insert_unique(suite);
491        }
492
493        Ok(Self {
494            rust_suites,
495            mode,
496            workspace_root,
497            env,
498            rust_build_meta,
499            updated_dylib_path,
500            test_count,
501            skip_counts: OnceLock::new(),
502        })
503    }
504
505    /// Returns the total number of tests across all binaries.
506    pub fn test_count(&self) -> usize {
507        self.test_count
508    }
509
510    /// Returns the mode nextest is running in.
511    pub fn mode(&self) -> NextestRunMode {
512        self.mode
513    }
514
515    /// Returns the Rust build-related metadata for this test list.
516    pub fn rust_build_meta(&self) -> &RustBuildMeta<TestListState> {
517        &self.rust_build_meta
518    }
519
520    /// Returns the total number of skipped tests.
521    pub fn skip_counts(&self) -> &SkipCounts {
522        self.skip_counts.get_or_init(|| {
523            let mut skipped_tests_rerun = 0;
524            let mut skipped_tests_non_benchmark = 0;
525            let mut skipped_tests_default_filter = 0;
526            let skipped_tests = self
527                .iter_tests()
528                .filter(|instance| match instance.test_info.filter_match {
529                    FilterMatch::Mismatch {
530                        reason: MismatchReason::RerunAlreadyPassed,
531                    } => {
532                        skipped_tests_rerun += 1;
533                        true
534                    }
535                    FilterMatch::Mismatch {
536                        reason: MismatchReason::NotBenchmark,
537                    } => {
538                        skipped_tests_non_benchmark += 1;
539                        true
540                    }
541                    FilterMatch::Mismatch {
542                        reason: MismatchReason::DefaultFilter,
543                    } => {
544                        skipped_tests_default_filter += 1;
545                        true
546                    }
547                    FilterMatch::Mismatch { .. } => true,
548                    FilterMatch::Matches => false,
549                })
550                .count();
551
552            let mut skipped_binaries_default_filter = 0;
553            let skipped_binaries = self
554                .rust_suites
555                .iter()
556                .filter(|suite| match suite.status {
557                    RustTestSuiteStatus::Skipped {
558                        reason: BinaryMismatchReason::DefaultSet,
559                    } => {
560                        skipped_binaries_default_filter += 1;
561                        true
562                    }
563                    RustTestSuiteStatus::Skipped { .. } => true,
564                    RustTestSuiteStatus::Listed { .. } => false,
565                })
566                .count();
567
568            SkipCounts {
569                skipped_tests,
570                skipped_tests_rerun,
571                skipped_tests_non_benchmark,
572                skipped_tests_default_filter,
573                skipped_binaries,
574                skipped_binaries_default_filter,
575            }
576        })
577    }
578
579    /// Returns the total number of tests that aren't skipped.
580    ///
581    /// It is always the case that `run_count + skip_count == test_count`.
582    pub fn run_count(&self) -> usize {
583        self.test_count - self.skip_counts().skipped_tests
584    }
585
586    /// Returns the total number of binaries that contain tests.
587    pub fn binary_count(&self) -> usize {
588        self.rust_suites.len()
589    }
590
591    /// Returns the total number of binaries that were listed (not skipped).
592    pub fn listed_binary_count(&self) -> usize {
593        self.binary_count() - self.skip_counts().skipped_binaries
594    }
595
596    /// Returns the mapped workspace root.
597    pub fn workspace_root(&self) -> &Utf8Path {
598        &self.workspace_root
599    }
600
601    /// Returns the environment variables to be used when running tests.
602    pub fn cargo_env(&self) -> &EnvironmentMap {
603        &self.env
604    }
605
606    /// Returns the updated dynamic library path used for tests.
607    pub fn updated_dylib_path(&self) -> &OsStr {
608        &self.updated_dylib_path
609    }
610
611    /// Constructs a serializble summary for this test list.
612    pub fn to_summary(&self) -> TestListSummary {
613        let rust_suites = self
614            .rust_suites
615            .iter()
616            .map(|test_suite| {
617                let (status, test_cases) = test_suite.status.to_summary();
618                let testsuite = RustTestSuiteSummary {
619                    package_name: test_suite.package.name().to_owned(),
620                    binary: RustTestBinarySummary {
621                        binary_name: test_suite.binary_name.clone(),
622                        package_id: test_suite.package.id().repr().to_owned(),
623                        kind: test_suite.kind.clone(),
624                        binary_path: test_suite.binary_path.clone(),
625                        binary_id: test_suite.binary_id.clone(),
626                        build_platform: test_suite.build_platform,
627                    },
628                    cwd: test_suite.cwd.clone(),
629                    status,
630                    test_cases,
631                };
632                (test_suite.binary_id.clone(), testsuite)
633            })
634            .collect();
635        let mut summary = TestListSummary::new(self.rust_build_meta.to_summary());
636        summary.test_count = self.test_count;
637        summary.rust_suites = rust_suites;
638        summary
639    }
640
641    /// Outputs this list to the given writer.
642    pub fn write(
643        &self,
644        output_format: OutputFormat,
645        writer: &mut dyn WriteStr,
646        colorize: bool,
647    ) -> Result<(), WriteTestListError> {
648        match output_format {
649            OutputFormat::Human { verbose } => self
650                .write_human(writer, verbose, colorize)
651                .map_err(WriteTestListError::Io),
652            OutputFormat::Oneline { verbose } => self
653                .write_oneline(writer, verbose, colorize)
654                .map_err(WriteTestListError::Io),
655            OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
656        }
657    }
658
659    /// Iterates over all the test suites.
660    pub fn iter(&self) -> impl Iterator<Item = &RustTestSuite<'_>> + '_ {
661        self.rust_suites.iter()
662    }
663
664    /// Looks up a test suite by binary ID.
665    pub fn get_suite(&self, binary_id: &RustBinaryId) -> Option<&RustTestSuite<'_>> {
666        self.rust_suites.get(binary_id)
667    }
668
669    /// Iterates over the list of tests, returning the path and test name.
670    pub fn iter_tests(&self) -> impl Iterator<Item = TestInstance<'_>> + '_ {
671        self.rust_suites.iter().flat_map(|test_suite| {
672            test_suite
673                .status
674                .test_cases()
675                .map(move |case| TestInstance::new(case, test_suite))
676        })
677    }
678
679    /// Produces a priority queue of tests based on the given profile.
680    pub fn to_priority_queue(
681        &'g self,
682        profile: &'g EvaluatableProfile<'g>,
683    ) -> TestPriorityQueue<'g> {
684        TestPriorityQueue::new(self, profile)
685    }
686
687    /// Outputs this list as a string with the given format.
688    pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
689        let mut s = String::with_capacity(1024);
690        self.write(output_format, &mut s, false)?;
691        Ok(s)
692    }
693
694    // ---
695    // Helper methods
696    // ---
697
698    /// Creates an empty test list.
699    ///
700    /// This is primarily for use in tests where a placeholder test list is needed.
701    pub fn empty() -> Self {
702        Self {
703            test_count: 0,
704            mode: NextestRunMode::Test,
705            workspace_root: Utf8PathBuf::new(),
706            rust_build_meta: RustBuildMeta::empty(),
707            env: EnvironmentMap::empty(),
708            updated_dylib_path: OsString::new(),
709            rust_suites: IdOrdMap::new(),
710            skip_counts: OnceLock::new(),
711        }
712    }
713
714    pub(crate) fn create_dylib_path(
715        rust_build_meta: &RustBuildMeta<TestListState>,
716    ) -> Result<OsString, CreateTestListError> {
717        let dylib_path = dylib_path();
718        let dylib_path_is_empty = dylib_path.is_empty();
719        let new_paths = rust_build_meta.dylib_paths();
720
721        let mut updated_dylib_path: Vec<PathBuf> =
722            Vec::with_capacity(dylib_path.len() + new_paths.len());
723        updated_dylib_path.extend(
724            new_paths
725                .iter()
726                .map(|path| path.clone().into_std_path_buf()),
727        );
728        updated_dylib_path.extend(dylib_path);
729
730        // On macOS, these are the defaults when DYLD_FALLBACK_LIBRARY_PATH isn't set or set to an
731        // empty string. (This is relevant if nextest is invoked as its own process and not
732        // a Cargo subcommand.)
733        //
734        // This copies the logic from
735        // https://cs.github.com/rust-lang/cargo/blob/7d289b171183578d45dcabc56db6db44b9accbff/src/cargo/core/compiler/compilation.rs#L292.
736        if cfg!(target_os = "macos") && dylib_path_is_empty {
737            if let Some(home) = home::home_dir() {
738                updated_dylib_path.push(home.join("lib"));
739            }
740            updated_dylib_path.push("/usr/local/lib".into());
741            updated_dylib_path.push("/usr/lib".into());
742        }
743
744        std::env::join_paths(updated_dylib_path)
745            .map_err(move |error| CreateTestListError::dylib_join_paths(new_paths, error))
746    }
747
748    fn process_output(
749        test_binary: RustTestArtifact<'g>,
750        filter: &TestFilter,
751        ecx: &EvalContext<'_>,
752        bound: FilterBound,
753        non_ignored: impl AsRef<str>,
754        ignored: impl AsRef<str>,
755    ) -> Result<RustTestSuite<'g>, CreateTestListError> {
756        let mut test_cases = IdOrdMap::new();
757
758        for (test_name, kind) in Self::parse(&test_binary.binary_id, non_ignored.as_ref())? {
759            let name = TestCaseName::new(test_name);
760            let filter_match = filter.filter_match(&test_binary, &name, &kind, ecx, bound, false);
761            test_cases.insert_overwrite(RustTestCase {
762                name,
763                test_info: RustTestCaseSummary {
764                    kind: Some(kind),
765                    ignored: false,
766                    filter_match,
767                },
768            });
769        }
770
771        for (test_name, kind) in Self::parse(&test_binary.binary_id, ignored.as_ref())? {
772            // Note that libtest prints out:
773            // * just ignored tests if --ignored is passed in
774            // * all tests, both ignored and non-ignored, if --ignored is not passed in
775            // Adding ignored tests after non-ignored ones makes everything resolve correctly.
776            let name = TestCaseName::new(test_name);
777            let filter_match = filter.filter_match(&test_binary, &name, &kind, ecx, bound, true);
778            test_cases.insert_overwrite(RustTestCase {
779                name,
780                test_info: RustTestCaseSummary {
781                    kind: Some(kind),
782                    ignored: true,
783                    filter_match,
784                },
785            });
786        }
787
788        Ok(test_binary.into_test_suite(RustTestSuiteStatus::Listed {
789            test_cases: test_cases.into(),
790        }))
791    }
792
793    fn process_skipped(
794        test_binary: RustTestArtifact<'g>,
795        reason: BinaryMismatchReason,
796    ) -> RustTestSuite<'g> {
797        test_binary.into_test_suite(RustTestSuiteStatus::Skipped { reason })
798    }
799
800    /// Applies partitioning to the test suites as a post-filtering step.
801    ///
802    /// This is called after all other filtering (name, expression, ignored) has
803    /// been applied. Partitioning operates on the set of tests that matched all
804    /// other filters, distributing them across shards.
805    fn apply_partitioning(
806        rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
807        partitioner_builder: Option<&PartitionerBuilder>,
808    ) {
809        let Some(partitioner_builder) = partitioner_builder else {
810            return;
811        };
812
813        match partitioner_builder.scope() {
814            PartitionerScope::PerBinary => {
815                Self::apply_per_binary_partitioning(rust_suites, partitioner_builder);
816            }
817            PartitionerScope::CrossBinary => {
818                Self::apply_cross_binary_partitioning(rust_suites, partitioner_builder);
819            }
820        }
821    }
822
823    /// Applies per-binary partitioning: each binary gets its own independent
824    /// partitioner instance, matching the old inline behavior.
825    fn apply_per_binary_partitioning(
826        rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
827        partitioner_builder: &PartitionerBuilder,
828    ) {
829        for mut suite in rust_suites.iter_mut() {
830            let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
831                continue;
832            };
833
834            // Non-ignored and ignored tests get independent partitioner state.
835            let mut non_ignored_partitioner = partitioner_builder.build();
836            apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
837
838            let mut ignored_partitioner = partitioner_builder.build();
839            apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
840        }
841    }
842
843    /// Applies cross-binary partitioning: a single partitioner instance spans
844    /// all binaries, producing even shard sizes regardless of how tests are
845    /// distributed across binaries.
846    fn apply_cross_binary_partitioning(
847        rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
848        partitioner_builder: &PartitionerBuilder,
849    ) {
850        // Pass 1: non-ignored tests across all binaries.
851        let mut non_ignored_partitioner = partitioner_builder.build();
852        for mut suite in rust_suites.iter_mut() {
853            let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
854                continue;
855            };
856            apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
857        }
858
859        // Pass 2: ignored tests across all binaries.
860        let mut ignored_partitioner = partitioner_builder.build();
861        for mut suite in rust_suites.iter_mut() {
862            let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
863                continue;
864            };
865            apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
866        }
867    }
868
869    /// Parses the output of --list --message-format terse and returns a sorted list.
870    fn parse<'a>(
871        binary_id: &'a RustBinaryId,
872        list_output: &'a str,
873    ) -> Result<Vec<(&'a str, RustTestKind)>, CreateTestListError> {
874        let mut list = parse_list_lines(binary_id, list_output).collect::<Result<Vec<_>, _>>()?;
875        list.sort_unstable();
876        Ok(list)
877    }
878
879    /// Writes this test list out in a human-friendly format.
880    pub fn write_human(
881        &self,
882        writer: &mut dyn WriteStr,
883        verbose: bool,
884        colorize: bool,
885    ) -> io::Result<()> {
886        self.write_human_impl(None, writer, verbose, colorize)
887    }
888
889    /// Writes this test list out in a human-friendly format with the given filter.
890    pub(crate) fn write_human_with_filter(
891        &self,
892        filter: &TestListDisplayFilter<'_>,
893        writer: &mut dyn WriteStr,
894        verbose: bool,
895        colorize: bool,
896    ) -> io::Result<()> {
897        self.write_human_impl(Some(filter), writer, verbose, colorize)
898    }
899
900    fn write_human_impl(
901        &self,
902        filter: Option<&TestListDisplayFilter<'_>>,
903        mut writer: &mut dyn WriteStr,
904        verbose: bool,
905        colorize: bool,
906    ) -> io::Result<()> {
907        let mut styles = Styles::default();
908        if colorize {
909            styles.colorize();
910        }
911
912        for info in &self.rust_suites {
913            let matcher = match filter {
914                Some(filter) => match filter.matcher_for(&info.binary_id) {
915                    Some(matcher) => matcher,
916                    None => continue,
917                },
918                None => DisplayFilterMatcher::All,
919            };
920
921            // Skip this binary if there are no tests within it that will be run, and this isn't
922            // verbose output.
923            if !verbose
924                && info
925                    .status
926                    .test_cases()
927                    .all(|case| !case.test_info.filter_match.is_match())
928            {
929                continue;
930            }
931
932            writeln!(writer, "{}:", info.binary_id.style(styles.binary_id))?;
933            if verbose {
934                writeln!(
935                    writer,
936                    "  {} {}",
937                    "bin:".style(styles.field),
938                    info.binary_path
939                )?;
940                writeln!(writer, "  {} {}", "cwd:".style(styles.field), info.cwd)?;
941                writeln!(
942                    writer,
943                    "  {} {}",
944                    "build platform:".style(styles.field),
945                    info.build_platform,
946                )?;
947            }
948
949            let mut indented = indented(writer).with_str("    ");
950
951            match &info.status {
952                RustTestSuiteStatus::Listed { test_cases } => {
953                    let matching_tests: Vec<_> = test_cases
954                        .iter()
955                        .filter(|case| matcher.is_match(&case.name))
956                        .collect();
957                    if matching_tests.is_empty() {
958                        writeln!(indented, "(no tests)")?;
959                    } else {
960                        for case in matching_tests {
961                            match (verbose, case.test_info.filter_match.is_match()) {
962                                (_, true) => {
963                                    write_test_name(&case.name, &styles, &mut indented)?;
964                                    writeln!(indented)?;
965                                }
966                                (true, false) => {
967                                    write_test_name(&case.name, &styles, &mut indented)?;
968                                    writeln!(indented, " (skipped)")?;
969                                }
970                                (false, false) => {
971                                    // Skip printing this test entirely if it isn't a match.
972                                }
973                            }
974                        }
975                    }
976                }
977                RustTestSuiteStatus::Skipped { reason } => {
978                    writeln!(indented, "(test binary {reason}, skipped)")?;
979                }
980            }
981
982            writer = indented.into_inner();
983        }
984        Ok(())
985    }
986
987    /// Writes this test list out in a one-line-per-test format.
988    pub fn write_oneline(
989        &self,
990        writer: &mut dyn WriteStr,
991        verbose: bool,
992        colorize: bool,
993    ) -> io::Result<()> {
994        let mut styles = Styles::default();
995        if colorize {
996            styles.colorize();
997        }
998
999        for info in &self.rust_suites {
1000            match &info.status {
1001                RustTestSuiteStatus::Listed { test_cases } => {
1002                    for case in test_cases.iter() {
1003                        let is_match = case.test_info.filter_match.is_match();
1004                        // Skip tests that don't match the filter (unless verbose).
1005                        if !verbose && !is_match {
1006                            continue;
1007                        }
1008
1009                        write!(writer, "{} ", info.binary_id.style(styles.binary_id))?;
1010                        write_test_name(&case.name, &styles, writer)?;
1011
1012                        if verbose {
1013                            write!(
1014                                writer,
1015                                " [{}{}] [{}{}] [{}{}]{}",
1016                                "bin: ".style(styles.field),
1017                                info.binary_path,
1018                                "cwd: ".style(styles.field),
1019                                info.cwd,
1020                                "build platform: ".style(styles.field),
1021                                info.build_platform,
1022                                if is_match { "" } else { " (skipped)" },
1023                            )?;
1024                        }
1025
1026                        writeln!(writer)?;
1027                    }
1028                }
1029                RustTestSuiteStatus::Skipped { .. } => {
1030                    // Skip binaries that were not listed.
1031                }
1032            }
1033        }
1034
1035        Ok(())
1036    }
1037}
1038
1039/// Applies a partitioner to all test cases with the given ignored status.
1040fn apply_partitioner_to_tests(
1041    test_cases: &mut IdOrdMap<RustTestCase>,
1042    partitioner: &mut dyn Partitioner,
1043    ignored: bool,
1044) {
1045    for mut test_case in test_cases.iter_mut() {
1046        if test_case.test_info.ignored == ignored {
1047            apply_partition_to_test(&mut test_case, partitioner);
1048        }
1049    }
1050}
1051
1052/// Applies a partitioner to a single test case.
1053///
1054/// - If the test currently matches, the partitioner decides whether to keep or exclude it.
1055/// - If the test is `RerunAlreadyPassed`, the partitioner counts it (to maintain stable bucketing)
1056///   but preserves its status.
1057/// - All other mismatch reasons mean the test was already filtered out and should not be counted by
1058///   the partitioner.
1059fn apply_partition_to_test(test_case: &mut RustTestCase, partitioner: &mut dyn Partitioner) {
1060    match test_case.test_info.filter_match {
1061        FilterMatch::Matches => {
1062            if !partitioner.test_matches(test_case.name.as_str()) {
1063                test_case.test_info.filter_match = FilterMatch::Mismatch {
1064                    reason: MismatchReason::Partition,
1065                };
1066            }
1067        }
1068        FilterMatch::Mismatch {
1069            reason: MismatchReason::RerunAlreadyPassed,
1070        } => {
1071            // Count the test to maintain consistent bucketing, but don't change its status.
1072            let _ = partitioner.test_matches(test_case.name.as_str());
1073        }
1074        FilterMatch::Mismatch { .. } => {
1075            // Already filtered out by another criterion; don't count it.
1076        }
1077    }
1078}
1079
1080fn parse_list_lines<'a>(
1081    binary_id: &'a RustBinaryId,
1082    list_output: &'a str,
1083) -> impl Iterator<Item = Result<(&'a str, RustTestKind), CreateTestListError>> + 'a + use<'a> {
1084    // The output is in the form:
1085    // <test name>: test
1086    // <test name>: test
1087    // ...
1088
1089    list_output
1090        .lines()
1091        .map(move |line| match line.strip_suffix(": test") {
1092            Some(test_name) => Ok((test_name, RustTestKind::TEST)),
1093            None => match line.strip_suffix(": benchmark") {
1094                Some(test_name) => Ok((test_name, RustTestKind::BENCH)),
1095                None => Err(CreateTestListError::parse_line(
1096                    binary_id.clone(),
1097                    format!(
1098                        "line {line:?} did not end with the string \": test\" or \": benchmark\""
1099                    ),
1100                    list_output,
1101                )),
1102            },
1103        })
1104}
1105
1106/// Profile implementation for test lists.
1107pub trait ListProfile {
1108    /// Returns the evaluation context.
1109    fn filterset_ecx(&self) -> EvalContext<'_>;
1110
1111    /// Returns list-time settings for a test binary.
1112    fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_>;
1113}
1114
1115impl<'g> ListProfile for EvaluatableProfile<'g> {
1116    fn filterset_ecx(&self) -> EvalContext<'_> {
1117        self.filterset_ecx()
1118    }
1119
1120    fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
1121        self.list_settings_for(query)
1122    }
1123}
1124
1125/// A test list that has been sorted and has had priorities applied to it.
1126pub struct TestPriorityQueue<'a> {
1127    tests: Vec<TestInstanceWithSettings<'a>>,
1128}
1129
1130impl<'a> TestPriorityQueue<'a> {
1131    fn new(test_list: &'a TestList<'a>, profile: &'a EvaluatableProfile<'a>) -> Self {
1132        let mode = test_list.mode();
1133        let mut tests = test_list
1134            .iter_tests()
1135            .map(|instance| {
1136                let settings = profile.settings_for(mode, &instance.to_test_query());
1137                TestInstanceWithSettings { instance, settings }
1138            })
1139            .collect::<Vec<_>>();
1140        // Note: this is a stable sort so that tests with the same priority are
1141        // sorted by what `iter_tests` produced.
1142        tests.sort_by_key(|test| test.settings.priority());
1143
1144        Self { tests }
1145    }
1146}
1147
1148impl<'a> IntoIterator for TestPriorityQueue<'a> {
1149    type Item = TestInstanceWithSettings<'a>;
1150    type IntoIter = std::vec::IntoIter<Self::Item>;
1151
1152    fn into_iter(self) -> Self::IntoIter {
1153        self.tests.into_iter()
1154    }
1155}
1156
1157/// A test instance, along with computed settings from a profile.
1158///
1159/// Returned from [`TestPriorityQueue`].
1160#[derive(Debug)]
1161pub struct TestInstanceWithSettings<'a> {
1162    /// The test instance.
1163    pub instance: TestInstance<'a>,
1164
1165    /// The settings for this test.
1166    pub settings: TestSettings<'a>,
1167}
1168
1169/// A suite of tests within a single Rust test binary.
1170///
1171/// This is a representation of [`nextest_metadata::RustTestSuiteSummary`] used internally by the runner.
1172#[derive(Clone, Debug, Eq, PartialEq)]
1173pub struct RustTestSuite<'g> {
1174    /// A unique identifier for this binary.
1175    pub binary_id: RustBinaryId,
1176
1177    /// The path to the binary.
1178    pub binary_path: Utf8PathBuf,
1179
1180    /// Package metadata.
1181    pub package: PackageMetadata<'g>,
1182
1183    /// The unique binary name defined in `Cargo.toml` or inferred by the filename.
1184    pub binary_name: String,
1185
1186    /// The kind of Rust test binary this is.
1187    pub kind: RustTestBinaryKind,
1188
1189    /// The working directory that this test binary will be executed in. If None, the current directory
1190    /// will not be changed.
1191    pub cwd: Utf8PathBuf,
1192
1193    /// The platform the test suite is for (host or target).
1194    pub build_platform: BuildPlatform,
1195
1196    /// Non-test binaries corresponding to this test suite (name, path).
1197    pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
1198
1199    /// Test suite status and test case names.
1200    pub status: RustTestSuiteStatus,
1201}
1202
1203impl IdOrdItem for RustTestSuite<'_> {
1204    type Key<'a>
1205        = &'a RustBinaryId
1206    where
1207        Self: 'a;
1208
1209    fn key(&self) -> Self::Key<'_> {
1210        &self.binary_id
1211    }
1212
1213    id_upcast!();
1214}
1215
1216impl RustTestArtifact<'_> {
1217    /// Run this binary with and without --ignored and get the corresponding outputs.
1218    async fn exec(
1219        &self,
1220        lctx: &LocalExecuteContext<'_>,
1221        list_settings: &ListSettings<'_>,
1222        target_runner: &TargetRunner,
1223    ) -> Result<(String, String), CreateTestListError> {
1224        // This error situation has been known to happen with reused builds. It produces
1225        // a really terrible and confusing "file not found" message if allowed to prceed.
1226        if !self.cwd.is_dir() {
1227            return Err(CreateTestListError::CwdIsNotDir {
1228                binary_id: self.binary_id.clone(),
1229                cwd: self.cwd.clone(),
1230            });
1231        }
1232        let platform_runner = target_runner.for_build_platform(self.build_platform);
1233
1234        let non_ignored = self.exec_single(false, lctx, list_settings, platform_runner);
1235        let ignored = self.exec_single(true, lctx, list_settings, platform_runner);
1236
1237        let (non_ignored_out, ignored_out) = futures::future::join(non_ignored, ignored).await;
1238        Ok((non_ignored_out?, ignored_out?))
1239    }
1240
1241    async fn exec_single(
1242        &self,
1243        ignored: bool,
1244        lctx: &LocalExecuteContext<'_>,
1245        list_settings: &ListSettings<'_>,
1246        runner: Option<&PlatformRunner>,
1247    ) -> Result<String, CreateTestListError> {
1248        let mut cli = TestCommandCli::default();
1249        cli.apply_wrappers(
1250            list_settings.list_wrapper(),
1251            runner,
1252            lctx.workspace_root,
1253            &lctx.rust_build_meta.target_directory,
1254        );
1255        cli.push(self.binary_path.as_str());
1256
1257        cli.extend(["--list", "--format", "terse"]);
1258        if ignored {
1259            cli.push("--ignored");
1260        }
1261
1262        let cmd = TestCommand::new(
1263            lctx,
1264            cli.program
1265                .clone()
1266                .expect("at least one argument passed in")
1267                .into_owned(),
1268            &cli.args,
1269            &self.cwd,
1270            &self.package,
1271            &self.non_test_binaries,
1272            &Interceptor::None, // Interceptors are not used during the test list phase.
1273        );
1274
1275        let output =
1276            cmd.wait_with_output()
1277                .await
1278                .map_err(|error| CreateTestListError::CommandExecFail {
1279                    binary_id: self.binary_id.clone(),
1280                    command: cli.to_owned_cli(),
1281                    error,
1282                })?;
1283
1284        if output.status.success() {
1285            String::from_utf8(output.stdout).map_err(|err| CreateTestListError::CommandNonUtf8 {
1286                binary_id: self.binary_id.clone(),
1287                command: cli.to_owned_cli(),
1288                stdout: err.into_bytes(),
1289                stderr: output.stderr,
1290            })
1291        } else {
1292            Err(CreateTestListError::CommandFail {
1293                binary_id: self.binary_id.clone(),
1294                command: cli.to_owned_cli(),
1295                exit_status: output.status,
1296                stdout: output.stdout,
1297                stderr: output.stderr,
1298            })
1299        }
1300    }
1301}
1302
1303/// Serializable information about the status of and test cases within a test suite.
1304///
1305/// Part of a [`RustTestSuiteSummary`].
1306#[derive(Clone, Debug, Eq, PartialEq)]
1307pub enum RustTestSuiteStatus {
1308    /// The test suite was executed with `--list` and the list of test cases was obtained.
1309    Listed {
1310        /// The test cases contained within this test suite.
1311        test_cases: DebugIgnore<IdOrdMap<RustTestCase>>,
1312    },
1313
1314    /// The test suite was not executed.
1315    Skipped {
1316        /// The reason why the test suite was skipped.
1317        reason: BinaryMismatchReason,
1318    },
1319}
1320
1321static EMPTY_TEST_CASE_MAP: IdOrdMap<RustTestCase> = IdOrdMap::new();
1322
1323impl RustTestSuiteStatus {
1324    /// Returns the number of test cases within this suite.
1325    pub fn test_count(&self) -> usize {
1326        match self {
1327            RustTestSuiteStatus::Listed { test_cases } => test_cases.len(),
1328            RustTestSuiteStatus::Skipped { .. } => 0,
1329        }
1330    }
1331
1332    /// Returns a test case by name, or `None` if the suite was skipped or the test doesn't exist.
1333    pub fn get(&self, name: &TestCaseName) -> Option<&RustTestCase> {
1334        match self {
1335            RustTestSuiteStatus::Listed { test_cases } => test_cases.get(name),
1336            RustTestSuiteStatus::Skipped { .. } => None,
1337        }
1338    }
1339
1340    /// Returns the list of test cases within this suite.
1341    pub fn test_cases(&self) -> impl Iterator<Item = &RustTestCase> + '_ {
1342        match self {
1343            RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
1344            RustTestSuiteStatus::Skipped { .. } => {
1345                // Return an empty test case.
1346                EMPTY_TEST_CASE_MAP.iter()
1347            }
1348        }
1349    }
1350
1351    /// Converts this status to its serializable form.
1352    pub fn to_summary(
1353        &self,
1354    ) -> (
1355        RustTestSuiteStatusSummary,
1356        BTreeMap<TestCaseName, RustTestCaseSummary>,
1357    ) {
1358        match self {
1359            Self::Listed { test_cases } => (
1360                RustTestSuiteStatusSummary::LISTED,
1361                test_cases
1362                    .iter()
1363                    .cloned()
1364                    .map(|case| (case.name, case.test_info))
1365                    .collect(),
1366            ),
1367            Self::Skipped {
1368                reason: BinaryMismatchReason::Expression,
1369            } => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
1370            Self::Skipped {
1371                reason: BinaryMismatchReason::DefaultSet,
1372            } => (
1373                RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER,
1374                BTreeMap::new(),
1375            ),
1376        }
1377    }
1378}
1379
1380/// A single test case within a test suite.
1381#[derive(Clone, Debug, Eq, PartialEq)]
1382pub struct RustTestCase {
1383    /// The name of the test.
1384    pub name: TestCaseName,
1385
1386    /// Information about the test.
1387    pub test_info: RustTestCaseSummary,
1388}
1389
1390impl IdOrdItem for RustTestCase {
1391    type Key<'a> = &'a TestCaseName;
1392    fn key(&self) -> Self::Key<'_> {
1393        &self.name
1394    }
1395    id_upcast!();
1396}
1397
1398/// Represents a single test with its associated binary.
1399#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1400pub struct TestInstance<'a> {
1401    /// The name of the test.
1402    pub name: &'a TestCaseName,
1403
1404    /// Information about the test suite.
1405    pub suite_info: &'a RustTestSuite<'a>,
1406
1407    /// Information about the test.
1408    pub test_info: &'a RustTestCaseSummary,
1409}
1410
1411impl<'a> TestInstance<'a> {
1412    /// Creates a new `TestInstance`.
1413    pub(crate) fn new(case: &'a RustTestCase, suite_info: &'a RustTestSuite) -> Self {
1414        Self {
1415            name: &case.name,
1416            suite_info,
1417            test_info: &case.test_info,
1418        }
1419    }
1420
1421    /// Return an identifier for test instances, including being able to sort
1422    /// them.
1423    #[inline]
1424    pub fn id(&self) -> TestInstanceId<'a> {
1425        TestInstanceId {
1426            binary_id: &self.suite_info.binary_id,
1427            test_name: self.name,
1428        }
1429    }
1430
1431    /// Returns the corresponding [`TestQuery`] for this `TestInstance`.
1432    pub fn to_test_query(&self) -> TestQuery<'a> {
1433        TestQuery {
1434            binary_query: BinaryQuery {
1435                package_id: self.suite_info.package.id(),
1436                binary_id: &self.suite_info.binary_id,
1437                kind: &self.suite_info.kind,
1438                binary_name: &self.suite_info.binary_name,
1439                platform: convert_build_platform(self.suite_info.build_platform),
1440            },
1441            test_name: self.name,
1442        }
1443    }
1444
1445    /// Creates the command for this test instance.
1446    pub(crate) fn make_command(
1447        &self,
1448        ctx: &TestExecuteContext<'_>,
1449        test_list: &TestList<'_>,
1450        wrapper_script: Option<&WrapperScriptConfig>,
1451        extra_args: &[String],
1452        interceptor: &Interceptor,
1453    ) -> TestCommand {
1454        // TODO: non-rust tests
1455        let cli = self.compute_cli(ctx, test_list, wrapper_script, extra_args);
1456
1457        let lctx = LocalExecuteContext {
1458            phase: TestCommandPhase::Run,
1459            workspace_root: test_list.workspace_root(),
1460            rust_build_meta: &test_list.rust_build_meta,
1461            double_spawn: ctx.double_spawn,
1462            dylib_path: test_list.updated_dylib_path(),
1463            profile_name: ctx.profile_name,
1464            env: &test_list.env,
1465        };
1466
1467        TestCommand::new(
1468            &lctx,
1469            cli.program
1470                .expect("at least one argument is guaranteed")
1471                .into_owned(),
1472            &cli.args,
1473            &self.suite_info.cwd,
1474            &self.suite_info.package,
1475            &self.suite_info.non_test_binaries,
1476            interceptor,
1477        )
1478    }
1479
1480    pub(crate) fn command_line(
1481        &self,
1482        ctx: &TestExecuteContext<'_>,
1483        test_list: &TestList<'_>,
1484        wrapper_script: Option<&WrapperScriptConfig>,
1485        extra_args: &[String],
1486    ) -> Vec<String> {
1487        self.compute_cli(ctx, test_list, wrapper_script, extra_args)
1488            .to_owned_cli()
1489    }
1490
1491    fn compute_cli(
1492        &self,
1493        ctx: &'a TestExecuteContext<'_>,
1494        test_list: &TestList<'_>,
1495        wrapper_script: Option<&'a WrapperScriptConfig>,
1496        extra_args: &'a [String],
1497    ) -> TestCommandCli<'a> {
1498        let platform_runner = ctx
1499            .target_runner
1500            .for_build_platform(self.suite_info.build_platform);
1501
1502        let mut cli = TestCommandCli::default();
1503        cli.apply_wrappers(
1504            wrapper_script,
1505            platform_runner,
1506            test_list.workspace_root(),
1507            &test_list.rust_build_meta().target_directory,
1508        );
1509        cli.push(self.suite_info.binary_path.as_str());
1510
1511        cli.extend(["--exact", self.name.as_str(), "--nocapture"]);
1512        if self.test_info.ignored {
1513            cli.push("--ignored");
1514        }
1515        match test_list.mode() {
1516            NextestRunMode::Test => {}
1517            NextestRunMode::Benchmark => {
1518                cli.push("--bench");
1519            }
1520        }
1521        cli.extend(extra_args.iter().map(String::as_str));
1522
1523        cli
1524    }
1525}
1526
1527#[derive(Clone, Debug, Default)]
1528struct TestCommandCli<'a> {
1529    program: Option<Cow<'a, str>>,
1530    args: Vec<Cow<'a, str>>,
1531}
1532
1533impl<'a> TestCommandCli<'a> {
1534    fn apply_wrappers(
1535        &mut self,
1536        wrapper_script: Option<&'a WrapperScriptConfig>,
1537        platform_runner: Option<&'a PlatformRunner>,
1538        workspace_root: &Utf8Path,
1539        target_dir: &Utf8Path,
1540    ) {
1541        // Apply the wrapper script if it's enabled.
1542        if let Some(wrapper) = wrapper_script {
1543            match wrapper.target_runner {
1544                WrapperScriptTargetRunner::Ignore => {
1545                    // Ignore the platform runner.
1546                    self.push(wrapper.command.program(workspace_root, target_dir));
1547                    self.extend(wrapper.command.args.iter().map(String::as_str));
1548                }
1549                WrapperScriptTargetRunner::AroundWrapper => {
1550                    // Platform runner goes first.
1551                    if let Some(runner) = platform_runner {
1552                        self.push(runner.binary());
1553                        self.extend(runner.args());
1554                    }
1555                    self.push(wrapper.command.program(workspace_root, target_dir));
1556                    self.extend(wrapper.command.args.iter().map(String::as_str));
1557                }
1558                WrapperScriptTargetRunner::WithinWrapper => {
1559                    // Wrapper script goes first.
1560                    self.push(wrapper.command.program(workspace_root, target_dir));
1561                    self.extend(wrapper.command.args.iter().map(String::as_str));
1562                    if let Some(runner) = platform_runner {
1563                        self.push(runner.binary());
1564                        self.extend(runner.args());
1565                    }
1566                }
1567                WrapperScriptTargetRunner::OverridesWrapper => {
1568                    // Target runner overrides wrapper.
1569                    if let Some(runner) = platform_runner {
1570                        self.push(runner.binary());
1571                        self.extend(runner.args());
1572                    }
1573                }
1574            }
1575        } else {
1576            // If no wrapper script is enabled, use the platform runner.
1577            if let Some(runner) = platform_runner {
1578                self.push(runner.binary());
1579                self.extend(runner.args());
1580            }
1581        }
1582    }
1583
1584    fn push(&mut self, arg: impl Into<Cow<'a, str>>) {
1585        if self.program.is_none() {
1586            self.program = Some(arg.into());
1587        } else {
1588            self.args.push(arg.into());
1589        }
1590    }
1591
1592    fn extend(&mut self, args: impl IntoIterator<Item = &'a str>) {
1593        for arg in args {
1594            self.push(arg);
1595        }
1596    }
1597
1598    fn to_owned_cli(&self) -> Vec<String> {
1599        let mut owned_cli = Vec::new();
1600        if let Some(program) = &self.program {
1601            owned_cli.push(program.to_string());
1602        }
1603        owned_cli.extend(self.args.iter().map(|arg| arg.clone().into_owned()));
1604        owned_cli
1605    }
1606}
1607
1608/// A key for identifying and sorting test instances.
1609///
1610/// Returned by [`TestInstance::id`].
1611#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize)]
1612pub struct TestInstanceId<'a> {
1613    /// The binary ID.
1614    pub binary_id: &'a RustBinaryId,
1615
1616    /// The name of the test.
1617    pub test_name: &'a TestCaseName,
1618}
1619
1620impl TestInstanceId<'_> {
1621    /// Return the attempt ID corresponding to this test instance.
1622    ///
1623    /// This string uniquely identifies a single test attempt.
1624    pub fn attempt_id(
1625        &self,
1626        run_id: ReportUuid,
1627        stress_index: Option<u32>,
1628        attempt: u32,
1629    ) -> String {
1630        let mut out = String::new();
1631        swrite!(out, "{run_id}:{}", self.binary_id);
1632        if let Some(stress_index) = stress_index {
1633            swrite!(out, "@stress-{}", stress_index);
1634        }
1635        swrite!(out, "${}", self.test_name);
1636        if attempt > 1 {
1637            swrite!(out, "#{attempt}");
1638        }
1639
1640        out
1641    }
1642}
1643
1644impl fmt::Display for TestInstanceId<'_> {
1645    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1646        write!(f, "{} {}", self.binary_id, self.test_name)
1647    }
1648}
1649
1650/// An owned version of [`TestInstanceId`].
1651#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
1652#[serde(rename_all = "kebab-case")]
1653#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1654pub struct OwnedTestInstanceId {
1655    /// The binary ID.
1656    pub binary_id: RustBinaryId,
1657
1658    /// The name of the test.
1659    #[serde(rename = "name")]
1660    pub test_name: TestCaseName,
1661}
1662
1663impl OwnedTestInstanceId {
1664    /// Borrow this as a [`TestInstanceId`].
1665    pub fn as_ref(&self) -> TestInstanceId<'_> {
1666        TestInstanceId {
1667            binary_id: &self.binary_id,
1668            test_name: &self.test_name,
1669        }
1670    }
1671}
1672
1673impl TestInstanceId<'_> {
1674    /// Convert this to an owned version.
1675    pub fn to_owned(&self) -> OwnedTestInstanceId {
1676        OwnedTestInstanceId {
1677            binary_id: self.binary_id.clone(),
1678            test_name: self.test_name.clone(),
1679        }
1680    }
1681}
1682
1683/// Trait to allow retrieving data from a set of [`OwnedTestInstanceId`] using a
1684/// [`TestInstanceId`].
1685///
1686/// This is an implementation of the [borrow-complex-key-example
1687/// pattern](https://github.com/sunshowers-code/borrow-complex-key-example).
1688pub trait TestInstanceIdKey {
1689    /// Converts self to a [`TestInstanceId`].
1690    fn key<'k>(&'k self) -> TestInstanceId<'k>;
1691}
1692
1693impl TestInstanceIdKey for OwnedTestInstanceId {
1694    fn key<'k>(&'k self) -> TestInstanceId<'k> {
1695        TestInstanceId {
1696            binary_id: &self.binary_id,
1697            test_name: &self.test_name,
1698        }
1699    }
1700}
1701
1702impl<'a> TestInstanceIdKey for TestInstanceId<'a> {
1703    fn key<'k>(&'k self) -> TestInstanceId<'k> {
1704        *self
1705    }
1706}
1707
1708impl<'a> Borrow<dyn TestInstanceIdKey + 'a> for OwnedTestInstanceId {
1709    fn borrow(&self) -> &(dyn TestInstanceIdKey + 'a) {
1710        self
1711    }
1712}
1713
1714impl<'a> PartialEq for dyn TestInstanceIdKey + 'a {
1715    fn eq(&self, other: &(dyn TestInstanceIdKey + 'a)) -> bool {
1716        self.key() == other.key()
1717    }
1718}
1719
1720impl<'a> Eq for dyn TestInstanceIdKey + 'a {}
1721
1722impl<'a> PartialOrd for dyn TestInstanceIdKey + 'a {
1723    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1724        Some(self.cmp(other))
1725    }
1726}
1727
1728impl<'a> Ord for dyn TestInstanceIdKey + 'a {
1729    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1730        self.key().cmp(&other.key())
1731    }
1732}
1733
1734impl<'a> Hash for dyn TestInstanceIdKey + 'a {
1735    fn hash<H: Hasher>(&self, state: &mut H) {
1736        self.key().hash(state);
1737    }
1738}
1739
1740/// Context required for test execution.
1741#[derive(Clone, Debug)]
1742pub struct TestExecuteContext<'a> {
1743    /// The name of the profile.
1744    pub profile_name: &'a str,
1745
1746    /// Double-spawn info.
1747    pub double_spawn: &'a DoubleSpawnInfo,
1748
1749    /// Target runner.
1750    pub target_runner: &'a TargetRunner,
1751}
1752
1753#[cfg(test)]
1754mod tests {
1755    use super::*;
1756    use crate::{
1757        cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
1758        config::scripts::{ScriptCommand, ScriptCommandRelativeTo},
1759        list::{
1760            SerializableFormat,
1761            test_helpers::{PACKAGE_GRAPH_FIXTURE, package_metadata},
1762        },
1763        platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
1764        target_runner::PlatformRunnerSource,
1765        test_filter::{RunIgnored, TestFilterPatterns},
1766    };
1767    use iddqd::id_ord_map;
1768    use indoc::indoc;
1769    use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, ParseContext};
1770    use nextest_metadata::{FilterMatch, MismatchReason, PlatformLibdirUnavailable, RustTestKind};
1771    use pretty_assertions::assert_eq;
1772    use std::hash::DefaultHasher;
1773    use target_spec::Platform;
1774    use test_strategy::proptest;
1775
1776    #[test]
1777    fn test_parse_test_list() {
1778        // Lines ending in ': benchmark' (output by the default Rust bencher) should be skipped.
1779        let non_ignored_output = indoc! {"
1780            tests::foo::test_bar: test
1781            tests::baz::test_quux: test
1782            benches::bench_foo: benchmark
1783        "};
1784        let ignored_output = indoc! {"
1785            tests::ignored::test_bar: test
1786            tests::baz::test_ignored: test
1787            benches::ignored_bench_foo: benchmark
1788        "};
1789
1790        let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
1791
1792        let test_filter = TestFilter::new(
1793            NextestRunMode::Test,
1794            RunIgnored::Default,
1795            TestFilterPatterns::default(),
1796            // Test against the platform() predicate because this is the most important one here.
1797            vec![
1798                Filterset::parse("platform(target)".to_owned(), &cx, FiltersetKind::Test).unwrap(),
1799            ],
1800        )
1801        .unwrap();
1802        let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
1803        let fake_binary_name = "fake-binary".to_owned();
1804        let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
1805
1806        let test_binary = RustTestArtifact {
1807            binary_path: "/fake/binary".into(),
1808            cwd: fake_cwd.clone(),
1809            package: package_metadata(),
1810            binary_name: fake_binary_name.clone(),
1811            binary_id: fake_binary_id.clone(),
1812            kind: RustTestBinaryKind::LIB,
1813            non_test_binaries: BTreeSet::new(),
1814            build_platform: BuildPlatform::Target,
1815        };
1816
1817        let skipped_binary_name = "skipped-binary".to_owned();
1818        let skipped_binary_id = RustBinaryId::new("fake-package::skipped-binary");
1819        let skipped_binary = RustTestArtifact {
1820            binary_path: "/fake/skipped-binary".into(),
1821            cwd: fake_cwd.clone(),
1822            package: package_metadata(),
1823            binary_name: skipped_binary_name.clone(),
1824            binary_id: skipped_binary_id.clone(),
1825            kind: RustTestBinaryKind::PROC_MACRO,
1826            non_test_binaries: BTreeSet::new(),
1827            build_platform: BuildPlatform::Host,
1828        };
1829
1830        let fake_triple = TargetTriple {
1831            platform: Platform::new(
1832                "aarch64-unknown-linux-gnu",
1833                target_spec::TargetFeatures::Unknown,
1834            )
1835            .unwrap(),
1836            source: TargetTripleSource::CliOption,
1837            location: TargetDefinitionLocation::Builtin,
1838        };
1839        let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
1840        let build_platforms = BuildPlatforms {
1841            host: HostPlatform {
1842                platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
1843                libdir: PlatformLibdir::Available(fake_host_libdir.into()),
1844            },
1845            target: Some(TargetPlatform {
1846                triple: fake_triple,
1847                // Test an unavailable libdir.
1848                libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
1849            }),
1850        };
1851
1852        let fake_env = EnvironmentMap::empty();
1853        let rust_build_meta =
1854            RustBuildMeta::new("/fake", build_platforms).map_paths(&PathMapper::noop());
1855        let ecx = EvalContext {
1856            default_filter: &CompiledExpr::ALL,
1857        };
1858        let test_list = TestList::new_with_outputs(
1859            [
1860                (test_binary, &non_ignored_output, &ignored_output),
1861                (
1862                    skipped_binary,
1863                    &"should-not-show-up-stdout",
1864                    &"should-not-show-up-stderr",
1865                ),
1866            ],
1867            Utf8PathBuf::from("/fake/path"),
1868            rust_build_meta,
1869            &test_filter,
1870            None,
1871            fake_env,
1872            &ecx,
1873            FilterBound::All,
1874        )
1875        .expect("valid output");
1876        assert_eq!(
1877            test_list.rust_suites,
1878            id_ord_map! {
1879                RustTestSuite {
1880                    status: RustTestSuiteStatus::Listed {
1881                        test_cases: id_ord_map! {
1882                            RustTestCase {
1883                                name: TestCaseName::new("tests::foo::test_bar"),
1884                                test_info: RustTestCaseSummary {
1885                                    kind: Some(RustTestKind::TEST),
1886                                    ignored: false,
1887                                    filter_match: FilterMatch::Matches,
1888                                },
1889                            },
1890                            RustTestCase {
1891                                name: TestCaseName::new("tests::baz::test_quux"),
1892                                test_info: RustTestCaseSummary {
1893                                    kind: Some(RustTestKind::TEST),
1894                                    ignored: false,
1895                                    filter_match: FilterMatch::Matches,
1896                                },
1897                            },
1898                            RustTestCase {
1899                                name: TestCaseName::new("benches::bench_foo"),
1900                                test_info: RustTestCaseSummary {
1901                                    kind: Some(RustTestKind::BENCH),
1902                                    ignored: false,
1903                                    filter_match: FilterMatch::Matches,
1904                                },
1905                            },
1906                            RustTestCase {
1907                                name: TestCaseName::new("tests::ignored::test_bar"),
1908                                test_info: RustTestCaseSummary {
1909                                    kind: Some(RustTestKind::TEST),
1910                                    ignored: true,
1911                                    filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1912                                },
1913                            },
1914                            RustTestCase {
1915                                name: TestCaseName::new("tests::baz::test_ignored"),
1916                                test_info: RustTestCaseSummary {
1917                                    kind: Some(RustTestKind::TEST),
1918                                    ignored: true,
1919                                    filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1920                                },
1921                            },
1922                            RustTestCase {
1923                                name: TestCaseName::new("benches::ignored_bench_foo"),
1924                                test_info: RustTestCaseSummary {
1925                                    kind: Some(RustTestKind::BENCH),
1926                                    ignored: true,
1927                                    filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
1928                                },
1929                            },
1930                        }.into(),
1931                    },
1932                    cwd: fake_cwd.clone(),
1933                    build_platform: BuildPlatform::Target,
1934                    package: package_metadata(),
1935                    binary_name: fake_binary_name,
1936                    binary_id: fake_binary_id,
1937                    binary_path: "/fake/binary".into(),
1938                    kind: RustTestBinaryKind::LIB,
1939                    non_test_binaries: BTreeSet::new(),
1940                },
1941                RustTestSuite {
1942                    status: RustTestSuiteStatus::Skipped {
1943                        reason: BinaryMismatchReason::Expression,
1944                    },
1945                    cwd: fake_cwd,
1946                    build_platform: BuildPlatform::Host,
1947                    package: package_metadata(),
1948                    binary_name: skipped_binary_name,
1949                    binary_id: skipped_binary_id,
1950                    binary_path: "/fake/skipped-binary".into(),
1951                    kind: RustTestBinaryKind::PROC_MACRO,
1952                    non_test_binaries: BTreeSet::new(),
1953                },
1954            }
1955        );
1956
1957        // Check that the expected outputs are valid.
1958        static EXPECTED_HUMAN: &str = indoc! {"
1959        fake-package::fake-binary:
1960            benches::bench_foo
1961            tests::baz::test_quux
1962            tests::foo::test_bar
1963        "};
1964        static EXPECTED_HUMAN_VERBOSE: &str = indoc! {"
1965            fake-package::fake-binary:
1966              bin: /fake/binary
1967              cwd: /fake/cwd
1968              build platform: target
1969                benches::bench_foo
1970                benches::ignored_bench_foo (skipped)
1971                tests::baz::test_ignored (skipped)
1972                tests::baz::test_quux
1973                tests::foo::test_bar
1974                tests::ignored::test_bar (skipped)
1975            fake-package::skipped-binary:
1976              bin: /fake/skipped-binary
1977              cwd: /fake/cwd
1978              build platform: host
1979                (test binary didn't match filtersets, skipped)
1980        "};
1981        static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
1982            {
1983              "rust-build-meta": {
1984                "target-directory": "/fake",
1985                "base-output-directories": [],
1986                "non-test-binaries": {},
1987                "build-script-out-dirs": {},
1988                "linked-paths": [],
1989                "platforms": {
1990                  "host": {
1991                    "platform": {
1992                      "triple": "x86_64-unknown-linux-gnu",
1993                      "target-features": "unknown"
1994                    },
1995                    "libdir": {
1996                      "status": "available",
1997                      "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
1998                    }
1999                  },
2000                  "targets": [
2001                    {
2002                      "platform": {
2003                        "triple": "aarch64-unknown-linux-gnu",
2004                        "target-features": "unknown"
2005                      },
2006                      "libdir": {
2007                        "status": "unavailable",
2008                        "reason": "test"
2009                      }
2010                    }
2011                  ]
2012                },
2013                "target-platforms": [
2014                  {
2015                    "triple": "aarch64-unknown-linux-gnu",
2016                    "target-features": "unknown"
2017                  }
2018                ],
2019                "target-platform": "aarch64-unknown-linux-gnu"
2020              },
2021              "test-count": 6,
2022              "rust-suites": {
2023                "fake-package::fake-binary": {
2024                  "package-name": "metadata-helper",
2025                  "binary-id": "fake-package::fake-binary",
2026                  "binary-name": "fake-binary",
2027                  "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
2028                  "kind": "lib",
2029                  "binary-path": "/fake/binary",
2030                  "build-platform": "target",
2031                  "cwd": "/fake/cwd",
2032                  "status": "listed",
2033                  "testcases": {
2034                    "benches::bench_foo": {
2035                      "kind": "bench",
2036                      "ignored": false,
2037                      "filter-match": {
2038                        "status": "matches"
2039                      }
2040                    },
2041                    "benches::ignored_bench_foo": {
2042                      "kind": "bench",
2043                      "ignored": true,
2044                      "filter-match": {
2045                        "status": "mismatch",
2046                        "reason": "ignored"
2047                      }
2048                    },
2049                    "tests::baz::test_ignored": {
2050                      "kind": "test",
2051                      "ignored": true,
2052                      "filter-match": {
2053                        "status": "mismatch",
2054                        "reason": "ignored"
2055                      }
2056                    },
2057                    "tests::baz::test_quux": {
2058                      "kind": "test",
2059                      "ignored": false,
2060                      "filter-match": {
2061                        "status": "matches"
2062                      }
2063                    },
2064                    "tests::foo::test_bar": {
2065                      "kind": "test",
2066                      "ignored": false,
2067                      "filter-match": {
2068                        "status": "matches"
2069                      }
2070                    },
2071                    "tests::ignored::test_bar": {
2072                      "kind": "test",
2073                      "ignored": true,
2074                      "filter-match": {
2075                        "status": "mismatch",
2076                        "reason": "ignored"
2077                      }
2078                    }
2079                  }
2080                },
2081                "fake-package::skipped-binary": {
2082                  "package-name": "metadata-helper",
2083                  "binary-id": "fake-package::skipped-binary",
2084                  "binary-name": "skipped-binary",
2085                  "package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
2086                  "kind": "proc-macro",
2087                  "binary-path": "/fake/skipped-binary",
2088                  "build-platform": "host",
2089                  "cwd": "/fake/cwd",
2090                  "status": "skipped",
2091                  "testcases": {}
2092                }
2093              }
2094            }"#};
2095        static EXPECTED_ONELINE: &str = indoc! {"
2096            fake-package::fake-binary benches::bench_foo
2097            fake-package::fake-binary tests::baz::test_quux
2098            fake-package::fake-binary tests::foo::test_bar
2099        "};
2100        static EXPECTED_ONELINE_VERBOSE: &str = indoc! {"
2101            fake-package::fake-binary benches::bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2102            fake-package::fake-binary benches::ignored_bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2103            fake-package::fake-binary tests::baz::test_ignored [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2104            fake-package::fake-binary tests::baz::test_quux [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2105            fake-package::fake-binary tests::foo::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
2106            fake-package::fake-binary tests::ignored::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
2107        "};
2108
2109        assert_eq!(
2110            test_list
2111                .to_string(OutputFormat::Human { verbose: false })
2112                .expect("human succeeded"),
2113            EXPECTED_HUMAN
2114        );
2115        assert_eq!(
2116            test_list
2117                .to_string(OutputFormat::Human { verbose: true })
2118                .expect("human succeeded"),
2119            EXPECTED_HUMAN_VERBOSE
2120        );
2121        println!(
2122            "{}",
2123            test_list
2124                .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
2125                .expect("json-pretty succeeded")
2126        );
2127        assert_eq!(
2128            test_list
2129                .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
2130                .expect("json-pretty succeeded"),
2131            EXPECTED_JSON_PRETTY
2132        );
2133        assert_eq!(
2134            test_list
2135                .to_string(OutputFormat::Oneline { verbose: false })
2136                .expect("oneline succeeded"),
2137            EXPECTED_ONELINE
2138        );
2139        assert_eq!(
2140            test_list
2141                .to_string(OutputFormat::Oneline { verbose: true })
2142                .expect("oneline verbose succeeded"),
2143            EXPECTED_ONELINE_VERBOSE
2144        );
2145    }
2146
2147    #[test]
2148    fn apply_wrappers_examples() {
2149        cfg_if::cfg_if! {
2150            if #[cfg(windows)]
2151            {
2152                let workspace_root = Utf8Path::new("D:\\workspace\\root");
2153                let target_dir = Utf8Path::new("C:\\foo\\bar");
2154            } else {
2155                let workspace_root = Utf8Path::new("/workspace/root");
2156                let target_dir = Utf8Path::new("/foo/bar");
2157            }
2158        };
2159
2160        // Test with no wrappers
2161        {
2162            let mut cli_no_wrappers = TestCommandCli::default();
2163            cli_no_wrappers.apply_wrappers(None, None, workspace_root, target_dir);
2164            cli_no_wrappers.extend(["binary", "arg"]);
2165            assert_eq!(cli_no_wrappers.to_owned_cli(), vec!["binary", "arg"]);
2166        }
2167
2168        // Test with platform runner only
2169        {
2170            let runner = PlatformRunner::debug_new(
2171                "runner".into(),
2172                Vec::new(),
2173                PlatformRunnerSource::Env("fake".to_owned()),
2174            );
2175            let mut cli_runner_only = TestCommandCli::default();
2176            cli_runner_only.apply_wrappers(None, Some(&runner), workspace_root, target_dir);
2177            cli_runner_only.extend(["binary", "arg"]);
2178            assert_eq!(
2179                cli_runner_only.to_owned_cli(),
2180                vec!["runner", "binary", "arg"],
2181            );
2182        }
2183
2184        // Test wrapper with ignore target runner
2185        {
2186            let runner = PlatformRunner::debug_new(
2187                "runner".into(),
2188                Vec::new(),
2189                PlatformRunnerSource::Env("fake".to_owned()),
2190            );
2191            let wrapper_ignore = WrapperScriptConfig {
2192                command: ScriptCommand {
2193                    program: "wrapper".into(),
2194                    args: Vec::new(),
2195                    relative_to: ScriptCommandRelativeTo::None,
2196                },
2197                target_runner: WrapperScriptTargetRunner::Ignore,
2198            };
2199            let mut cli_wrapper_ignore = TestCommandCli::default();
2200            cli_wrapper_ignore.apply_wrappers(
2201                Some(&wrapper_ignore),
2202                Some(&runner),
2203                workspace_root,
2204                target_dir,
2205            );
2206            cli_wrapper_ignore.extend(["binary", "arg"]);
2207            assert_eq!(
2208                cli_wrapper_ignore.to_owned_cli(),
2209                vec!["wrapper", "binary", "arg"],
2210            );
2211        }
2212
2213        // Test wrapper with around wrapper (runner first)
2214        {
2215            let runner = PlatformRunner::debug_new(
2216                "runner".into(),
2217                Vec::new(),
2218                PlatformRunnerSource::Env("fake".to_owned()),
2219            );
2220            let wrapper_around = WrapperScriptConfig {
2221                command: ScriptCommand {
2222                    program: "wrapper".into(),
2223                    args: Vec::new(),
2224                    relative_to: ScriptCommandRelativeTo::None,
2225                },
2226                target_runner: WrapperScriptTargetRunner::AroundWrapper,
2227            };
2228            let mut cli_wrapper_around = TestCommandCli::default();
2229            cli_wrapper_around.apply_wrappers(
2230                Some(&wrapper_around),
2231                Some(&runner),
2232                workspace_root,
2233                target_dir,
2234            );
2235            cli_wrapper_around.extend(["binary", "arg"]);
2236            assert_eq!(
2237                cli_wrapper_around.to_owned_cli(),
2238                vec!["runner", "wrapper", "binary", "arg"],
2239            );
2240        }
2241
2242        // Test wrapper with within wrapper (wrapper first)
2243        {
2244            let runner = PlatformRunner::debug_new(
2245                "runner".into(),
2246                Vec::new(),
2247                PlatformRunnerSource::Env("fake".to_owned()),
2248            );
2249            let wrapper_within = WrapperScriptConfig {
2250                command: ScriptCommand {
2251                    program: "wrapper".into(),
2252                    args: Vec::new(),
2253                    relative_to: ScriptCommandRelativeTo::None,
2254                },
2255                target_runner: WrapperScriptTargetRunner::WithinWrapper,
2256            };
2257            let mut cli_wrapper_within = TestCommandCli::default();
2258            cli_wrapper_within.apply_wrappers(
2259                Some(&wrapper_within),
2260                Some(&runner),
2261                workspace_root,
2262                target_dir,
2263            );
2264            cli_wrapper_within.extend(["binary", "arg"]);
2265            assert_eq!(
2266                cli_wrapper_within.to_owned_cli(),
2267                vec!["wrapper", "runner", "binary", "arg"],
2268            );
2269        }
2270
2271        // Test wrapper with overrides wrapper (runner only)
2272        {
2273            let runner = PlatformRunner::debug_new(
2274                "runner".into(),
2275                Vec::new(),
2276                PlatformRunnerSource::Env("fake".to_owned()),
2277            );
2278            let wrapper_overrides = WrapperScriptConfig {
2279                command: ScriptCommand {
2280                    program: "wrapper".into(),
2281                    args: Vec::new(),
2282                    relative_to: ScriptCommandRelativeTo::None,
2283                },
2284                target_runner: WrapperScriptTargetRunner::OverridesWrapper,
2285            };
2286            let mut cli_wrapper_overrides = TestCommandCli::default();
2287            cli_wrapper_overrides.apply_wrappers(
2288                Some(&wrapper_overrides),
2289                Some(&runner),
2290                workspace_root,
2291                target_dir,
2292            );
2293            cli_wrapper_overrides.extend(["binary", "arg"]);
2294            assert_eq!(
2295                cli_wrapper_overrides.to_owned_cli(),
2296                vec!["runner", "binary", "arg"],
2297            );
2298        }
2299
2300        // Test wrapper with args
2301        {
2302            let wrapper_with_args = WrapperScriptConfig {
2303                command: ScriptCommand {
2304                    program: "wrapper".into(),
2305                    args: vec!["--flag".to_string(), "value".to_string()],
2306                    relative_to: ScriptCommandRelativeTo::None,
2307                },
2308                target_runner: WrapperScriptTargetRunner::Ignore,
2309            };
2310            let mut cli_wrapper_args = TestCommandCli::default();
2311            cli_wrapper_args.apply_wrappers(
2312                Some(&wrapper_with_args),
2313                None,
2314                workspace_root,
2315                target_dir,
2316            );
2317            cli_wrapper_args.extend(["binary", "arg"]);
2318            assert_eq!(
2319                cli_wrapper_args.to_owned_cli(),
2320                vec!["wrapper", "--flag", "value", "binary", "arg"],
2321            );
2322        }
2323
2324        // Test platform runner with args
2325        {
2326            let runner_with_args = PlatformRunner::debug_new(
2327                "runner".into(),
2328                vec!["--runner-flag".into(), "value".into()],
2329                PlatformRunnerSource::Env("fake".to_owned()),
2330            );
2331            let mut cli_runner_args = TestCommandCli::default();
2332            cli_runner_args.apply_wrappers(
2333                None,
2334                Some(&runner_with_args),
2335                workspace_root,
2336                target_dir,
2337            );
2338            cli_runner_args.extend(["binary", "arg"]);
2339            assert_eq!(
2340                cli_runner_args.to_owned_cli(),
2341                vec!["runner", "--runner-flag", "value", "binary", "arg"],
2342            );
2343        }
2344
2345        // Test wrapper with ScriptCommandRelativeTo::WorkspaceRoot
2346        {
2347            let wrapper_relative_to_workspace_root = WrapperScriptConfig {
2348                command: ScriptCommand {
2349                    program: "abc/def/my-wrapper".into(),
2350                    args: vec!["--verbose".to_string()],
2351                    relative_to: ScriptCommandRelativeTo::WorkspaceRoot,
2352                },
2353                target_runner: WrapperScriptTargetRunner::Ignore,
2354            };
2355            let mut cli_wrapper_relative = TestCommandCli::default();
2356            cli_wrapper_relative.apply_wrappers(
2357                Some(&wrapper_relative_to_workspace_root),
2358                None,
2359                workspace_root,
2360                target_dir,
2361            );
2362            cli_wrapper_relative.extend(["binary", "arg"]);
2363
2364            cfg_if::cfg_if! {
2365                if #[cfg(windows)] {
2366                    let wrapper_path = "D:\\workspace\\root\\abc\\def\\my-wrapper";
2367                } else {
2368                    let wrapper_path = "/workspace/root/abc/def/my-wrapper";
2369                }
2370            }
2371            assert_eq!(
2372                cli_wrapper_relative.to_owned_cli(),
2373                vec![wrapper_path, "--verbose", "binary", "arg"],
2374            );
2375        }
2376
2377        // Test wrapper with ScriptCommandRelativeTo::Target
2378        {
2379            let wrapper_relative_to_target = WrapperScriptConfig {
2380                command: ScriptCommand {
2381                    program: "abc/def/my-wrapper".into(),
2382                    args: vec!["--verbose".to_string()],
2383                    relative_to: ScriptCommandRelativeTo::Target,
2384                },
2385                target_runner: WrapperScriptTargetRunner::Ignore,
2386            };
2387            let mut cli_wrapper_relative = TestCommandCli::default();
2388            cli_wrapper_relative.apply_wrappers(
2389                Some(&wrapper_relative_to_target),
2390                None,
2391                workspace_root,
2392                target_dir,
2393            );
2394            cli_wrapper_relative.extend(["binary", "arg"]);
2395            cfg_if::cfg_if! {
2396                if #[cfg(windows)] {
2397                    let wrapper_path = "C:\\foo\\bar\\abc\\def\\my-wrapper";
2398                } else {
2399                    let wrapper_path = "/foo/bar/abc/def/my-wrapper";
2400                }
2401            }
2402            assert_eq!(
2403                cli_wrapper_relative.to_owned_cli(),
2404                vec![wrapper_path, "--verbose", "binary", "arg"],
2405            );
2406        }
2407    }
2408
2409    #[test]
2410    fn test_parse_list_lines() {
2411        let binary_id = RustBinaryId::new("test-package::test-binary");
2412
2413        // Valid: tests only.
2414        let input = indoc! {"
2415            simple_test: test
2416            module::nested_test: test
2417            deeply::nested::module::test_name: test
2418        "};
2419        let results: Vec<_> = parse_list_lines(&binary_id, input)
2420            .collect::<Result<_, _>>()
2421            .expect("parsed valid test output");
2422        insta::assert_debug_snapshot!("valid_tests", results);
2423
2424        // Valid: benchmarks only.
2425        let input = indoc! {"
2426            simple_bench: benchmark
2427            benches::module::my_benchmark: benchmark
2428        "};
2429        let results: Vec<_> = parse_list_lines(&binary_id, input)
2430            .collect::<Result<_, _>>()
2431            .expect("parsed valid benchmark output");
2432        insta::assert_debug_snapshot!("valid_benchmarks", results);
2433
2434        // Valid: mixed tests and benchmarks.
2435        let input = indoc! {"
2436            test_one: test
2437            bench_one: benchmark
2438            test_two: test
2439            bench_two: benchmark
2440        "};
2441        let results: Vec<_> = parse_list_lines(&binary_id, input)
2442            .collect::<Result<_, _>>()
2443            .expect("parsed mixed output");
2444        insta::assert_debug_snapshot!("mixed_tests_and_benchmarks", results);
2445
2446        // Valid: special characters.
2447        let input = indoc! {r#"
2448            test_with_underscore_123: test
2449            test::with::colons: test
2450            test_with_numbers_42: test
2451        "#};
2452        let results: Vec<_> = parse_list_lines(&binary_id, input)
2453            .collect::<Result<_, _>>()
2454            .expect("parsed tests with special characters");
2455        insta::assert_debug_snapshot!("special_characters", results);
2456
2457        // Valid: empty input.
2458        let input = "";
2459        let results: Vec<_> = parse_list_lines(&binary_id, input)
2460            .collect::<Result<_, _>>()
2461            .expect("parsed empty output");
2462        insta::assert_debug_snapshot!("empty_input", results);
2463
2464        // Invalid: wrong suffix.
2465        let input = "invalid_test: wrong_suffix";
2466        let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2467        assert!(result.is_err());
2468        insta::assert_snapshot!("invalid_suffix_error", result.unwrap_err());
2469
2470        // Invalid: missing suffix.
2471        let input = "test_without_suffix";
2472        let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2473        assert!(result.is_err());
2474        insta::assert_snapshot!("missing_suffix_error", result.unwrap_err());
2475
2476        // Invalid: partial valid (stops at first error).
2477        let input = indoc! {"
2478            valid_test: test
2479            invalid_line
2480            another_valid: benchmark
2481        "};
2482        let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2483        assert!(result.is_err());
2484        insta::assert_snapshot!("partial_valid_error", result.unwrap_err());
2485
2486        // Invalid: control character.
2487        let input = indoc! {"
2488            valid_test: test
2489            \rinvalid_line
2490            another_valid: benchmark
2491        "};
2492        let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
2493        assert!(result.is_err());
2494        insta::assert_snapshot!("control_character_error", result.unwrap_err());
2495    }
2496
2497    // Proptest to verify that the `Borrow<dyn TestInstanceIdKey>` implementation for
2498    // `OwnedTestInstanceId` is consistent with Eq, Ord, and Hash.
2499    #[proptest]
2500    fn test_instance_id_key_borrow_consistency(
2501        owned1: OwnedTestInstanceId,
2502        owned2: OwnedTestInstanceId,
2503    ) {
2504        // Create borrowed trait object references.
2505        let borrowed1: &dyn TestInstanceIdKey = &owned1;
2506        let borrowed2: &dyn TestInstanceIdKey = &owned2;
2507
2508        // Verify Eq consistency: owned equality must match borrowed equality.
2509        assert_eq!(
2510            owned1 == owned2,
2511            borrowed1 == borrowed2,
2512            "Eq must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2513        );
2514
2515        // Verify PartialOrd consistency.
2516        assert_eq!(
2517            owned1.partial_cmp(&owned2),
2518            borrowed1.partial_cmp(borrowed2),
2519            "PartialOrd must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2520        );
2521
2522        // Verify Ord consistency.
2523        assert_eq!(
2524            owned1.cmp(&owned2),
2525            borrowed1.cmp(borrowed2),
2526            "Ord must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
2527        );
2528
2529        // Verify Hash consistency.
2530        fn hash_value(x: &impl Hash) -> u64 {
2531            let mut hasher = DefaultHasher::new();
2532            x.hash(&mut hasher);
2533            hasher.finish()
2534        }
2535
2536        assert_eq!(
2537            hash_value(&owned1),
2538            hash_value(&borrowed1),
2539            "Hash must be consistent for owned1 and its borrowed form"
2540        );
2541        assert_eq!(
2542            hash_value(&owned2),
2543            hash_value(&borrowed2),
2544            "Hash must be consistent for owned2 and its borrowed form"
2545        );
2546    }
2547}