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