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",
"\n[features]\nloom = []\n\n[[test]]\nname = \"loom-test\"\npath = \"tests/loom.rs\"\nrequired-features = [\"loom\"]\n",
);
fs::create_dir_all(root.join("alpha/tests")).expect("mkdir alpha tests");
fs::write(root.join("alpha/tests/loom.rs"), "#[test]\nfn loom_test() {}\n").expect("write loom test");
write_bin(root, "beta", "\n[dev-dependencies]\nalpha = { path = \"../alpha\" }\n");
fs::create_dir_all(root.join("beta/examples")).expect("mkdir beta examples");
fs::write(root.join("beta/examples/demo.rs"), "fn main() {}\n").expect("write beta example");
write_lib(
root,
"gamma",
"0.1.0",
"publish = false\n\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(windows)]
fn compile_probe(source: &Path, executable: &Path, marker: &str) {
fs::write(source, format!("fn main() {{ println!(\"{marker}\"); }}\n")).expect("write probe source");
let output = std::process::Command::new("rustc")
.arg(source)
.arg("-o")
.arg(executable)
.output()
.expect("rustc must be available to compile the Windows resolution probe");
assert!(
output.status.success(),
"failed to compile Windows resolution probe:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[cfg(windows)]
#[cfg_attr(miri, ignore = "spawns cargo-each and child processes; miri supports neither")]
#[test]
fn relative_child_program_uses_path_before_cargo_each_directory() {
let (_workspace, manifest) = fixture();
let layout = tempfile::tempdir().expect("resolution layout");
let executable_dir = layout.path().join("executable");
let path_dir = layout.path().join("path");
fs::create_dir_all(&executable_dir).expect("create executable directory");
fs::create_dir_all(&path_dir).expect("create PATH directory");
let cargo_each = executable_dir.join("cargo-each.exe");
fs::copy(assert_cmd::cargo::cargo_bin!("cargo-each"), &cargo_each).expect("copy cargo-each");
let probe_name = "cargo-each-path-resolution-probe.exe";
compile_probe(
&layout.path().join("adjacent.rs"),
&executable_dir.join(probe_name),
"adjacent executable",
);
compile_probe(&layout.path().join("path.rs"), &path_dir.join(probe_name), "PATH executable");
let inherited = std::env::var_os("PATH").unwrap_or_default();
let path =
std::env::join_paths(std::iter::once(path_dir).chain(std::env::split_paths(&inherited))).expect("fixture PATH must be valid");
Command::new(cargo_each)
.arg("each")
.arg("--manifest-path")
.arg(manifest)
.args(["--package", "alpha", "--once", "--", probe_name])
.env("PATH", path)
.assert()
.success()
.stdout(predicate::str::contains("PATH executable"))
.stdout(predicate::str::contains("adjacent executable").not());
}
#[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 boolean_filter_supports_grouped_or() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--filter",
"publishable and (feature:loom or target-kind:bin)",
"--dry-run",
"--",
"echo",
"{name}",
])
.assert()
.success()
.stdout(
predicate::str::contains("echo alpha")
.and(predicate::str::contains("echo beta"))
.and(predicate::str::contains("echo epsilon"))
.and(predicate::str::contains("echo gamma").not())
.and(predicate::str::contains("echo delta").not()),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn richer_package_predicates_use_cargo_metadata() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--filter",
"target-kind:example",
"--filter",
"publishable",
"--dry-run",
"--",
"echo",
"{name}",
])
.assert()
.success()
.stdout(predicate::str::contains("echo beta").and(predicate::str::contains("echo gamma").not()));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn publishable_filter_excludes_publish_false() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"-p",
"alpha",
"-p",
"gamma",
"--filter",
"publishable",
"--dry-run",
"--",
"echo",
"{name}",
])
.assert()
.success()
.stdout(predicate::str::contains("echo alpha").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 per_target_mode_filters_required_features_and_substitutes_target() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--each-target",
"test",
"--target-required-feature",
"loom",
"--dry-run",
"--",
"echo",
"{name}::{target}",
])
.assert()
.success()
.stdout(
predicate::str::contains("echo alpha::loom-test")
.and(predicate::str::contains("beta").not())
.and(predicate::str::contains("epsilon").not()),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn per_target_rejects_unknown_kind() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--each-target",
"unknown-kind",
"--dry-run",
"--",
"echo",
"{target}",
])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("invalid target kind"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn per_target_and_once_are_mutually_exclusive() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--each-target",
"test",
"--once",
"--dry-run",
"--",
"echo",
"{target}",
])
.assert()
.failure()
.code(2);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn target_required_feature_requires_target_mode() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--target-required-feature",
"loom",
"--dry-run",
"--",
"echo",
"{name}",
])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("--each-target"));
}
#[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_filter_expression_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 expression"));
}
#[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_boolean_filter_uses_explicit_packages_not_workspace() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--filter",
"feature:loom",
"--once",
"--dry-run",
"--",
"cargo",
"x",
"{packages}",
])
.assert()
.success()
.stdout(
predicate::str::contains("--package alpha")
.and(predicate::str::contains("--workspace").not())
.and(predicate::str::contains("--package beta").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()));
}