nextest_metadata/
test_list.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::CommandError;
5use camino::{Utf8Path, Utf8PathBuf};
6use serde::{Deserialize, Serialize};
7use smol_str::SmolStr;
8use std::{
9    borrow::Cow,
10    cmp::Ordering,
11    collections::{BTreeMap, BTreeSet},
12    fmt::{self, Write as _},
13    path::PathBuf,
14    process::Command,
15};
16use target_spec::summaries::PlatformSummary;
17
18/// Command builder for `cargo nextest list`.
19#[derive(Clone, Debug, Default)]
20pub struct ListCommand {
21    cargo_path: Option<Box<Utf8Path>>,
22    manifest_path: Option<Box<Utf8Path>>,
23    current_dir: Option<Box<Utf8Path>>,
24    args: Vec<Box<str>>,
25}
26
27impl ListCommand {
28    /// Creates a new `ListCommand`.
29    ///
30    /// This command runs `cargo nextest list`.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Path to `cargo` executable. If not set, this will use the the `$CARGO` environment variable, and
36    /// if that is not set, will simply be `cargo`.
37    pub fn cargo_path(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
38        self.cargo_path = Some(path.into().into());
39        self
40    }
41
42    /// Path to `Cargo.toml`.
43    pub fn manifest_path(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
44        self.manifest_path = Some(path.into().into());
45        self
46    }
47
48    /// Current directory of the `cargo nextest list` process.
49    pub fn current_dir(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
50        self.current_dir = Some(path.into().into());
51        self
52    }
53
54    /// Adds an argument to the end of `cargo nextest list`.
55    pub fn add_arg(&mut self, arg: impl Into<String>) -> &mut Self {
56        self.args.push(arg.into().into());
57        self
58    }
59
60    /// Adds several arguments to the end of `cargo nextest list`.
61    pub fn add_args(&mut self, args: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
62        for arg in args {
63            self.add_arg(arg.into());
64        }
65        self
66    }
67
68    /// Builds a command for `cargo nextest list`. This is the first part of the work of [`self.exec`].
69    pub fn cargo_command(&self) -> Command {
70        let cargo_path: PathBuf = self.cargo_path.as_ref().map_or_else(
71            || std::env::var_os("CARGO").map_or("cargo".into(), PathBuf::from),
72            |path| PathBuf::from(path.as_std_path()),
73        );
74
75        let mut command = Command::new(cargo_path);
76        if let Some(path) = &self.manifest_path.as_deref() {
77            command.args(["--manifest-path", path.as_str()]);
78        }
79        if let Some(current_dir) = &self.current_dir.as_deref() {
80            command.current_dir(current_dir);
81        }
82
83        command.args(["nextest", "list", "--message-format=json"]);
84
85        command.args(self.args.iter().map(|s| s.as_ref()));
86        command
87    }
88
89    /// Executes `cargo nextest list` and parses the output into a [`TestListSummary`].
90    pub fn exec(&self) -> Result<TestListSummary, CommandError> {
91        let mut command = self.cargo_command();
92        let output = command.output().map_err(CommandError::Exec)?;
93
94        if !output.status.success() {
95            // The process exited with a non-zero code.
96            let exit_code = output.status.code();
97            let stderr = output.stderr;
98            return Err(CommandError::CommandFailed { exit_code, stderr });
99        }
100
101        // Try parsing stdout.
102        serde_json::from_slice(&output.stdout).map_err(CommandError::Json)
103    }
104
105    /// Executes `cargo nextest list --list-type binaries-only` and parses the output into a
106    /// [`BinaryListSummary`].
107    pub fn exec_binaries_only(&self) -> Result<BinaryListSummary, CommandError> {
108        let mut command = self.cargo_command();
109        command.arg("--list-type=binaries-only");
110        let output = command.output().map_err(CommandError::Exec)?;
111
112        if !output.status.success() {
113            // The process exited with a non-zero code.
114            let exit_code = output.status.code();
115            let stderr = output.stderr;
116            return Err(CommandError::CommandFailed { exit_code, stderr });
117        }
118
119        // Try parsing stdout.
120        serde_json::from_slice(&output.stdout).map_err(CommandError::Json)
121    }
122}
123
124/// Root element for a serializable list of tests generated by nextest.
125#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
126#[serde(rename_all = "kebab-case")]
127#[non_exhaustive]
128pub struct TestListSummary {
129    /// Rust metadata used for builds and test runs.
130    pub rust_build_meta: RustBuildMetaSummary,
131
132    /// Number of tests (including skipped and ignored) across all binaries.
133    pub test_count: usize,
134
135    /// A map of Rust test suites to the test binaries within them, keyed by a unique identifier
136    /// for each test suite.
137    pub rust_suites: BTreeMap<RustBinaryId, RustTestSuiteSummary>,
138}
139
140impl TestListSummary {
141    /// Creates a new `TestListSummary` with the given Rust metadata.
142    pub fn new(rust_build_meta: RustBuildMetaSummary) -> Self {
143        Self {
144            rust_build_meta,
145            test_count: 0,
146            rust_suites: BTreeMap::new(),
147        }
148    }
149    /// Parse JSON output from `cargo nextest list --message-format json`.
150    pub fn parse_json(json: impl AsRef<str>) -> Result<Self, serde_json::Error> {
151        serde_json::from_str(json.as_ref())
152    }
153}
154
155/// The platform a binary was built on (useful for cross-compilation)
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
157#[serde(rename_all = "kebab-case")]
158pub enum BuildPlatform {
159    /// The target platform.
160    Target,
161
162    /// The host platform: the platform the build was performed on.
163    Host,
164}
165
166impl fmt::Display for BuildPlatform {
167    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
168        match self {
169            Self::Target => write!(f, "target"),
170            Self::Host => write!(f, "host"),
171        }
172    }
173}
174
175/// A serializable Rust test binary.
176///
177/// Part of a [`RustTestSuiteSummary`] and [`BinaryListSummary`].
178#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
179#[serde(rename_all = "kebab-case")]
180pub struct RustTestBinarySummary {
181    /// A unique binary ID.
182    pub binary_id: RustBinaryId,
183
184    /// The name of the test binary within the package.
185    pub binary_name: String,
186
187    /// The unique package ID assigned by Cargo to this test.
188    ///
189    /// This package ID can be used for lookups in `cargo metadata`.
190    pub package_id: String,
191
192    /// The kind of Rust test binary this is.
193    pub kind: RustTestBinaryKind,
194
195    /// The path to the test binary executable.
196    pub binary_path: Utf8PathBuf,
197
198    /// Platform for which this binary was built.
199    /// (Proc-macro tests are built for the host.)
200    pub build_platform: BuildPlatform,
201}
202
203/// Information about the kind of a Rust test binary.
204///
205/// Kinds are used to generate [`RustBinaryId`] instances, and to figure out whether some
206/// environment variables should be set.
207#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
208#[serde(transparent)]
209pub struct RustTestBinaryKind(pub Cow<'static, str>);
210
211impl RustTestBinaryKind {
212    /// Creates a new `RustTestBinaryKind` from a string.
213    #[inline]
214    pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
215        Self(kind.into())
216    }
217
218    /// Creates a new `RustTestBinaryKind` from a static string.
219    #[inline]
220    pub const fn new_const(kind: &'static str) -> Self {
221        Self(Cow::Borrowed(kind))
222    }
223
224    /// Returns the kind as a string.
225    pub fn as_str(&self) -> &str {
226        &self.0
227    }
228
229    /// The "lib" kind, used for unit tests within the library.
230    pub const LIB: Self = Self::new_const("lib");
231
232    /// The "test" kind, used for integration tests.
233    pub const TEST: Self = Self::new_const("test");
234
235    /// The "bench" kind, used for benchmarks.
236    pub const BENCH: Self = Self::new_const("bench");
237
238    /// The "bin" kind, used for unit tests within binaries.
239    pub const BIN: Self = Self::new_const("bin");
240
241    /// The "example" kind, used for unit tests within examples.
242    pub const EXAMPLE: Self = Self::new_const("example");
243
244    /// The "proc-macro" kind, used for tests within procedural macros.
245    pub const PROC_MACRO: Self = Self::new_const("proc-macro");
246}
247
248impl fmt::Display for RustTestBinaryKind {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        write!(f, "{}", self.0)
251    }
252}
253
254/// A serializable suite of test binaries.
255#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
256#[serde(rename_all = "kebab-case")]
257pub struct BinaryListSummary {
258    /// Rust metadata used for builds and test runs.
259    pub rust_build_meta: RustBuildMetaSummary,
260
261    /// The list of Rust test binaries (indexed by binary-id).
262    pub rust_binaries: BTreeMap<RustBinaryId, RustTestBinarySummary>,
263}
264
265// IMPLEMENTATION NOTE: SmolStr is *not* part of the public API.
266
267/// A unique identifier for a test suite (a Rust binary).
268#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
269#[serde(transparent)]
270pub struct RustBinaryId(SmolStr);
271
272impl fmt::Display for RustBinaryId {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        f.write_str(&self.0)
275    }
276}
277
278impl RustBinaryId {
279    /// Creates a new `RustBinaryId` from a string.
280    #[inline]
281    pub fn new(id: &str) -> Self {
282        Self(id.into())
283    }
284
285    /// Creates a new `RustBinaryId` from its constituent parts:
286    ///
287    /// * `package_name`: The name of the package as defined in `Cargo.toml`.
288    /// * `kind`: The kind of the target (see [`RustTestBinaryKind`]).
289    /// * `target_name`: The name of the target.
290    ///
291    /// The algorithm is as follows:
292    ///
293    /// 1. If the kind is `lib` or `proc-macro` (i.e. for unit tests), the binary ID is the same as
294    ///    the package name. There can only be one library per package, so this will always be
295    ///    unique.
296    /// 2. If the target is an integration test, the binary ID is `package_name::target_name`.
297    /// 3. Otherwise, the binary ID is `package_name::{kind}/{target_name}`.
298    ///
299    /// This format is part of nextest's stable API.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use nextest_metadata::{RustBinaryId, RustTestBinaryKind};
305    ///
306    /// // The lib and proc-macro kinds.
307    /// assert_eq!(
308    ///     RustBinaryId::from_parts("foo-lib", &RustTestBinaryKind::LIB, "foo_lib"),
309    ///     RustBinaryId::new("foo-lib"),
310    /// );
311    /// assert_eq!(
312    ///    RustBinaryId::from_parts("foo-derive", &RustTestBinaryKind::PROC_MACRO, "derive"),
313    ///    RustBinaryId::new("foo-derive"),
314    /// );
315    ///
316    /// // Integration tests.
317    /// assert_eq!(
318    ///    RustBinaryId::from_parts("foo-lib", &RustTestBinaryKind::TEST, "foo_test"),
319    ///    RustBinaryId::new("foo-lib::foo_test"),
320    /// );
321    ///
322    /// // Other kinds.
323    /// assert_eq!(
324    ///     RustBinaryId::from_parts("foo-lib", &RustTestBinaryKind::BIN, "foo_bin"),
325    ///     RustBinaryId::new("foo-lib::bin/foo_bin"),
326    /// );
327    /// ```
328    pub fn from_parts(package_name: &str, kind: &RustTestBinaryKind, target_name: &str) -> Self {
329        let mut id = package_name.to_owned();
330        // To ensure unique binary IDs, we use the following scheme:
331        if kind == &RustTestBinaryKind::LIB || kind == &RustTestBinaryKind::PROC_MACRO {
332            // 1. The binary ID is the same as the package name.
333        } else if kind == &RustTestBinaryKind::TEST {
334            // 2. For integration tests, use package_name::target_name. Cargo enforces unique names
335            //    for the same kind of targets in a package, so these will always be unique.
336            id.push_str("::");
337            id.push_str(target_name);
338        } else {
339            // 3. For all other target kinds, use a combination of the target kind and
340            //    the target name. For the same reason as above, these will always be
341            //    unique.
342            write!(id, "::{kind}/{target_name}").unwrap();
343        }
344
345        Self(id.into())
346    }
347
348    /// Returns the identifier as a string.
349    #[inline]
350    pub fn as_str(&self) -> &str {
351        &self.0
352    }
353
354    /// Returns the length of the identifier in bytes.
355    #[inline]
356    pub fn len(&self) -> usize {
357        self.0.len()
358    }
359
360    /// Returns `true` if the identifier is empty.
361    #[inline]
362    pub fn is_empty(&self) -> bool {
363        self.0.is_empty()
364    }
365
366    /// Returns the components of this identifier.
367    #[inline]
368    pub fn components(&self) -> RustBinaryIdComponents<'_> {
369        RustBinaryIdComponents::new(self)
370    }
371}
372
373impl<S> From<S> for RustBinaryId
374where
375    S: AsRef<str>,
376{
377    #[inline]
378    fn from(s: S) -> Self {
379        Self(s.as_ref().into())
380    }
381}
382
383impl Ord for RustBinaryId {
384    fn cmp(&self, other: &RustBinaryId) -> Ordering {
385        // Use the components as the canonical sort order.
386        //
387        // Note: this means that we can't impl Borrow<str> for RustBinaryId,
388        // since the Ord impl is inconsistent with that of &str.
389        self.components().cmp(&other.components())
390    }
391}
392
393impl PartialOrd for RustBinaryId {
394    fn partial_cmp(&self, other: &RustBinaryId) -> Option<Ordering> {
395        Some(self.cmp(other))
396    }
397}
398
399/// The components of a [`RustBinaryId`].
400///
401/// This defines the canonical sort order for a `RustBinaryId`.
402///
403/// Returned by [`RustBinaryId::components`].
404#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
405pub struct RustBinaryIdComponents<'a> {
406    /// The name of the package.
407    pub package_name: &'a str,
408
409    /// The kind and binary name, if specified.
410    pub binary_name_and_kind: RustBinaryIdNameAndKind<'a>,
411}
412
413impl<'a> RustBinaryIdComponents<'a> {
414    fn new(id: &'a RustBinaryId) -> Self {
415        let mut parts = id.as_str().splitn(2, "::");
416
417        let package_name = parts
418            .next()
419            .expect("splitn(2) returns at least 1 component");
420        let binary_name_and_kind = if let Some(suffix) = parts.next() {
421            let mut parts = suffix.splitn(2, '/');
422
423            let part1 = parts
424                .next()
425                .expect("splitn(2) returns at least 1 component");
426            if let Some(binary_name) = parts.next() {
427                RustBinaryIdNameAndKind::NameAndKind {
428                    kind: part1,
429                    binary_name,
430                }
431            } else {
432                RustBinaryIdNameAndKind::NameOnly { binary_name: part1 }
433            }
434        } else {
435            RustBinaryIdNameAndKind::None
436        };
437
438        Self {
439            package_name,
440            binary_name_and_kind,
441        }
442    }
443}
444
445/// The name and kind of a Rust binary, present within a [`RustBinaryId`].
446///
447/// Part of [`RustBinaryIdComponents`].
448#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
449pub enum RustBinaryIdNameAndKind<'a> {
450    /// The binary has no name or kind.
451    None,
452
453    /// The binary has a name but no kind.
454    NameOnly {
455        /// The name of the binary.
456        binary_name: &'a str,
457    },
458
459    /// The binary has a name and kind.
460    NameAndKind {
461        /// The kind of the binary.
462        kind: &'a str,
463
464        /// The name of the binary.
465        binary_name: &'a str,
466    },
467}
468
469/// Rust metadata used for builds and test runs.
470#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
471#[serde(rename_all = "kebab-case")]
472pub struct RustBuildMetaSummary {
473    /// The target directory for Rust artifacts.
474    pub target_directory: Utf8PathBuf,
475
476    /// Base output directories, relative to the target directory.
477    pub base_output_directories: BTreeSet<Utf8PathBuf>,
478
479    /// Information about non-test binaries, keyed by package ID.
480    pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
481
482    /// Build script output directory, relative to the target directory and keyed by package ID.
483    /// Only present for workspace packages that have build scripts.
484    ///
485    /// Added in cargo-nextest 0.9.65.
486    #[serde(default)]
487    pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
488
489    /// Linked paths, relative to the target directory.
490    pub linked_paths: BTreeSet<Utf8PathBuf>,
491
492    /// The build platforms used while compiling the Rust artifacts.
493    ///
494    /// Added in cargo-nextest 0.9.72.
495    #[serde(default)]
496    pub platforms: Option<BuildPlatformsSummary>,
497
498    /// The target platforms used while compiling the Rust artifacts.
499    ///
500    /// Deprecated in favor of [`Self::platforms`]; use that if available.
501    #[serde(default)]
502    pub target_platforms: Vec<PlatformSummary>,
503
504    /// A deprecated form of the target platform used for cross-compilation, if any.
505    ///
506    /// Deprecated in favor of (in order) [`Self::platforms`] and [`Self::target_platforms`]; use
507    /// those if available.
508    #[serde(default)]
509    pub target_platform: Option<String>,
510}
511
512/// A non-test Rust binary. Used to set the correct environment
513/// variables in reused builds.
514#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
515#[serde(rename_all = "kebab-case")]
516pub struct RustNonTestBinarySummary {
517    /// The name of the binary.
518    pub name: String,
519
520    /// The kind of binary this is.
521    pub kind: RustNonTestBinaryKind,
522
523    /// The path to the binary, relative to the target directory.
524    pub path: Utf8PathBuf,
525}
526
527/// Serialized representation of the host and the target platform.
528#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
529#[serde(rename_all = "kebab-case")]
530pub struct BuildPlatformsSummary {
531    /// The host platform used while compiling the Rust artifacts.
532    pub host: HostPlatformSummary,
533
534    /// The target platforms used while compiling the Rust artifacts.
535    ///
536    /// With current versions of nextest, this will contain at most one element.
537    pub targets: Vec<TargetPlatformSummary>,
538}
539
540/// Serialized representation of the host platform.
541#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
542#[serde(rename_all = "kebab-case")]
543pub struct HostPlatformSummary {
544    /// The host platform, if specified.
545    pub platform: PlatformSummary,
546
547    /// The libdir for the host platform.
548    pub libdir: PlatformLibdirSummary,
549}
550
551/// Serialized representation of the target platform.
552#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
553#[serde(rename_all = "kebab-case")]
554pub struct TargetPlatformSummary {
555    /// The target platform, if specified.
556    pub platform: PlatformSummary,
557
558    /// The libdir for the target platform.
559    ///
560    /// Err if we failed to discover it.
561    pub libdir: PlatformLibdirSummary,
562}
563
564/// Serialized representation of a platform's library directory.
565#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
566#[serde(tag = "status", rename_all = "kebab-case")]
567pub enum PlatformLibdirSummary {
568    /// The libdir is available.
569    Available {
570        /// The libdir.
571        path: Utf8PathBuf,
572    },
573
574    /// The libdir is unavailable, for the reason provided in the inner value.
575    Unavailable {
576        /// The reason why the libdir is unavailable.
577        reason: PlatformLibdirUnavailable,
578    },
579}
580
581/// The reason why a platform libdir is unavailable.
582///
583/// Part of [`PlatformLibdirSummary`].
584///
585/// This is an open-ended enum that may have additional deserializable variants in the future.
586#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
587pub struct PlatformLibdirUnavailable(pub Cow<'static, str>);
588
589impl PlatformLibdirUnavailable {
590    /// The libdir is not available because the rustc invocation to obtain it failed.
591    pub const RUSTC_FAILED: Self = Self::new_const("rustc-failed");
592
593    /// The libdir is not available because it was attempted to be read from rustc, but there was an
594    /// issue with its output.
595    pub const RUSTC_OUTPUT_ERROR: Self = Self::new_const("rustc-output-error");
596
597    /// The libdir is unavailable because it was deserialized from a summary serialized by an older
598    /// version of nextest.
599    pub const OLD_SUMMARY: Self = Self::new_const("old-summary");
600
601    /// The libdir is unavailable because a build was reused from an archive, and the libdir was not
602    /// present in the archive
603    pub const NOT_IN_ARCHIVE: Self = Self::new_const("not-in-archive");
604
605    /// Converts a static string into Self.
606    pub const fn new_const(reason: &'static str) -> Self {
607        Self(Cow::Borrowed(reason))
608    }
609
610    /// Converts a string into Self.
611    pub fn new(reason: impl Into<Cow<'static, str>>) -> Self {
612        Self(reason.into())
613    }
614
615    /// Returns self as a string.
616    pub fn as_str(&self) -> &str {
617        &self.0
618    }
619}
620
621/// Information about the kind of a Rust non-test binary.
622///
623/// This is part of [`RustNonTestBinarySummary`], and is used to determine runtime environment
624/// variables.
625#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
626#[serde(transparent)]
627pub struct RustNonTestBinaryKind(pub Cow<'static, str>);
628
629impl RustNonTestBinaryKind {
630    /// Creates a new `RustNonTestBinaryKind` from a string.
631    #[inline]
632    pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
633        Self(kind.into())
634    }
635
636    /// Creates a new `RustNonTestBinaryKind` from a static string.
637    #[inline]
638    pub const fn new_const(kind: &'static str) -> Self {
639        Self(Cow::Borrowed(kind))
640    }
641
642    /// Returns the kind as a string.
643    pub fn as_str(&self) -> &str {
644        &self.0
645    }
646
647    /// The "dylib" kind, used for dynamic libraries (`.so` on Linux). Also used for
648    /// .pdb and other similar files on Windows.
649    pub const DYLIB: Self = Self::new_const("dylib");
650
651    /// The "bin-exe" kind, used for binary executables.
652    pub const BIN_EXE: Self = Self::new_const("bin-exe");
653}
654
655impl fmt::Display for RustNonTestBinaryKind {
656    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657        write!(f, "{}", self.0)
658    }
659}
660
661/// A serializable suite of tests within a Rust test binary.
662///
663/// Part of a [`TestListSummary`].
664#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
665#[serde(rename_all = "kebab-case")]
666pub struct RustTestSuiteSummary {
667    /// The name of this package in the workspace.
668    pub package_name: String,
669
670    /// The binary within the package.
671    #[serde(flatten)]
672    pub binary: RustTestBinarySummary,
673
674    /// The working directory that tests within this package are run in.
675    pub cwd: Utf8PathBuf,
676
677    /// Status of this test suite.
678    ///
679    /// Introduced in cargo-nextest 0.9.25. Older versions always imply
680    /// [`LISTED`](RustTestSuiteStatusSummary::LISTED).
681    #[serde(default = "listed_status")]
682    pub status: RustTestSuiteStatusSummary,
683
684    /// Test cases within this test suite.
685    #[serde(rename = "testcases")]
686    pub test_cases: BTreeMap<String, RustTestCaseSummary>,
687}
688
689fn listed_status() -> RustTestSuiteStatusSummary {
690    RustTestSuiteStatusSummary::LISTED
691}
692
693/// Information about whether a test suite was listed or skipped.
694///
695/// This is part of [`RustTestSuiteSummary`].
696#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
697#[serde(transparent)]
698pub struct RustTestSuiteStatusSummary(pub Cow<'static, str>);
699
700impl RustTestSuiteStatusSummary {
701    /// Creates a new `RustNonTestBinaryKind` from a string.
702    #[inline]
703    pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
704        Self(kind.into())
705    }
706
707    /// Creates a new `RustNonTestBinaryKind` from a static string.
708    #[inline]
709    pub const fn new_const(kind: &'static str) -> Self {
710        Self(Cow::Borrowed(kind))
711    }
712
713    /// Returns the kind as a string.
714    pub fn as_str(&self) -> &str {
715        &self.0
716    }
717
718    /// The "listed" kind, which means that the test binary was executed with `--list` to gather the
719    /// list of tests in it.
720    pub const LISTED: Self = Self::new_const("listed");
721
722    /// The "skipped" kind, which indicates that the test binary was not executed because it didn't
723    /// match any filtersets.
724    ///
725    /// In this case, the contents of [`RustTestSuiteSummary::test_cases`] is empty.
726    pub const SKIPPED: Self = Self::new_const("skipped");
727
728    /// The binary doesn't match the profile's `default-filter`.
729    ///
730    /// This is the lowest-priority reason for skipping a binary.
731    pub const SKIPPED_DEFAULT_FILTER: Self = Self::new_const("skipped-default-filter");
732}
733
734/// Serializable information about an individual test case within a Rust test suite.
735///
736/// Part of a [`RustTestSuiteSummary`].
737#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
738#[serde(rename_all = "kebab-case")]
739pub struct RustTestCaseSummary {
740    /// Returns true if this test is marked ignored.
741    ///
742    /// Ignored tests, if run, are executed with the `--ignored` argument.
743    pub ignored: bool,
744
745    /// Whether the test matches the provided test filter.
746    ///
747    /// Only tests that match the filter are run.
748    pub filter_match: FilterMatch,
749}
750
751/// An enum describing whether a test matches a filter.
752#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
753#[serde(rename_all = "kebab-case", tag = "status")]
754pub enum FilterMatch {
755    /// This test matches this filter.
756    Matches,
757
758    /// This test does not match this filter.
759    Mismatch {
760        /// Describes the reason this filter isn't matched.
761        reason: MismatchReason,
762    },
763}
764
765impl FilterMatch {
766    /// Returns true if the filter doesn't match.
767    pub fn is_match(&self) -> bool {
768        matches!(self, FilterMatch::Matches)
769    }
770}
771
772/// The reason for why a test doesn't match a filter.
773#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
774#[serde(rename_all = "kebab-case")]
775#[non_exhaustive]
776pub enum MismatchReason {
777    /// This test does not match the run-ignored option in the filter.
778    Ignored,
779
780    /// This test does not match the provided string filters.
781    String,
782
783    /// This test does not match the provided expression filters.
784    Expression,
785
786    /// This test is in a different partition.
787    Partition,
788
789    /// This test is filtered out by the default-filter.
790    ///
791    /// This is the lowest-priority reason for skipping a test.
792    DefaultFilter,
793}
794
795impl fmt::Display for MismatchReason {
796    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
797        match self {
798            MismatchReason::Ignored => write!(f, "does not match the run-ignored option"),
799            MismatchReason::String => write!(f, "does not match the provided string filters"),
800            MismatchReason::Expression => {
801                write!(f, "does not match the provided expression filters")
802            }
803            MismatchReason::Partition => write!(f, "is in a different partition"),
804            MismatchReason::DefaultFilter => {
805                write!(f, "is filtered out by the profile's default-filter")
806            }
807        }
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814    use test_case::test_case;
815
816    #[test_case(r#"{
817        "target-directory": "/foo",
818        "base-output-directories": [],
819        "non-test-binaries": {},
820        "linked-paths": []
821    }"#, RustBuildMetaSummary {
822        target_directory: "/foo".into(),
823        base_output_directories: BTreeSet::new(),
824        non_test_binaries: BTreeMap::new(),
825        build_script_out_dirs: BTreeMap::new(),
826        linked_paths: BTreeSet::new(),
827        target_platform: None,
828        target_platforms: vec![],
829        platforms: None,
830    }; "no target platform")]
831    #[test_case(r#"{
832        "target-directory": "/foo",
833        "base-output-directories": [],
834        "non-test-binaries": {},
835        "linked-paths": [],
836        "target-platform": "x86_64-unknown-linux-gnu"
837    }"#, RustBuildMetaSummary {
838        target_directory: "/foo".into(),
839        base_output_directories: BTreeSet::new(),
840        non_test_binaries: BTreeMap::new(),
841        build_script_out_dirs: BTreeMap::new(),
842        linked_paths: BTreeSet::new(),
843        target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
844        target_platforms: vec![],
845        platforms: None,
846    }; "single target platform specified")]
847    fn test_deserialize_old_rust_build_meta(input: &str, expected: RustBuildMetaSummary) {
848        let build_meta: RustBuildMetaSummary =
849            serde_json::from_str(input).expect("input deserialized correctly");
850        assert_eq!(
851            build_meta, expected,
852            "deserialized input matched expected output"
853        );
854    }
855
856    #[test]
857    fn test_binary_id_ord() {
858        let empty = RustBinaryId::new("");
859        let foo = RustBinaryId::new("foo");
860        let bar = RustBinaryId::new("bar");
861        let foo_name1 = RustBinaryId::new("foo::name1");
862        let foo_name2 = RustBinaryId::new("foo::name2");
863        let bar_name = RustBinaryId::new("bar::name");
864        let foo_bin_name1 = RustBinaryId::new("foo::bin/name1");
865        let foo_bin_name2 = RustBinaryId::new("foo::bin/name2");
866        let bar_bin_name = RustBinaryId::new("bar::bin/name");
867        let foo_proc_macro_name = RustBinaryId::new("foo::proc_macro/name");
868        let bar_proc_macro_name = RustBinaryId::new("bar::proc_macro/name");
869
870        // This defines the expected sort order.
871        let sorted_ids = [
872            empty,
873            bar,
874            bar_name,
875            bar_bin_name,
876            bar_proc_macro_name,
877            foo,
878            foo_name1,
879            foo_name2,
880            foo_bin_name1,
881            foo_bin_name2,
882            foo_proc_macro_name,
883        ];
884
885        for (i, id) in sorted_ids.iter().enumerate() {
886            for (j, other_id) in sorted_ids.iter().enumerate() {
887                let expected = i.cmp(&j);
888                assert_eq!(
889                    id.cmp(other_id),
890                    expected,
891                    "comparing {id:?} to {other_id:?} gave {expected:?}"
892                );
893            }
894        }
895    }
896}