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