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