Skip to main content

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/// The string `"@global"`, representing the implicit global test group.
19///
20/// All tests belong to `@global` unless assigned to a custom group by
21/// a per-test override.
22pub const GLOBAL_TEST_GROUP: &str = "@global";
23
24/// Command builder for `cargo nextest list`.
25#[derive(Clone, Debug, Default)]
26pub struct ListCommand {
27    cargo_path: Option<Box<Utf8Path>>,
28    manifest_path: Option<Box<Utf8Path>>,
29    current_dir: Option<Box<Utf8Path>>,
30    args: Vec<Box<str>>,
31}
32
33impl ListCommand {
34    /// Creates a new `ListCommand`.
35    ///
36    /// This command runs `cargo nextest list`.
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Path to `cargo` executable. If not set, this will use the the `$CARGO` environment variable, and
42    /// if that is not set, will simply be `cargo`.
43    pub fn cargo_path(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
44        self.cargo_path = Some(path.into().into());
45        self
46    }
47
48    /// Path to `Cargo.toml`.
49    pub fn manifest_path(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
50        self.manifest_path = Some(path.into().into());
51        self
52    }
53
54    /// Current directory of the `cargo nextest list` process.
55    pub fn current_dir(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
56        self.current_dir = Some(path.into().into());
57        self
58    }
59
60    /// Adds an argument to the end of `cargo nextest list`.
61    pub fn add_arg(&mut self, arg: impl Into<String>) -> &mut Self {
62        self.args.push(arg.into().into());
63        self
64    }
65
66    /// Adds several arguments to the end of `cargo nextest list`.
67    pub fn add_args(&mut self, args: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
68        for arg in args {
69            self.add_arg(arg.into());
70        }
71        self
72    }
73
74    /// Builds a command for `cargo nextest list`. This is the first part of the
75    /// work of [`Self::exec`].
76    pub fn cargo_command(&self) -> Command {
77        let cargo_path: PathBuf = self.cargo_path.as_ref().map_or_else(
78            || std::env::var_os("CARGO").map_or("cargo".into(), PathBuf::from),
79            |path| PathBuf::from(path.as_std_path()),
80        );
81
82        let mut command = Command::new(cargo_path);
83        if let Some(path) = &self.manifest_path.as_deref() {
84            command.args(["--manifest-path", path.as_str()]);
85        }
86        if let Some(current_dir) = &self.current_dir.as_deref() {
87            command.current_dir(current_dir);
88        }
89
90        command.args(["nextest", "list", "--message-format=json"]);
91
92        command.args(self.args.iter().map(|s| s.as_ref()));
93        command
94    }
95
96    /// Executes `cargo nextest list` and parses the output into a [`TestListSummary`].
97    pub fn exec(&self) -> Result<TestListSummary, CommandError> {
98        let mut command = self.cargo_command();
99        let output = command.output().map_err(CommandError::Exec)?;
100
101        if !output.status.success() {
102            // The process exited with a non-zero code.
103            let exit_code = output.status.code();
104            let stderr = output.stderr;
105            return Err(CommandError::CommandFailed { exit_code, stderr });
106        }
107
108        // Try parsing stdout.
109        serde_json::from_slice(&output.stdout).map_err(CommandError::Json)
110    }
111
112    /// Executes `cargo nextest list --list-type binaries-only` and parses the output into a
113    /// [`BinaryListSummary`].
114    pub fn exec_binaries_only(&self) -> Result<BinaryListSummary, CommandError> {
115        let mut command = self.cargo_command();
116        command.arg("--list-type=binaries-only");
117        let output = command.output().map_err(CommandError::Exec)?;
118
119        if !output.status.success() {
120            // The process exited with a non-zero code.
121            let exit_code = output.status.code();
122            let stderr = output.stderr;
123            return Err(CommandError::CommandFailed { exit_code, stderr });
124        }
125
126        // Try parsing stdout.
127        serde_json::from_slice(&output.stdout).map_err(CommandError::Json)
128    }
129}
130
131/// Root element for a serializable list of tests generated by nextest.
132#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
133#[serde(rename_all = "kebab-case")]
134#[non_exhaustive]
135pub struct TestListSummary {
136    /// Rust metadata used for builds and test runs.
137    pub rust_build_meta: RustBuildMetaSummary,
138
139    /// Number of tests (including skipped and ignored) across all binaries.
140    pub test_count: usize,
141
142    /// A map of Rust test suites to the test binaries within them, keyed by a unique identifier
143    /// for each test suite.
144    pub rust_suites: BTreeMap<RustBinaryId, RustTestSuiteSummary>,
145}
146
147impl TestListSummary {
148    /// Creates a new `TestListSummary` with the given Rust metadata.
149    pub fn new(rust_build_meta: RustBuildMetaSummary) -> Self {
150        Self {
151            rust_build_meta,
152            test_count: 0,
153            rust_suites: BTreeMap::new(),
154        }
155    }
156    /// Parse JSON output from `cargo nextest list --message-format json`.
157    pub fn parse_json(json: impl AsRef<str>) -> Result<Self, serde_json::Error> {
158        serde_json::from_str(json.as_ref())
159    }
160}
161
162/// The platform a binary was built for: the host or the target.
163///
164/// This is relevant for cross-compilation; if that isn't occurring, binaries
165/// are built for the target platform.
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
167#[serde(rename_all = "kebab-case")]
168pub enum BuildPlatform {
169    /// The target platform.
170    Target,
171
172    /// The host platform: the platform the build was performed on.
173    Host,
174}
175
176impl fmt::Display for BuildPlatform {
177    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
178        match self {
179            Self::Target => write!(f, "target"),
180            Self::Host => write!(f, "host"),
181        }
182    }
183}
184
185/// A serializable Rust test binary.
186///
187/// Part of a [`RustTestSuiteSummary`] and [`BinaryListSummary`].
188#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
189#[serde(rename_all = "kebab-case")]
190pub struct RustTestBinarySummary {
191    /// A unique binary ID.
192    pub binary_id: RustBinaryId,
193
194    /// The name of the test binary within the package.
195    pub binary_name: String,
196
197    /// The unique package ID assigned by Cargo to this test.
198    ///
199    /// This package ID can be used for lookups in `cargo metadata`.
200    pub package_id: String,
201
202    /// The kind of Rust test binary this is.
203    pub kind: RustTestBinaryKind,
204
205    /// The path to the test binary executable.
206    pub binary_path: Utf8PathBuf,
207
208    /// Platform for which this binary was built.
209    /// (Proc-macro tests are built for the host.)
210    pub build_platform: BuildPlatform,
211}
212
213/// Information about the kind of a Rust test binary.
214///
215/// Kinds are used to generate [`RustBinaryId`] instances, and to figure out whether some
216/// environment variables should be set.
217#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
218#[serde(transparent)]
219pub struct RustTestBinaryKind(pub Cow<'static, str>);
220
221impl RustTestBinaryKind {
222    /// Creates a new `RustTestBinaryKind` from a string.
223    #[inline]
224    pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
225        Self(kind.into())
226    }
227
228    /// Creates a new `RustTestBinaryKind` from a static string.
229    #[inline]
230    pub const fn new_const(kind: &'static str) -> Self {
231        Self(Cow::Borrowed(kind))
232    }
233
234    /// Returns the kind as a string.
235    pub fn as_str(&self) -> &str {
236        &self.0
237    }
238
239    /// The "lib" kind, used for unit tests within the library.
240    pub const LIB: Self = Self::new_const("lib");
241
242    /// The "test" kind, used for integration tests.
243    pub const TEST: Self = Self::new_const("test");
244
245    /// The "bench" kind, used for benchmarks.
246    pub const BENCH: Self = Self::new_const("bench");
247
248    /// The "bin" kind, used for unit tests within binaries.
249    pub const BIN: Self = Self::new_const("bin");
250
251    /// The "example" kind, used for unit tests within examples.
252    pub const EXAMPLE: Self = Self::new_const("example");
253
254    /// The "proc-macro" kind, used for tests within procedural macros.
255    pub const PROC_MACRO: Self = Self::new_const("proc-macro");
256}
257
258impl fmt::Display for RustTestBinaryKind {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        write!(f, "{}", self.0)
261    }
262}
263
264/// A serializable suite of test binaries.
265#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
266#[serde(rename_all = "kebab-case")]
267pub struct BinaryListSummary {
268    /// Rust metadata used for builds and test runs.
269    pub rust_build_meta: RustBuildMetaSummary,
270
271    /// The list of Rust test binaries (indexed by binary-id).
272    pub rust_binaries: BTreeMap<RustBinaryId, RustTestBinarySummary>,
273}
274
275// IMPLEMENTATION NOTE: SmolStr is *not* part of the public API.
276
277/// A unique identifier for a test suite (a Rust binary).
278#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
279#[serde(transparent)]
280pub struct RustBinaryId(SmolStr);
281
282impl fmt::Display for RustBinaryId {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        f.write_str(&self.0)
285    }
286}
287
288impl RustBinaryId {
289    /// Creates a new `RustBinaryId` from a string.
290    #[inline]
291    pub fn new(id: &str) -> Self {
292        Self(id.into())
293    }
294
295    /// Creates a new `RustBinaryId` from its constituent parts:
296    ///
297    /// * `package_name`: The name of the package as defined in `Cargo.toml`.
298    /// * `kind`: The kind of the target (see [`RustTestBinaryKind`]).
299    /// * `target_name`: The name of the target.
300    ///
301    /// The algorithm is as follows:
302    ///
303    /// 1. If the kind is `lib` or `proc-macro` (i.e. for unit tests), the binary ID is the same as
304    ///    the package name. There can only be one library per package, so this will always be
305    ///    unique.
306    /// 2. If the target is an integration test, the binary ID is `package_name::target_name`.
307    /// 3. Otherwise, the binary ID is `package_name::{kind}/{target_name}`.
308    ///
309    /// This format is part of nextest's stable API.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// use nextest_metadata::{RustBinaryId, RustTestBinaryKind};
315    ///
316    /// // The lib and proc-macro kinds.
317    /// assert_eq!(
318    ///     RustBinaryId::from_parts("foo-lib", &RustTestBinaryKind::LIB, "foo_lib"),
319    ///     RustBinaryId::new("foo-lib"),
320    /// );
321    /// assert_eq!(
322    ///     RustBinaryId::from_parts("foo-derive", &RustTestBinaryKind::PROC_MACRO, "derive"),
323    ///     RustBinaryId::new("foo-derive"),
324    /// );
325    ///
326    /// // Integration tests.
327    /// assert_eq!(
328    ///     RustBinaryId::from_parts("foo-lib", &RustTestBinaryKind::TEST, "foo_test"),
329    ///     RustBinaryId::new("foo-lib::foo_test"),
330    /// );
331    ///
332    /// // Other kinds.
333    /// assert_eq!(
334    ///     RustBinaryId::from_parts("foo-lib", &RustTestBinaryKind::BIN, "foo_bin"),
335    ///     RustBinaryId::new("foo-lib::bin/foo_bin"),
336    /// );
337    /// ```
338    pub fn from_parts(package_name: &str, kind: &RustTestBinaryKind, target_name: &str) -> Self {
339        let mut id = package_name.to_owned();
340        // To ensure unique binary IDs, we use the following scheme:
341        if kind == &RustTestBinaryKind::LIB || kind == &RustTestBinaryKind::PROC_MACRO {
342            // 1. The binary ID is the same as the package name.
343        } else if kind == &RustTestBinaryKind::TEST {
344            // 2. For integration tests, use package_name::target_name. Cargo enforces unique names
345            //    for the same kind of targets in a package, so these will always be unique.
346            id.push_str("::");
347            id.push_str(target_name);
348        } else {
349            // 3. For all other target kinds, use a combination of the target kind and
350            //    the target name. For the same reason as above, these will always be
351            //    unique.
352            write!(id, "::{kind}/{target_name}").unwrap();
353        }
354
355        Self(id.into())
356    }
357
358    /// Returns the identifier as a string.
359    #[inline]
360    pub fn as_str(&self) -> &str {
361        &self.0
362    }
363
364    /// Returns the length of the identifier in bytes.
365    #[inline]
366    pub fn len(&self) -> usize {
367        self.0.len()
368    }
369
370    /// Returns `true` if the identifier is empty.
371    #[inline]
372    pub fn is_empty(&self) -> bool {
373        self.0.is_empty()
374    }
375
376    /// Returns the components of this identifier.
377    #[inline]
378    pub fn components(&self) -> RustBinaryIdComponents<'_> {
379        RustBinaryIdComponents::new(self)
380    }
381}
382
383impl<S> From<S> for RustBinaryId
384where
385    S: AsRef<str>,
386{
387    #[inline]
388    fn from(s: S) -> Self {
389        Self(s.as_ref().into())
390    }
391}
392
393impl Ord for RustBinaryId {
394    fn cmp(&self, other: &RustBinaryId) -> Ordering {
395        // Use the components as the canonical sort order.
396        //
397        // Note: this means that we can't impl Borrow<str> for RustBinaryId,
398        // since the Ord impl is inconsistent with that of &str.
399        self.components().cmp(&other.components())
400    }
401}
402
403impl PartialOrd for RustBinaryId {
404    fn partial_cmp(&self, other: &RustBinaryId) -> Option<Ordering> {
405        Some(self.cmp(other))
406    }
407}
408
409/// The components of a [`RustBinaryId`].
410///
411/// This defines the canonical sort order for a `RustBinaryId`.
412///
413/// Returned by [`RustBinaryId::components`].
414#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
415pub struct RustBinaryIdComponents<'a> {
416    /// The name of the package.
417    pub package_name: &'a str,
418
419    /// The kind and binary name, if specified.
420    pub binary_name_and_kind: RustBinaryIdNameAndKind<'a>,
421}
422
423impl<'a> RustBinaryIdComponents<'a> {
424    fn new(id: &'a RustBinaryId) -> Self {
425        let mut parts = id.as_str().splitn(2, "::");
426
427        let package_name = parts
428            .next()
429            .expect("splitn(2) returns at least 1 component");
430        let binary_name_and_kind = if let Some(suffix) = parts.next() {
431            let mut parts = suffix.splitn(2, '/');
432
433            let part1 = parts
434                .next()
435                .expect("splitn(2) returns at least 1 component");
436            if let Some(binary_name) = parts.next() {
437                RustBinaryIdNameAndKind::NameAndKind {
438                    kind: part1,
439                    binary_name,
440                }
441            } else {
442                RustBinaryIdNameAndKind::NameOnly { binary_name: part1 }
443            }
444        } else {
445            RustBinaryIdNameAndKind::None
446        };
447
448        Self {
449            package_name,
450            binary_name_and_kind,
451        }
452    }
453}
454
455/// The name and kind of a Rust binary, present within a [`RustBinaryId`].
456///
457/// Part of [`RustBinaryIdComponents`].
458#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
459pub enum RustBinaryIdNameAndKind<'a> {
460    /// The binary has no name or kind.
461    None,
462
463    /// The binary has a name but no kind.
464    NameOnly {
465        /// The name of the binary.
466        binary_name: &'a str,
467    },
468
469    /// The binary has a name and kind.
470    NameAndKind {
471        /// The kind of the binary.
472        kind: &'a str,
473
474        /// The name of the binary.
475        binary_name: &'a str,
476    },
477}
478
479/// The name of a test case within a binary.
480///
481/// This is the identifier for an individual test within a Rust test binary.
482/// Test case names are typically the full path to the test function, like
483/// `module::submodule::test_name`.
484#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
485#[serde(transparent)]
486pub struct TestCaseName(SmolStr);
487
488impl fmt::Display for TestCaseName {
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        f.write_str(&self.0)
491    }
492}
493
494impl TestCaseName {
495    /// Creates a new `TestCaseName` from a string.
496    #[inline]
497    pub fn new(name: &str) -> Self {
498        Self(name.into())
499    }
500
501    /// Returns the name as a string.
502    #[inline]
503    pub fn as_str(&self) -> &str {
504        &self.0
505    }
506
507    /// Returns the name as bytes.
508    #[inline]
509    pub fn as_bytes(&self) -> &[u8] {
510        self.0.as_bytes()
511    }
512
513    /// Returns the length of the name in bytes.
514    #[inline]
515    pub fn len(&self) -> usize {
516        self.0.len()
517    }
518
519    /// Returns `true` if the name is empty.
520    #[inline]
521    pub fn is_empty(&self) -> bool {
522        self.0.is_empty()
523    }
524
525    /// Returns `true` if the name contains the given pattern.
526    #[inline]
527    pub fn contains(&self, pattern: &str) -> bool {
528        self.0.contains(pattern)
529    }
530
531    /// Returns an iterator over the `::` separated components of this test case name.
532    ///
533    /// Test case names typically follow Rust's module path syntax, like
534    /// `module::submodule::test_name`. This method splits on `::` to yield each component.
535    ///
536    /// # Examples
537    ///
538    /// ```
539    /// use nextest_metadata::TestCaseName;
540    ///
541    /// let name = TestCaseName::new("foo::bar::test_baz");
542    /// let components: Vec<_> = name.components().collect();
543    /// assert_eq!(components, vec!["foo", "bar", "test_baz"]);
544    ///
545    /// let simple = TestCaseName::new("test_simple");
546    /// let components: Vec<_> = simple.components().collect();
547    /// assert_eq!(components, vec!["test_simple"]);
548    /// ```
549    #[inline]
550    pub fn components(&self) -> std::str::Split<'_, &str> {
551        self.0.split("::")
552    }
553
554    /// Splits the test case name into a module path prefix and trailing name.
555    ///
556    /// Returns `(Some(module_path), name)` if the test case name contains `::`,
557    /// or `(None, name)` if it doesn't.
558    ///
559    /// # Examples
560    ///
561    /// ```
562    /// use nextest_metadata::TestCaseName;
563    ///
564    /// let name = TestCaseName::new("foo::bar::test_baz");
565    /// assert_eq!(name.module_path_and_name(), (Some("foo::bar"), "test_baz"));
566    ///
567    /// let simple = TestCaseName::new("test_simple");
568    /// assert_eq!(simple.module_path_and_name(), (None, "test_simple"));
569    /// ```
570    #[inline]
571    pub fn module_path_and_name(&self) -> (Option<&str>, &str) {
572        match self.0.rsplit_once("::") {
573            Some((module_path, name)) => (Some(module_path), name),
574            None => (None, &self.0),
575        }
576    }
577}
578
579impl AsRef<str> for TestCaseName {
580    #[inline]
581    fn as_ref(&self) -> &str {
582        &self.0
583    }
584}
585
586/// Rust metadata used for builds and test runs.
587#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
588#[serde(rename_all = "kebab-case")]
589pub struct RustBuildMetaSummary {
590    /// The target directory for Rust artifacts.
591    pub target_directory: Utf8PathBuf,
592
593    /// The build directory for intermediate Cargo artifacts (test binaries,
594    /// build script outputs, etc.). When Cargo's `build.build-dir` is
595    /// configured, this differs from `target_directory`. Otherwise it equals
596    /// `target_directory`.
597    ///
598    /// Absent in archives and metadata from older nextest versions (pre-0.9.131),
599    /// in which case consumers should treat it as equal to `target_directory`.
600    ///
601    /// Added in cargo-nextest 0.9.131.
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    pub build_directory: Option<Utf8PathBuf>,
604
605    /// Base output directories, of the form `[<triple>/]<profile>`, stored
606    /// relative to the build directory.
607    ///
608    /// To reconstruct the dynamic library search path, resolve each entry `E`
609    /// against both of:
610    ///
611    /// * the Cargo artifact directory, where Cargo uplifts final artifacts to:
612    ///   `target_directory.join(E)`
613    /// * the `deps` directory (legacy layout only): `build_directory.join(E).join("deps")`
614    pub base_output_directories: BTreeSet<Utf8PathBuf>,
615
616    /// Information about non-test binaries, keyed by package ID.
617    pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
618
619    /// Build script output directory, relative to the build directory and keyed
620    /// by package ID. Only present for workspace packages that have build
621    /// scripts.
622    ///
623    /// Added in cargo-nextest 0.9.65.
624    #[serde(default)]
625    pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
626
627    /// Extended build script information, keyed by package ID. Only present for workspace
628    /// packages that have build scripts.
629    ///
630    /// `None` means this field was absent (old archive/metadata that predates this field).
631    /// `Some(map)` means the field was present, even if the map is empty.
632    ///
633    /// Added in cargo-nextest 0.9.131.
634    #[serde(default)]
635    pub build_script_info: Option<BTreeMap<String, BuildScriptInfoSummary>>,
636
637    /// Linked paths, relative to the build directory.
638    pub linked_paths: BTreeSet<Utf8PathBuf>,
639
640    /// The build platforms used while compiling the Rust artifacts.
641    ///
642    /// Added in cargo-nextest 0.9.72.
643    #[serde(default)]
644    pub platforms: Option<BuildPlatformsSummary>,
645
646    /// The target platforms used while compiling the Rust artifacts.
647    ///
648    /// Deprecated in favor of [`Self::platforms`]; use that if available.
649    #[serde(default)]
650    pub target_platforms: Vec<PlatformSummary>,
651
652    /// A deprecated form of the target platform used for cross-compilation, if any.
653    ///
654    /// Deprecated in favor of (in order) [`Self::platforms`] and [`Self::target_platforms`]; use
655    /// those if available.
656    #[serde(default)]
657    pub target_platform: Option<String>,
658}
659
660/// Extended build script information for a single package.
661///
662/// This struct is extensible; new fields may be added in the future. Use
663/// `#[serde(default)]` when deserializing.
664#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
665#[serde(rename_all = "kebab-case")]
666pub struct BuildScriptInfoSummary {
667    /// Environment variables set by the build script via `cargo::rustc-env`
668    /// directives.
669    #[serde(default)]
670    pub envs: BTreeMap<String, String>,
671}
672
673/// A non-test Rust binary. Used to set the correct environment
674/// variables in reused builds.
675#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
676#[serde(rename_all = "kebab-case")]
677pub struct RustNonTestBinarySummary {
678    /// The name of the binary.
679    pub name: String,
680
681    /// The kind of binary this is.
682    pub kind: RustNonTestBinaryKind,
683
684    /// The path to the binary, relative to the target directory.
685    pub path: Utf8PathBuf,
686
687    /// The platform the binary was built for.
688    ///
689    /// In current versions of nextest, this is heuristically determined due to
690    /// a [Cargo limitation](https://github.com/rust-lang/cargo/issues/12869).
691    ///
692    /// Added in cargo-nextest 0.9.141.
693    #[serde(default, skip_serializing_if = "Option::is_none")]
694    pub build_platform: Option<BuildPlatform>,
695}
696
697/// Serialized representation of the host and the target platform.
698#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
699#[serde(rename_all = "kebab-case")]
700pub struct BuildPlatformsSummary {
701    /// The host platform used while compiling the Rust artifacts.
702    pub host: HostPlatformSummary,
703
704    /// The target platforms used while compiling the Rust artifacts.
705    ///
706    /// With current versions of nextest, this will contain at most one element.
707    pub targets: Vec<TargetPlatformSummary>,
708}
709
710/// Serialized representation of the host platform.
711#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
712#[serde(rename_all = "kebab-case")]
713pub struct HostPlatformSummary {
714    /// The host platform, if specified.
715    pub platform: PlatformSummary,
716
717    /// The libdir for the host platform.
718    pub libdir: PlatformLibdirSummary,
719}
720
721/// Serialized representation of the target platform.
722#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
723#[serde(rename_all = "kebab-case")]
724pub struct TargetPlatformSummary {
725    /// The target platform, if specified.
726    pub platform: PlatformSummary,
727
728    /// The libdir for the target platform.
729    ///
730    /// Err if we failed to discover it.
731    pub libdir: PlatformLibdirSummary,
732}
733
734/// Serialized representation of a platform's library directory.
735#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
736#[serde(tag = "status", rename_all = "kebab-case")]
737pub enum PlatformLibdirSummary {
738    /// The libdir is available.
739    Available {
740        /// The libdir.
741        path: Utf8PathBuf,
742    },
743
744    /// The libdir is unavailable, for the reason provided in the inner value.
745    Unavailable {
746        /// The reason why the libdir is unavailable.
747        reason: PlatformLibdirUnavailable,
748    },
749}
750
751/// The reason why a platform libdir is unavailable.
752///
753/// Part of [`PlatformLibdirSummary`].
754///
755/// This is an open-ended enum that may have additional deserializable variants in the future.
756#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
757pub struct PlatformLibdirUnavailable(pub Cow<'static, str>);
758
759impl PlatformLibdirUnavailable {
760    /// The libdir is not available because the rustc invocation to obtain it failed.
761    pub const RUSTC_FAILED: Self = Self::new_const("rustc-failed");
762
763    /// The libdir is not available because it was attempted to be read from rustc, but there was an
764    /// issue with its output.
765    pub const RUSTC_OUTPUT_ERROR: Self = Self::new_const("rustc-output-error");
766
767    /// The libdir is unavailable because it was deserialized from a summary serialized by an older
768    /// version of nextest.
769    pub const OLD_SUMMARY: Self = Self::new_const("old-summary");
770
771    /// The libdir is unavailable because a build was reused from an archive, and the libdir was not
772    /// present in the archive
773    pub const NOT_IN_ARCHIVE: Self = Self::new_const("not-in-archive");
774
775    /// Converts a static string into Self.
776    pub const fn new_const(reason: &'static str) -> Self {
777        Self(Cow::Borrowed(reason))
778    }
779
780    /// Converts a string into Self.
781    pub fn new(reason: impl Into<Cow<'static, str>>) -> Self {
782        Self(reason.into())
783    }
784
785    /// Returns self as a string.
786    pub fn as_str(&self) -> &str {
787        &self.0
788    }
789}
790
791/// Information about the kind of a Rust non-test binary.
792///
793/// This is part of [`RustNonTestBinarySummary`], and is used to determine runtime environment
794/// variables.
795#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
796#[serde(transparent)]
797pub struct RustNonTestBinaryKind(pub Cow<'static, str>);
798
799impl RustNonTestBinaryKind {
800    /// Creates a new `RustNonTestBinaryKind` from a string.
801    #[inline]
802    pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
803        Self(kind.into())
804    }
805
806    /// Creates a new `RustNonTestBinaryKind` from a static string.
807    #[inline]
808    pub const fn new_const(kind: &'static str) -> Self {
809        Self(Cow::Borrowed(kind))
810    }
811
812    /// Returns the kind as a string.
813    pub fn as_str(&self) -> &str {
814        &self.0
815    }
816
817    /// The "dylib" kind, used for dynamic libraries (`.so` on Linux). Also used for
818    /// .pdb and other similar files on Windows.
819    pub const DYLIB: Self = Self::new_const("dylib");
820
821    /// The "bin-exe" kind, used for binary executables.
822    pub const BIN_EXE: Self = Self::new_const("bin-exe");
823}
824
825impl fmt::Display for RustNonTestBinaryKind {
826    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
827        write!(f, "{}", self.0)
828    }
829}
830
831/// A serializable suite of tests within a Rust test binary.
832///
833/// Part of a [`TestListSummary`].
834#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
835#[serde(rename_all = "kebab-case")]
836pub struct RustTestSuiteSummary {
837    /// The name of this package in the workspace.
838    pub package_name: String,
839
840    /// The binary within the package.
841    #[serde(flatten)]
842    pub binary: RustTestBinarySummary,
843
844    /// The working directory that tests within this package are run in.
845    pub cwd: Utf8PathBuf,
846
847    /// Status of this test suite.
848    ///
849    /// Introduced in cargo-nextest 0.9.25. Older versions always imply
850    /// [`LISTED`](RustTestSuiteStatusSummary::LISTED).
851    #[serde(default = "listed_status")]
852    pub status: RustTestSuiteStatusSummary,
853
854    /// Test cases within this test suite.
855    #[serde(rename = "testcases")]
856    pub test_cases: BTreeMap<TestCaseName, RustTestCaseSummary>,
857}
858
859fn listed_status() -> RustTestSuiteStatusSummary {
860    RustTestSuiteStatusSummary::LISTED
861}
862
863/// Information about whether a test suite was listed or skipped.
864///
865/// This is part of [`RustTestSuiteSummary`].
866#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
867#[serde(transparent)]
868pub struct RustTestSuiteStatusSummary(pub Cow<'static, str>);
869
870impl RustTestSuiteStatusSummary {
871    /// Creates a new `RustNonTestBinaryKind` from a string.
872    #[inline]
873    pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
874        Self(kind.into())
875    }
876
877    /// Creates a new `RustNonTestBinaryKind` from a static string.
878    #[inline]
879    pub const fn new_const(kind: &'static str) -> Self {
880        Self(Cow::Borrowed(kind))
881    }
882
883    /// Returns the kind as a string.
884    pub fn as_str(&self) -> &str {
885        &self.0
886    }
887
888    /// The "listed" kind, which means that the test binary was executed with `--list` to gather the
889    /// list of tests in it.
890    pub const LISTED: Self = Self::new_const("listed");
891
892    /// The "skipped" kind, which indicates that the test binary was not executed because it didn't
893    /// match any filtersets.
894    ///
895    /// In this case, the contents of [`RustTestSuiteSummary::test_cases`] is empty.
896    pub const SKIPPED: Self = Self::new_const("skipped");
897
898    /// The binary doesn't match the profile's `default-filter`.
899    ///
900    /// This is the lowest-priority reason for skipping a binary.
901    pub const SKIPPED_DEFAULT_FILTER: Self = Self::new_const("skipped-default-filter");
902}
903
904/// Serializable information about an individual test case within a Rust test suite.
905///
906/// Part of a [`RustTestSuiteSummary`].
907#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
908#[serde(rename_all = "kebab-case")]
909pub struct RustTestCaseSummary {
910    /// The kind of Rust test this is.
911    ///
912    /// This field is present since cargo-nextest 0.9.117. In earlier versions
913    /// it is set to null.
914    pub kind: Option<RustTestKind>,
915
916    /// Returns true if this test is marked ignored.
917    ///
918    /// Ignored tests, if run, are executed with the `--ignored` argument.
919    pub ignored: bool,
920
921    /// Whether the test matches the provided test filter.
922    ///
923    /// Only tests that match the filter are run.
924    pub filter_match: FilterMatch,
925}
926
927/// The kind of Rust test something is.
928///
929/// Part of a [`RustTestCaseSummary`].
930#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
931#[serde(transparent)]
932pub struct RustTestKind(pub Cow<'static, str>);
933
934impl RustTestKind {
935    /// Creates a new `RustTestKind` from a string.
936    #[inline]
937    pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
938        Self(kind.into())
939    }
940
941    /// Creates a new `RustTestKind` from a static string.
942    #[inline]
943    pub const fn new_const(kind: &'static str) -> Self {
944        Self(Cow::Borrowed(kind))
945    }
946
947    /// Returns the kind as a string.
948    pub fn as_str(&self) -> &str {
949        &self.0
950    }
951
952    /// The "test" kind, used for functions annotated with `#[test]`.
953    pub const TEST: Self = Self::new_const("test");
954
955    /// The "bench" kind, used for functions annotated with `#[bench]`.
956    pub const BENCH: Self = Self::new_const("bench");
957}
958
959/// An enum describing whether a test matches a filter.
960#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
961#[serde(rename_all = "kebab-case", tag = "status")]
962pub enum FilterMatch {
963    /// This test matches this filter.
964    Matches,
965
966    /// This test does not match this filter.
967    Mismatch {
968        /// Describes the reason this filter isn't matched.
969        reason: MismatchReason,
970    },
971}
972
973impl FilterMatch {
974    /// Returns true if the filter doesn't match.
975    pub fn is_match(&self) -> bool {
976        matches!(self, FilterMatch::Matches)
977    }
978}
979
980/// The reason for why a test doesn't match a filter.
981#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
982#[serde(rename_all = "kebab-case")]
983#[non_exhaustive]
984pub enum MismatchReason {
985    /// Nextest is running in benchmark mode and this test is not a benchmark.
986    NotBenchmark,
987
988    /// This test does not match the run-ignored option in the filter.
989    Ignored,
990
991    /// This test does not match the provided string filters.
992    String,
993
994    /// This test does not match the provided expression filters.
995    Expression,
996
997    /// This test is in a different partition.
998    Partition,
999
1000    /// This is a rerun and the test already passed.
1001    RerunAlreadyPassed,
1002
1003    /// This test is filtered out by the default-filter.
1004    ///
1005    /// This is the lowest-priority reason for skipping a test.
1006    DefaultFilter,
1007}
1008
1009impl MismatchReason {
1010    /// All known variants of `MismatchReason`.
1011    ///
1012    /// This slice is provided for exhaustive testing. New variants may be added
1013    /// in future versions, so this slice's length is not guaranteed to be stable.
1014    pub const ALL_VARIANTS: &'static [Self] = &[
1015        Self::NotBenchmark,
1016        Self::Ignored,
1017        Self::String,
1018        Self::Expression,
1019        Self::Partition,
1020        Self::RerunAlreadyPassed,
1021        Self::DefaultFilter,
1022    ];
1023    /// Returns true if the test was skipped because its ignore status didn't
1024    /// match the user-provided `--run-ignored` setting.
1025    pub fn is_ignore_mismatch(self) -> bool {
1026        match self {
1027            MismatchReason::Ignored => true,
1028            MismatchReason::NotBenchmark
1029            | MismatchReason::String
1030            | MismatchReason::Expression
1031            | MismatchReason::Partition
1032            | MismatchReason::RerunAlreadyPassed
1033            | MismatchReason::DefaultFilter => false,
1034        }
1035    }
1036
1037    /// Returns true if the skip reflects a real filtering decision, rather than
1038    /// a run-mode artifact such as a non-benchmark test excluded from a
1039    /// benchmark run.
1040    pub fn is_substantive_skip(self) -> bool {
1041        match self {
1042            MismatchReason::NotBenchmark => false,
1043            MismatchReason::Ignored
1044            | MismatchReason::String
1045            | MismatchReason::Expression
1046            | MismatchReason::Partition
1047            | MismatchReason::RerunAlreadyPassed
1048            | MismatchReason::DefaultFilter => true,
1049        }
1050    }
1051}
1052
1053impl fmt::Display for MismatchReason {
1054    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1055        match self {
1056            MismatchReason::NotBenchmark => write!(f, "is not a benchmark"),
1057            MismatchReason::Ignored => write!(f, "does not match the run-ignored option"),
1058            MismatchReason::String => write!(f, "does not match the provided string filters"),
1059            MismatchReason::Expression => {
1060                write!(f, "does not match the provided expression filters")
1061            }
1062            MismatchReason::Partition => write!(f, "is in a different partition"),
1063            MismatchReason::RerunAlreadyPassed => write!(f, "already passed"),
1064            MismatchReason::DefaultFilter => {
1065                write!(f, "is filtered out by the profile's default-filter")
1066            }
1067        }
1068    }
1069}
1070
1071// --- Proptest support ---
1072
1073#[cfg(feature = "proptest1")]
1074mod proptest_impls {
1075    use super::*;
1076    use proptest::prelude::*;
1077
1078    impl Arbitrary for RustBinaryId {
1079        type Parameters = ();
1080        type Strategy = BoxedStrategy<Self>;
1081
1082        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1083            any::<String>().prop_map(|s| RustBinaryId::new(&s)).boxed()
1084        }
1085    }
1086
1087    impl Arbitrary for TestCaseName {
1088        type Parameters = ();
1089        type Strategy = BoxedStrategy<Self>;
1090
1091        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1092            any::<String>().prop_map(|s| TestCaseName::new(&s)).boxed()
1093        }
1094    }
1095
1096    impl Arbitrary for MismatchReason {
1097        type Parameters = ();
1098        type Strategy = BoxedStrategy<Self>;
1099
1100        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1101            proptest::sample::select(MismatchReason::ALL_VARIANTS).boxed()
1102        }
1103    }
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108    use super::*;
1109    use test_case::test_case;
1110
1111    #[test_case(r#"{
1112        "target-directory": "/foo",
1113        "base-output-directories": [],
1114        "non-test-binaries": {},
1115        "linked-paths": []
1116    }"#, RustBuildMetaSummary {
1117        target_directory: "/foo".into(),
1118        build_directory: None,
1119        base_output_directories: BTreeSet::new(),
1120        non_test_binaries: BTreeMap::new(),
1121        build_script_out_dirs: BTreeMap::new(),
1122        build_script_info: None,
1123        linked_paths: BTreeSet::new(),
1124        target_platform: None,
1125        target_platforms: vec![],
1126        platforms: None,
1127    }; "no target platform")]
1128    #[test_case(r#"{
1129        "target-directory": "/foo",
1130        "base-output-directories": [],
1131        "non-test-binaries": {},
1132        "linked-paths": [],
1133        "target-platform": "x86_64-unknown-linux-gnu"
1134    }"#, RustBuildMetaSummary {
1135        target_directory: "/foo".into(),
1136        build_directory: None,
1137        base_output_directories: BTreeSet::new(),
1138        non_test_binaries: BTreeMap::new(),
1139        build_script_out_dirs: BTreeMap::new(),
1140        build_script_info: None,
1141        linked_paths: BTreeSet::new(),
1142        target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
1143        target_platforms: vec![],
1144        platforms: None,
1145    }; "single target platform specified")]
1146    #[test_case(r#"{
1147        "target-directory": "/foo",
1148        "base-output-directories": [],
1149        "non-test-binaries": {
1150            "my-package-id": [
1151                {
1152                    "name": "my-name",
1153                    "kind": "bin-exe",
1154                    "path": "debug/my-name"
1155                }
1156            ]
1157        },
1158        "linked-paths": []
1159    }"#, RustBuildMetaSummary {
1160        target_directory: "/foo".into(),
1161        build_directory: None,
1162        base_output_directories: BTreeSet::new(),
1163        non_test_binaries: BTreeMap::from([("my-package-id".to_owned(), BTreeSet::from([
1164            RustNonTestBinarySummary {
1165                name: "my-name".to_owned(),
1166                kind: RustNonTestBinaryKind::BIN_EXE,
1167                path: "debug/my-name".into(),
1168                build_platform: None,
1169            },
1170        ]))]),
1171        build_script_out_dirs: BTreeMap::new(),
1172        build_script_info: None,
1173        linked_paths: BTreeSet::new(),
1174        target_platform: None,
1175        target_platforms: vec![],
1176        platforms: None,
1177    }; "non-test binary without a build platform")]
1178    fn test_deserialize_old_rust_build_meta(input: &str, expected: RustBuildMetaSummary) {
1179        let build_meta: RustBuildMetaSummary =
1180            serde_json::from_str(input).expect("input deserialized correctly");
1181        assert_eq!(
1182            build_meta, expected,
1183            "deserialized input matched expected output"
1184        );
1185    }
1186
1187    #[test]
1188    fn test_binary_id_ord() {
1189        let empty = RustBinaryId::new("");
1190        let foo = RustBinaryId::new("foo");
1191        let bar = RustBinaryId::new("bar");
1192        let foo_name1 = RustBinaryId::new("foo::name1");
1193        let foo_name2 = RustBinaryId::new("foo::name2");
1194        let bar_name = RustBinaryId::new("bar::name");
1195        let foo_bin_name1 = RustBinaryId::new("foo::bin/name1");
1196        let foo_bin_name2 = RustBinaryId::new("foo::bin/name2");
1197        let bar_bin_name = RustBinaryId::new("bar::bin/name");
1198        let foo_proc_macro_name = RustBinaryId::new("foo::proc_macro/name");
1199        let bar_proc_macro_name = RustBinaryId::new("bar::proc_macro/name");
1200
1201        // This defines the expected sort order.
1202        let sorted_ids = [
1203            empty,
1204            bar,
1205            bar_name,
1206            bar_bin_name,
1207            bar_proc_macro_name,
1208            foo,
1209            foo_name1,
1210            foo_name2,
1211            foo_bin_name1,
1212            foo_bin_name2,
1213            foo_proc_macro_name,
1214        ];
1215
1216        for (i, id) in sorted_ids.iter().enumerate() {
1217            for (j, other_id) in sorted_ids.iter().enumerate() {
1218                let expected = i.cmp(&j);
1219                assert_eq!(
1220                    id.cmp(other_id),
1221                    expected,
1222                    "comparing {id:?} to {other_id:?} gave {expected:?}"
1223                );
1224            }
1225        }
1226    }
1227
1228    /// Verify that `MismatchReason::ALL_VARIANTS` contains all variants.
1229    #[test]
1230    fn mismatch_reason_all_variants_is_complete() {
1231        // Exhaustive match.
1232        fn check_exhaustive(reason: MismatchReason) {
1233            match reason {
1234                MismatchReason::NotBenchmark
1235                | MismatchReason::Ignored
1236                | MismatchReason::String
1237                | MismatchReason::Expression
1238                | MismatchReason::Partition
1239                | MismatchReason::RerunAlreadyPassed
1240                | MismatchReason::DefaultFilter => {}
1241            }
1242        }
1243
1244        for &reason in MismatchReason::ALL_VARIANTS {
1245            check_exhaustive(reason);
1246        }
1247
1248        // If you add a variant, update ALL_VARIANTS and this count.
1249        assert_eq!(MismatchReason::ALL_VARIANTS.len(), 7);
1250    }
1251    #[test]
1252    fn mismatch_reason_predicates() {
1253        assert!(MismatchReason::Ignored.is_ignore_mismatch());
1254        for &reason in MismatchReason::ALL_VARIANTS {
1255            if reason != MismatchReason::Ignored {
1256                assert!(
1257                    !reason.is_ignore_mismatch(),
1258                    "{reason:?} is not an ignore mismatch"
1259                );
1260            }
1261        }
1262
1263        assert!(!MismatchReason::NotBenchmark.is_substantive_skip());
1264        for &reason in MismatchReason::ALL_VARIANTS {
1265            if reason != MismatchReason::NotBenchmark {
1266                assert!(
1267                    reason.is_substantive_skip(),
1268                    "{reason:?} is a substantive skip"
1269                );
1270            }
1271        }
1272    }
1273}