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