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