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