use crate::CommandError;
use camino::{Utf8Path, Utf8PathBuf};
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
collections::{BTreeMap, BTreeSet},
fmt,
path::PathBuf,
process::Command,
};
#[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", "--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<String, 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: String,
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 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<String, RustTestBinarySummary>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RustBuildMetaSummary {
pub target_directory: Utf8PathBuf,
pub base_output_directories: BTreeSet<Utf8PathBuf>,
pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
pub linked_paths: BTreeSet<Utf8PathBuf>,
}
#[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, 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<String, 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");
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RustTestCaseSummary {
pub ignored: bool,
pub filter_match: FilterMatch,
}
#[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 {
Ignored,
String,
Expression,
Partition,
}
impl fmt::Display for MismatchReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
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"),
}
}
}