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