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