use crate::CommandError;
use camino::{Utf8Path, Utf8PathBuf};
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::{
borrow::Cow,
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
fmt::{self, Write as _},
path::PathBuf,
process::Command,
};
use target_spec::summaries::PlatformSummary;
pub const GLOBAL_TEST_GROUP: &str = "@global";
#[derive(Clone, Debug, Default)]
pub struct ListCommand {
cargo_path: Option<Box<Utf8Path>>,
manifest_path: Option<Box<Utf8Path>>,
current_dir: Option<Box<Utf8Path>>,
args: Vec<Box<str>>,
}
impl ListCommand {
pub fn new() -> Self {
Self::default()
}
pub fn cargo_path(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
self.cargo_path = Some(path.into().into());
self
}
pub fn manifest_path(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
self.manifest_path = Some(path.into().into());
self
}
pub fn current_dir(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
self.current_dir = Some(path.into().into());
self
}
pub fn add_arg(&mut self, arg: impl Into<String>) -> &mut Self {
self.args.push(arg.into().into());
self
}
pub fn add_args(&mut self, args: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
for arg in args {
self.add_arg(arg.into());
}
self
}
pub fn cargo_command(&self) -> Command {
let cargo_path: PathBuf = self.cargo_path.as_ref().map_or_else(
|| std::env::var_os("CARGO").map_or("cargo".into(), PathBuf::from),
|path| PathBuf::from(path.as_std_path()),
);
let mut command = Command::new(cargo_path);
if let Some(path) = &self.manifest_path.as_deref() {
command.args(["--manifest-path", path.as_str()]);
}
if let Some(current_dir) = &self.current_dir.as_deref() {
command.current_dir(current_dir);
}
command.args(["nextest", "list", "--message-format=json"]);
command.args(self.args.iter().map(|s| s.as_ref()));
command
}
pub fn exec(&self) -> Result<TestListSummary, CommandError> {
let mut command = self.cargo_command();
let output = command.output().map_err(CommandError::Exec)?;
if !output.status.success() {
let exit_code = output.status.code();
let stderr = output.stderr;
return Err(CommandError::CommandFailed { exit_code, stderr });
}
serde_json::from_slice(&output.stdout).map_err(CommandError::Json)
}
pub fn exec_binaries_only(&self) -> Result<BinaryListSummary, CommandError> {
let mut command = self.cargo_command();
command.arg("--list-type=binaries-only");
let output = command.output().map_err(CommandError::Exec)?;
if !output.status.success() {
let exit_code = output.status.code();
let stderr = output.stderr;
return Err(CommandError::CommandFailed { exit_code, stderr });
}
serde_json::from_slice(&output.stdout).map_err(CommandError::Json)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct TestListSummary {
pub rust_build_meta: RustBuildMetaSummary,
pub test_count: usize,
pub rust_suites: BTreeMap<RustBinaryId, RustTestSuiteSummary>,
}
impl TestListSummary {
pub fn new(rust_build_meta: RustBuildMetaSummary) -> Self {
Self {
rust_build_meta,
test_count: 0,
rust_suites: BTreeMap::new(),
}
}
pub fn parse_json(json: impl AsRef<str>) -> Result<Self, serde_json::Error> {
serde_json::from_str(json.as_ref())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum BuildPlatform {
Target,
Host,
}
impl fmt::Display for BuildPlatform {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Target => write!(f, "target"),
Self::Host => write!(f, "host"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RustTestBinarySummary {
pub binary_id: RustBinaryId,
pub binary_name: String,
pub package_id: String,
pub kind: RustTestBinaryKind,
pub binary_path: Utf8PathBuf,
pub build_platform: BuildPlatform,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(transparent)]
pub struct RustTestBinaryKind(pub Cow<'static, str>);
impl RustTestBinaryKind {
#[inline]
pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
Self(kind.into())
}
#[inline]
pub const fn new_const(kind: &'static str) -> Self {
Self(Cow::Borrowed(kind))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub const LIB: Self = Self::new_const("lib");
pub const TEST: Self = Self::new_const("test");
pub const BENCH: Self = Self::new_const("bench");
pub const BIN: Self = Self::new_const("bin");
pub const EXAMPLE: Self = Self::new_const("example");
pub const PROC_MACRO: Self = Self::new_const("proc-macro");
}
impl fmt::Display for RustTestBinaryKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct BinaryListSummary {
pub rust_build_meta: RustBuildMetaSummary,
pub rust_binaries: BTreeMap<RustBinaryId, RustTestBinarySummary>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct RustBinaryId(SmolStr);
impl fmt::Display for RustBinaryId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl RustBinaryId {
#[inline]
pub fn new(id: &str) -> Self {
Self(id.into())
}
pub fn from_parts(package_name: &str, kind: &RustTestBinaryKind, target_name: &str) -> Self {
let mut id = package_name.to_owned();
if kind == &RustTestBinaryKind::LIB || kind == &RustTestBinaryKind::PROC_MACRO {
} else if kind == &RustTestBinaryKind::TEST {
id.push_str("::");
id.push_str(target_name);
} else {
write!(id, "::{kind}/{target_name}").unwrap();
}
Self(id.into())
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
pub fn len(&self) -> usize {
self.0.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
pub fn components(&self) -> RustBinaryIdComponents<'_> {
RustBinaryIdComponents::new(self)
}
}
impl<S> From<S> for RustBinaryId
where
S: AsRef<str>,
{
#[inline]
fn from(s: S) -> Self {
Self(s.as_ref().into())
}
}
impl Ord for RustBinaryId {
fn cmp(&self, other: &RustBinaryId) -> Ordering {
self.components().cmp(&other.components())
}
}
impl PartialOrd for RustBinaryId {
fn partial_cmp(&self, other: &RustBinaryId) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RustBinaryIdComponents<'a> {
pub package_name: &'a str,
pub binary_name_and_kind: RustBinaryIdNameAndKind<'a>,
}
impl<'a> RustBinaryIdComponents<'a> {
fn new(id: &'a RustBinaryId) -> Self {
let mut parts = id.as_str().splitn(2, "::");
let package_name = parts
.next()
.expect("splitn(2) returns at least 1 component");
let binary_name_and_kind = if let Some(suffix) = parts.next() {
let mut parts = suffix.splitn(2, '/');
let part1 = parts
.next()
.expect("splitn(2) returns at least 1 component");
if let Some(binary_name) = parts.next() {
RustBinaryIdNameAndKind::NameAndKind {
kind: part1,
binary_name,
}
} else {
RustBinaryIdNameAndKind::NameOnly { binary_name: part1 }
}
} else {
RustBinaryIdNameAndKind::None
};
Self {
package_name,
binary_name_and_kind,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum RustBinaryIdNameAndKind<'a> {
None,
NameOnly {
binary_name: &'a str,
},
NameAndKind {
kind: &'a str,
binary_name: &'a str,
},
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct TestCaseName(SmolStr);
impl fmt::Display for TestCaseName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TestCaseName {
#[inline]
pub fn new(name: &str) -> Self {
Self(name.into())
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
#[inline]
pub fn len(&self) -> usize {
self.0.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
pub fn contains(&self, pattern: &str) -> bool {
self.0.contains(pattern)
}
#[inline]
pub fn components(&self) -> std::str::Split<'_, &str> {
self.0.split("::")
}
#[inline]
pub fn module_path_and_name(&self) -> (Option<&str>, &str) {
match self.0.rsplit_once("::") {
Some((module_path, name)) => (Some(module_path), name),
None => (None, &self.0),
}
}
}
impl AsRef<str> for TestCaseName {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct RustBuildMetaSummary {
pub target_directory: Utf8PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_directory: Option<Utf8PathBuf>,
pub base_output_directories: BTreeSet<Utf8PathBuf>,
pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
#[serde(default)]
pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
#[serde(default)]
pub build_script_info: Option<BTreeMap<String, BuildScriptInfoSummary>>,
pub linked_paths: BTreeSet<Utf8PathBuf>,
#[serde(default)]
pub platforms: Option<BuildPlatformsSummary>,
#[serde(default)]
pub target_platforms: Vec<PlatformSummary>,
#[serde(default)]
pub target_platform: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct BuildScriptInfoSummary {
#[serde(default)]
pub envs: BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RustNonTestBinarySummary {
pub name: String,
pub kind: RustNonTestBinaryKind,
pub path: Utf8PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct BuildPlatformsSummary {
pub host: HostPlatformSummary,
pub targets: Vec<TargetPlatformSummary>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct HostPlatformSummary {
pub platform: PlatformSummary,
pub libdir: PlatformLibdirSummary,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct TargetPlatformSummary {
pub platform: PlatformSummary,
pub libdir: PlatformLibdirSummary,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "status", rename_all = "kebab-case")]
pub enum PlatformLibdirSummary {
Available {
path: Utf8PathBuf,
},
Unavailable {
reason: PlatformLibdirUnavailable,
},
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct PlatformLibdirUnavailable(pub Cow<'static, str>);
impl PlatformLibdirUnavailable {
pub const RUSTC_FAILED: Self = Self::new_const("rustc-failed");
pub const RUSTC_OUTPUT_ERROR: Self = Self::new_const("rustc-output-error");
pub const OLD_SUMMARY: Self = Self::new_const("old-summary");
pub const NOT_IN_ARCHIVE: Self = Self::new_const("not-in-archive");
pub const fn new_const(reason: &'static str) -> Self {
Self(Cow::Borrowed(reason))
}
pub fn new(reason: impl Into<Cow<'static, str>>) -> Self {
Self(reason.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(transparent)]
pub struct RustNonTestBinaryKind(pub Cow<'static, str>);
impl RustNonTestBinaryKind {
#[inline]
pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
Self(kind.into())
}
#[inline]
pub const fn new_const(kind: &'static str) -> Self {
Self(Cow::Borrowed(kind))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub const DYLIB: Self = Self::new_const("dylib");
pub const BIN_EXE: Self = Self::new_const("bin-exe");
}
impl fmt::Display for RustNonTestBinaryKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RustTestSuiteSummary {
pub package_name: String,
#[serde(flatten)]
pub binary: RustTestBinarySummary,
pub cwd: Utf8PathBuf,
#[serde(default = "listed_status")]
pub status: RustTestSuiteStatusSummary,
#[serde(rename = "testcases")]
pub test_cases: BTreeMap<TestCaseName, RustTestCaseSummary>,
}
fn listed_status() -> RustTestSuiteStatusSummary {
RustTestSuiteStatusSummary::LISTED
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(transparent)]
pub struct RustTestSuiteStatusSummary(pub Cow<'static, str>);
impl RustTestSuiteStatusSummary {
#[inline]
pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
Self(kind.into())
}
#[inline]
pub const fn new_const(kind: &'static str) -> Self {
Self(Cow::Borrowed(kind))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub const LISTED: Self = Self::new_const("listed");
pub const SKIPPED: Self = Self::new_const("skipped");
pub const SKIPPED_DEFAULT_FILTER: Self = Self::new_const("skipped-default-filter");
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RustTestCaseSummary {
pub kind: Option<RustTestKind>,
pub ignored: bool,
pub filter_match: FilterMatch,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(transparent)]
pub struct RustTestKind(pub Cow<'static, str>);
impl RustTestKind {
#[inline]
pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
Self(kind.into())
}
#[inline]
pub const fn new_const(kind: &'static str) -> Self {
Self(Cow::Borrowed(kind))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub const TEST: Self = Self::new_const("test");
pub const BENCH: Self = Self::new_const("bench");
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", tag = "status")]
pub enum FilterMatch {
Matches,
Mismatch {
reason: MismatchReason,
},
}
impl FilterMatch {
pub fn is_match(&self) -> bool {
matches!(self, FilterMatch::Matches)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum MismatchReason {
NotBenchmark,
Ignored,
String,
Expression,
Partition,
RerunAlreadyPassed,
DefaultFilter,
}
impl MismatchReason {
pub const ALL_VARIANTS: &'static [Self] = &[
Self::NotBenchmark,
Self::Ignored,
Self::String,
Self::Expression,
Self::Partition,
Self::RerunAlreadyPassed,
Self::DefaultFilter,
];
}
impl fmt::Display for MismatchReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MismatchReason::NotBenchmark => write!(f, "is not a benchmark"),
MismatchReason::Ignored => write!(f, "does not match the run-ignored option"),
MismatchReason::String => write!(f, "does not match the provided string filters"),
MismatchReason::Expression => {
write!(f, "does not match the provided expression filters")
}
MismatchReason::Partition => write!(f, "is in a different partition"),
MismatchReason::RerunAlreadyPassed => write!(f, "already passed"),
MismatchReason::DefaultFilter => {
write!(f, "is filtered out by the profile's default-filter")
}
}
}
}
#[cfg(feature = "proptest1")]
mod proptest_impls {
use super::*;
use proptest::prelude::*;
impl Arbitrary for RustBinaryId {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
any::<String>().prop_map(|s| RustBinaryId::new(&s)).boxed()
}
}
impl Arbitrary for TestCaseName {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
any::<String>().prop_map(|s| TestCaseName::new(&s)).boxed()
}
}
impl Arbitrary for MismatchReason {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
proptest::sample::select(MismatchReason::ALL_VARIANTS).boxed()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[test_case(r#"{
"target-directory": "/foo",
"base-output-directories": [],
"non-test-binaries": {},
"linked-paths": []
}"#, RustBuildMetaSummary {
target_directory: "/foo".into(),
build_directory: None,
base_output_directories: BTreeSet::new(),
non_test_binaries: BTreeMap::new(),
build_script_out_dirs: BTreeMap::new(),
build_script_info: None,
linked_paths: BTreeSet::new(),
target_platform: None,
target_platforms: vec![],
platforms: None,
}; "no target platform")]
#[test_case(r#"{
"target-directory": "/foo",
"base-output-directories": [],
"non-test-binaries": {},
"linked-paths": [],
"target-platform": "x86_64-unknown-linux-gnu"
}"#, RustBuildMetaSummary {
target_directory: "/foo".into(),
build_directory: None,
base_output_directories: BTreeSet::new(),
non_test_binaries: BTreeMap::new(),
build_script_out_dirs: BTreeMap::new(),
build_script_info: None,
linked_paths: BTreeSet::new(),
target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
target_platforms: vec![],
platforms: None,
}; "single target platform specified")]
fn test_deserialize_old_rust_build_meta(input: &str, expected: RustBuildMetaSummary) {
let build_meta: RustBuildMetaSummary =
serde_json::from_str(input).expect("input deserialized correctly");
assert_eq!(
build_meta, expected,
"deserialized input matched expected output"
);
}
#[test]
fn test_binary_id_ord() {
let empty = RustBinaryId::new("");
let foo = RustBinaryId::new("foo");
let bar = RustBinaryId::new("bar");
let foo_name1 = RustBinaryId::new("foo::name1");
let foo_name2 = RustBinaryId::new("foo::name2");
let bar_name = RustBinaryId::new("bar::name");
let foo_bin_name1 = RustBinaryId::new("foo::bin/name1");
let foo_bin_name2 = RustBinaryId::new("foo::bin/name2");
let bar_bin_name = RustBinaryId::new("bar::bin/name");
let foo_proc_macro_name = RustBinaryId::new("foo::proc_macro/name");
let bar_proc_macro_name = RustBinaryId::new("bar::proc_macro/name");
let sorted_ids = [
empty,
bar,
bar_name,
bar_bin_name,
bar_proc_macro_name,
foo,
foo_name1,
foo_name2,
foo_bin_name1,
foo_bin_name2,
foo_proc_macro_name,
];
for (i, id) in sorted_ids.iter().enumerate() {
for (j, other_id) in sorted_ids.iter().enumerate() {
let expected = i.cmp(&j);
assert_eq!(
id.cmp(other_id),
expected,
"comparing {id:?} to {other_id:?} gave {expected:?}"
);
}
}
}
#[test]
fn mismatch_reason_all_variants_is_complete() {
fn check_exhaustive(reason: MismatchReason) {
match reason {
MismatchReason::NotBenchmark
| MismatchReason::Ignored
| MismatchReason::String
| MismatchReason::Expression
| MismatchReason::Partition
| MismatchReason::RerunAlreadyPassed
| MismatchReason::DefaultFilter => {}
}
}
for &reason in MismatchReason::ALL_VARIANTS {
check_exhaustive(reason);
}
assert_eq!(MismatchReason::ALL_VARIANTS.len(), 7);
}
}