nextest_runner/list/
test_list.rs

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