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