use std::fs;
use std::path::{Path, PathBuf};
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
fn fixture() -> (TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
fs::write(
root.join("Cargo.toml"),
"[workspace]\nresolver = \"2\"\nmembers = [\"alpha\", \"beta\", \"gamma\", \"delta\", \"epsilon\"]\n",
)
.expect("write workspace root");
write_lib(root, "alpha", "0.1.0", "");
write_bin(root, "beta", "\n[dev-dependencies]\nalpha = { path = \"../alpha\" }\n");
write_lib(root, "gamma", "0.1.0", "\n[package.metadata]\nrole = \"script-only\"\n");
write_lib(root, "delta", "1.2.3-beta.1+build", "");
write_lib_and_bin(root, "epsilon");
let manifest = root.join("Cargo.toml");
(tmp, manifest)
}
fn default_members_fixture() -> (TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
fs::write(
root.join("Cargo.toml"),
"[workspace]\nresolver = \"2\"\nmembers = [\"alpha\", \"beta\"]\ndefault-members = [\"alpha\"]\n",
)
.expect("write workspace root");
write_lib(root, "alpha", "0.1.0", "");
write_bin(root, "beta", "");
let manifest = root.join("Cargo.toml");
(tmp, manifest)
}
fn write_lib(root: &Path, name: &str, version: &str, extra: &str) {
let dir = root.join(name);
fs::create_dir_all(dir.join("src")).expect("mkdir src");
fs::write(
dir.join("Cargo.toml"),
format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\nedition = \"2021\"\n{extra}"),
)
.expect("write member Cargo.toml");
fs::write(dir.join("src/lib.rs"), "// fixture\n").expect("write lib.rs");
}
fn write_bin(root: &Path, name: &str, extra: &str) {
let dir = root.join(name);
fs::create_dir_all(dir.join("src")).expect("mkdir src");
fs::write(
dir.join("Cargo.toml"),
format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n{extra}"),
)
.expect("write member Cargo.toml");
fs::write(dir.join("src/main.rs"), "fn main() {}\n").expect("write main.rs");
}
fn write_lib_and_bin(root: &Path, name: &str) {
let dir = root.join(name);
fs::create_dir_all(dir.join("src")).expect("mkdir src");
fs::write(
dir.join("Cargo.toml"),
format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"),
)
.expect("write member Cargo.toml");
fs::write(dir.join("src/lib.rs"), "// fixture\n").expect("write lib.rs");
fs::write(dir.join("src/main.rs"), "fn main() {}\n").expect("write main.rs");
}
fn each(manifest: &Path) -> Command {
let mut cmd = Command::cargo_bin("cargo-each").expect("binary");
cmd.arg("each").arg("--manifest-path").arg(manifest);
cmd
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn once_whole_workspace_expands_to_workspace_flag() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--once", "--dry-run", "--", "cargo", "clippy", "{packages}"])
.assert()
.success()
.stdout(predicate::str::contains("cargo clippy --workspace"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn per_package_runs_once_per_selected_member() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha", "-p", "gamma", "--dry-run", "--", "echo", "{name}"])
.assert()
.success()
.stdout(predicate::str::contains("echo alpha").and(predicate::str::contains("echo gamma")));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn none_is_a_successful_noop() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--none", "--once", "--dry-run", "--", "cargo", "test", "{packages}"])
.assert()
.success()
.stderr(predicate::str::contains("nothing to do"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn none_with_misused_placeholder_is_a_usage_error() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--none", "--once", "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("{name}"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn version_spec_partial_matches_over_metadata() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha@0.1", "--dry-run", "--", "echo", "{spec}"])
.assert()
.success()
.stdout(predicate::str::contains("echo alpha@0.1.0"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn version_spec_mismatch_is_a_usage_error_over_metadata() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha@9.9.9", "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("did not match"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn prerelease_version_spec_matches_exactly_over_metadata() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "delta@1.2.3-beta.1", "--dry-run", "--", "echo", "{spec}"])
.assert()
.success()
.stdout(predicate::str::contains("echo delta@1.2.3-beta.1+build"));
each(&manifest)
.args(["-p", "delta@1.2.3", "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("did not match"));
each(&manifest)
.args(["-p", "delta@1.2.3-beta", "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("did not match"));
each(&manifest)
.args(["-p", "delta@1.2.3-beta.1+wrong", "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("did not match"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn malformed_version_spec_is_rejected_over_metadata() {
let (_tmp, manifest) = fixture();
for spec in ["alpha@0.1.0-", "alpha@0.1.0+", "alpha@01.0.0"] {
each(&manifest)
.args(["-p", spec, "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("did not match"));
}
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn multiple_filters_intersect() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--filter",
"lib",
"--filter",
"bin",
"--dry-run",
"--",
"echo",
"{name}",
])
.assert()
.success()
.stdout(
predicate::str::contains("echo epsilon")
.and(predicate::str::contains("echo alpha").not())
.and(predicate::str::contains("echo beta").not())
.and(predicate::str::contains("echo gamma").not()),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn multiple_exclude_filters_union() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--exclude-filter",
"bin",
"--exclude-filter",
"metadata:role=script-only",
"--dry-run",
"--",
"echo",
"{name}",
])
.assert()
.success()
.stdout(
predicate::str::contains("echo alpha")
.and(predicate::str::contains("echo delta"))
.and(predicate::str::contains("echo beta").not())
.and(predicate::str::contains("echo gamma").not())
.and(predicate::str::contains("echo epsilon").not()),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn filter_lib_drops_bin_only_member() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--filter", "lib", "--dry-run", "--", "tool", "{name}"])
.assert()
.success()
.stdout(
predicate::str::contains("tool alpha")
.and(predicate::str::contains("tool gamma"))
.and(predicate::str::contains("tool beta").not()),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn exclude_filter_metadata_drops_matching_member() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--exclude-filter",
"metadata:role=script-only",
"--dry-run",
"--",
"tool",
"{name}",
])
.assert()
.success()
.stdout(
predicate::str::contains("tool gamma")
.not()
.and(predicate::str::contains("tool alpha")),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn exclude_requires_workspace() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha", "--exclude", "beta", "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("--workspace"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn exclude_with_workspace_removes_member() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--exclude", "beta", "--dry-run", "--", "echo", "{name}"])
.assert()
.success()
.stdout(predicate::str::contains("echo alpha").and(predicate::str::contains("echo beta").not()));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn chdir_shows_crate_root_in_dry_run() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha", "--chdir", "--dry-run", "--", "echo", "{name}"])
.assert()
.success()
.stdout(predicate::str::contains("(cd ").and(predicate::str::contains("alpha")));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn chdir_with_once_is_a_usage_error() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--chdir", "--once", "--dry-run", "--", "echo", "{packages}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("--chdir").and(predicate::str::contains("--once")));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn unknown_selector_is_a_usage_error() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "does-not-exist", "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("did not match"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn bad_predicate_is_a_usage_error() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--filter", "nonsense", "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("invalid filter predicate"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn executes_command_and_propagates_success() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--once", "--", "cargo", "--version"])
.assert()
.success();
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn executes_command_and_propagates_failure() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha", "--", "cargo", "this-subcommand-does-not-exist"])
.assert()
.failure();
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn chdir_executes_from_crate_root() {
let (_tmp, manifest) = fixture();
let member_suffix = format!("alpha{}Cargo.toml", std::path::MAIN_SEPARATOR);
each(&manifest)
.args([
"-p",
"alpha",
"--chdir",
"--",
"cargo",
"locate-project",
"--message-format",
"plain",
])
.assert()
.success()
.stdout(predicate::str::contains(member_suffix));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn keep_going_runs_all_members_then_fails() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"-p",
"alpha",
"-p",
"gamma",
"--keep-going",
"--",
"cargo",
"this-subcommand-does-not-exist",
])
.assert()
.failure()
.code(1);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn keep_going_treats_spawn_failure_as_invocation_failure() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"-p",
"alpha",
"-p",
"gamma",
"--keep-going",
"--",
"cargo-each-no-such-program-xyz",
"{name}",
])
.assert()
.failure()
.code(1)
.stderr(
predicate::str::contains("cargo each: alpha")
.and(predicate::str::contains("cargo each: gamma"))
.and(predicate::str::contains("failed to spawn")),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn spawn_failure_without_keep_going_is_a_usage_error() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha", "--", "cargo-each-no-such-program-xyz"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("failed to spawn"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn dry_run_quotes_arguments_with_whitespace() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--once", "--dry-run", "--", "echo", "a b"])
.assert()
.success()
.stdout(predicate::str::contains("\"a b\""));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn once_with_filter_uses_explicit_packages_not_workspace() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--filter",
"lib",
"--once",
"--dry-run",
"--",
"cargo",
"x",
"{packages}",
])
.assert()
.success()
.stdout(predicate::str::contains("--package alpha").and(predicate::str::contains("--workspace").not()));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn once_with_exclude_filter_uses_explicit_packages_not_workspace() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--exclude-filter",
"metadata:role=script-only",
"--once",
"--dry-run",
"--",
"cargo",
"x",
"{packages}",
])
.assert()
.success()
.stdout(predicate::str::contains("--package").and(predicate::str::contains("--workspace").not()));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn fail_fast_stops_before_running_later_members() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha", "-p", "gamma", "--", "cargo", "this-subcommand-does-not-exist"])
.assert()
.failure()
.stderr(predicate::str::contains("cargo each: alpha").and(predicate::str::contains("cargo each: gamma").not()));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn dep_filter_selects_member_with_declared_dependency() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--filter", "dep:alpha", "--dry-run", "--", "echo", "{name}"])
.assert()
.success()
.stdout(
predicate::str::contains("echo beta")
.and(predicate::str::contains("echo alpha").not())
.and(predicate::str::contains("echo gamma").not()),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn bare_invocation_runs_only_default_members() {
let (_tmp, manifest) = default_members_fixture();
each(&manifest)
.args(["--dry-run", "--", "echo", "{name}"])
.assert()
.success()
.stdout(predicate::str::contains("echo alpha").and(predicate::str::contains("echo beta").not()));
}