use std::path::PathBuf;
use crate::error::{ChdirRequiresPerPackageError, EachError};
use crate::substitute::{Placeholders, substitute, validate_placeholders};
use crate::workspace::Member;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
PerPackage,
Once,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PackagesExpansion {
Workspace,
Explicit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Invocation {
pub(crate) label: Option<String>,
pub(crate) argv: Vec<String>,
pub(crate) work_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Plan {
pub(crate) invocations: Vec<Invocation>,
}
impl Plan {
pub(crate) fn build(
members: &[&Member],
mode: Mode,
chdir: bool,
packages: PackagesExpansion,
command: &[String],
) -> Result<Self, EachError> {
if chdir && mode == Mode::Once {
return Err(ChdirRequiresPerPackageError::new().into());
}
validate_placeholders(command, mode)?;
if members.is_empty() {
return Ok(Self { invocations: Vec::new() });
}
let invocations = match mode {
Mode::PerPackage => members
.iter()
.map(|m| {
let placeholders = Placeholders::Package {
name: m.name.clone(),
spec: m.spec(),
version: m.version.clone(),
manifest: m.manifest_path.display().to_string(),
};
Ok(Invocation {
label: Some(m.name.clone()),
argv: substitute(command, &placeholders)?,
work_dir: chdir.then(|| m.manifest_dir().to_path_buf()),
})
})
.collect::<Result<Vec<_>, EachError>>()?,
Mode::Once => {
let packages = packages_flags(members, packages);
let placeholders = Placeholders::Once { packages };
vec![Invocation {
label: None,
argv: substitute(command, &placeholders)?,
work_dir: None,
}]
}
};
Ok(Self { invocations })
}
}
fn packages_flags(members: &[&Member], packages: PackagesExpansion) -> Vec<String> {
if packages == PackagesExpansion::Workspace {
return vec!["--workspace".to_owned()];
}
let mut flags = Vec::with_capacity(members.len() * 2);
for m in members {
flags.push("--package".to_owned());
flags.push(m.spec());
}
flags
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::collections::BTreeSet;
use serde_json::Value;
use super::*;
fn member(name: &str) -> Member {
Member {
name: name.to_owned(),
version: "1.2.3".to_owned(),
manifest_path: PathBuf::from(format!("/ws/{name}/Cargo.toml")),
has_lib: true,
has_bin: false,
dependencies: BTreeSet::new(),
metadata: Value::Null,
}
}
fn cmd(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| (*s).to_owned()).collect()
}
#[test]
fn empty_set_yields_empty_plan() {
let plan = Plan::build(&[], Mode::PerPackage, false, PackagesExpansion::Explicit, &cmd(&["cargo", "test"])).expect("build");
assert!(plan.invocations.is_empty());
}
#[test]
fn empty_set_still_validates_placeholders() {
let err = Plan::build(
&[],
Mode::Once,
false,
PackagesExpansion::Explicit,
&cmd(&["cargo", "test", "{name}"]),
)
.expect_err("misused placeholder must error");
assert!(err.to_string().contains("{name}"));
let plan = Plan::build(
&[],
Mode::Once,
false,
PackagesExpansion::Explicit,
&cmd(&["cargo", "test", "{packages}"]),
)
.expect("build");
assert!(plan.invocations.is_empty());
}
#[test]
fn per_package_builds_one_invocation_each() {
let a = member("alpha");
let b = member("beta");
let plan = Plan::build(
&[&a, &b],
Mode::PerPackage,
false,
PackagesExpansion::Explicit,
&cmd(&["cargo", "check-external-types", "--manifest-path", "{manifest}"]),
)
.expect("build");
assert_eq!(plan.invocations.len(), 2);
assert_eq!(plan.invocations[0].label.as_deref(), Some("alpha"));
assert_eq!(
plan.invocations[0].argv,
["cargo", "check-external-types", "--manifest-path", "/ws/alpha/Cargo.toml"]
);
assert!(plan.invocations[0].work_dir.is_none());
assert_eq!(plan.invocations[1].argv[3], "/ws/beta/Cargo.toml");
}
#[test]
fn chdir_sets_work_dir_to_crate_root() {
let a = member("alpha");
let plan = Plan::build(&[&a], Mode::PerPackage, true, PackagesExpansion::Explicit, &cmd(&["cargo", "fmt"])).expect("build");
assert_eq!(plan.invocations[0].work_dir.as_deref(), Some(PathBuf::from("/ws/alpha").as_path()));
}
#[test]
fn chdir_with_once_is_a_usage_error() {
let a = member("alpha");
let err = Plan::build(
&[&a],
Mode::Once,
true,
PackagesExpansion::Explicit,
&cmd(&["cargo", "test", "{packages}"]),
)
.expect_err("chdir+once must error");
let rendered = err.to_string();
assert!(rendered.contains("--chdir"), "rendered: {rendered}");
assert!(rendered.contains("--once"), "rendered: {rendered}");
}
#[test]
fn once_whole_workspace_uses_workspace_flag() {
let a = member("alpha");
let plan = Plan::build(
&[&a],
Mode::Once,
false,
PackagesExpansion::Workspace,
&cmd(&["cargo", "clippy", "{packages}"]),
)
.expect("build");
assert_eq!(plan.invocations.len(), 1);
assert_eq!(plan.invocations[0].argv, ["cargo", "clippy", "--workspace"]);
}
#[test]
fn once_subset_uses_explicit_package_flags() {
let a = member("alpha");
let b = member("beta");
let plan = Plan::build(
&[&a, &b],
Mode::Once,
false,
PackagesExpansion::Explicit,
&cmd(&["cargo", "clippy", "{packages}"]),
)
.expect("build");
assert_eq!(
plan.invocations[0].argv,
["cargo", "clippy", "--package", "alpha@1.2.3", "--package", "beta@1.2.3"]
);
}
}