#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#![cfg_attr(coverage_nightly, coverage(off))]
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
}
fn json_each() -> Command {
let mut cmd = Command::cargo_bin("cargo-each").expect("binary");
cmd.arg("each");
cmd
}
fn rust_version_fixture(root_floor: Option<&str>, members: &[(&str, Option<&str>)]) -> (TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let member_names = members.iter().map(|(name, _)| format!("\"{name}\"")).collect::<Vec<_>>().join(", ");
let workspace_package = root_floor.map_or_else(String::new, |floor| format!("\n[workspace.package]\nrust-version = \"{floor}\"\n"));
fs::write(
root.join("Cargo.toml"),
format!("[workspace]\nresolver = \"2\"\nmembers = [{member_names}]\n{workspace_package}"),
)
.expect("write workspace root");
for (name, rust_version) in members {
let declaration = match rust_version {
Some("workspace") => "rust-version.workspace = true\n".to_owned(),
Some(version) => format!("rust-version = \"{version}\"\n"),
None => String::new(),
};
write_lib(root, name, "0.1.0", &declaration);
}
let manifest = root.join("Cargo.toml");
(tmp, manifest)
}
fn single_package_fixture(rust_version: &str) -> (TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
fs::create_dir_all(root.join("src")).expect("mkdir src");
fs::write(
root.join("Cargo.toml"),
format!("[package]\nname = \"single\"\nversion = \"0.1.0\"\nedition = \"2021\"\nrust-version = \"{rust_version}\"\n"),
)
.expect("write package manifest");
fs::write(root.join("src/lib.rs"), "// fixture\n").expect("write lib");
let manifest = root.join("Cargo.toml");
(tmp, manifest)
}
#[expect(
clippy::too_many_lines,
reason = "the embedded standalone probe stays together so rustc compiles one auditable cross-platform fixture"
)]
fn compile_execution_probe(directory: &Path) -> PathBuf {
let source = directory.join("execution-probe.rs");
let executable = directory.join(format!("execution-probe{}", std::env::consts::EXE_SUFFIX));
fs::write(
&source,
r#"
use std::env;
use std::fs::{self, OpenOptions};
use std::io::{Read as _, Write as _};
use std::process::{self, Command};
use std::thread;
use std::time::{Duration, Instant};
fn append(path: &str, value: &str) {
let mut file = OpenOptions::new().create(true).append(true).open(path).expect("open log");
writeln!(file, "{value}").expect("append log");
}
fn wait_for(path: &std::path::Path) {
let deadline = Instant::now() + Duration::from_secs(10);
while !path.exists() {
if Instant::now() >= deadline {
process::exit(90);
}
thread::sleep(Duration::from_millis(1));
}
}
fn main() {
let args: Vec<String> = env::args().collect();
match args[1].as_str() {
"ordered" => {
let name = &args[2];
println!("{name}:start");
thread::sleep(Duration::from_millis(if name == "alpha" { 250 } else { 20 }));
println!("{name}:end");
append(&args[3], name);
}
"parallel-ordered" => {
let name = &args[2];
let beta_complete = std::path::Path::new(&args[3]).with_extension("beta-complete");
println!("{name}:start");
if name == "alpha" {
wait_for(&beta_complete);
}
println!("{name}:end");
append(&args[3], name);
if name == "beta" {
fs::write(beta_complete, "").expect("write beta completion marker");
}
}
"fail-order" => {
let name = &args[2];
thread::sleep(Duration::from_millis(if name == "alpha" { 180 } else { 20 }));
process::exit(if name == "alpha" { 7 } else { 9 });
}
"fail-stop" => {
let name = &args[2];
let sync_dir = std::path::Path::new(&args[3]);
fs::write(sync_dir.join(format!("{name}.started")), "").expect("write start marker");
match name.as_str() {
"alpha" => {
wait_for(&sync_dir.join("beta.started"));
process::exit(7);
}
"beta" => {
wait_for(&sync_dir.join("alpha.started"));
process::exit(9);
}
_ => process::exit(0),
}
}
"keep-going" => {
let name = &args[2];
append(&args[3], name);
process::exit(if name == "alpha" { 7 } else { 0 });
}
"large-output" => {
let name = &args[2];
let size = 1_100_000;
let stdout_byte = if name == "alpha" { b'A' } else { b'B' };
let stderr_byte = if name == "alpha" { b'C' } else { b'D' };
let mut stdout = std::io::stdout().lock();
writeln!(stdout, "{name}:stdout").expect("write stdout header");
stdout.write_all(&vec![stdout_byte; size]).expect("write large stdout");
let mut stderr = std::io::stderr().lock();
writeln!(stderr, "{name}:stderr").expect("write stderr header");
stderr.write_all(&vec![stderr_byte; size]).expect("write large stderr");
process::exit(if name == "alpha" { 7 } else { 0 });
}
"stdin" => {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input).expect("read stdin");
println!("{}:stdin:{input}", args[2]);
}
"timeout-fail-fast" => {
if args[2] == "alpha" {
thread::sleep(Duration::from_secs(5));
} else {
fs::write(&args[3], "later invocation ran").expect("write later marker");
}
}
"timeout-keep-going" => {
if args[2] == "alpha" {
thread::sleep(Duration::from_secs(5));
} else {
fs::write(&args[3], "later invocation ran").expect("write later marker");
}
}
"tree-parent" => {
let marker = &args[2];
Command::new(env::current_exe().expect("current exe"))
.arg("tree-child")
.arg(marker)
.spawn()
.expect("spawn tree child");
thread::sleep(Duration::from_secs(5));
}
"tree-child" => {
thread::sleep(Duration::from_millis(500));
fs::write(&args[2], "survived").expect("write marker");
}
"background-parent" => {
Command::new(env::current_exe().expect("current exe"))
.arg("background-child")
.arg(&args[2])
.spawn()
.expect("spawn background child");
}
"background-child" => {
thread::sleep(Duration::from_millis(100));
fs::write(&args[2], "completed").expect("write background marker");
}
"timed-background-parent" => {
Command::new(env::current_exe().expect("current exe"))
.arg("timed-background-child")
.arg(&args[2])
.spawn()
.expect("spawn timed background child");
}
"timed-background-child" => {
thread::sleep(Duration::from_millis(1200));
fs::write(&args[2], "completed").expect("write timed background marker");
}
"stubborn-background-parent" => {
Command::new(env::current_exe().expect("current exe"))
.arg("stubborn-background-child")
.arg(&args[2])
.spawn()
.expect("spawn stubborn background child");
}
"stubborn-background-child" => {
thread::sleep(Duration::from_secs(6));
println!("late descendant output");
fs::write(&args[2], "completed").expect("write stubborn background marker");
}
"snapshot-offset-parent" => {
if args[2] == "alpha" {
let mut stdout = std::io::stdout().lock();
stdout.write_all(&vec![b'A'; 1_100_000]).expect("write snapshot payload");
stdout.flush().expect("flush snapshot payload");
Command::new(env::current_exe().expect("current exe"))
.arg("snapshot-offset-child")
.arg(&args[4])
.arg(&args[5])
.spawn()
.expect("spawn snapshot offset child");
fs::write(&args[3], "ready").expect("write snapshot-ready marker");
}
}
"snapshot-offset-child" => {
wait_for(std::path::Path::new(&args[2]));
println!("late descendant output");
fs::write(&args[3], "completed").expect("write snapshot child marker");
}
other => panic!("unknown probe mode: {other}"),
}
}
"#,
)
.expect("write execution probe");
let output = std::process::Command::new("rustc")
.arg(&source)
.arg("-o")
.arg(&executable)
.output()
.expect("rustc must be available to compile the execution probe");
assert!(
output.status.success(),
"failed to compile execution probe:\n{}",
String::from_utf8_lossy(&output.stderr)
);
executable
}
#[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"));
}
#[test]
#[cfg_attr(miri, ignore = "uses temporary directories and spawns cargo-each")]
fn once_implicit_default_members_expand_explicitly() {
let (_tmp, manifest) = default_members_fixture();
each(&manifest)
.args(["--once", "--dry-run", "--", "echo", "{packages}"])
.assert()
.success()
.stdout("echo --package alpha@0.1.0\n");
}
#[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 package_files_union_with_direct_packages_and_each_other() {
let (tmp, manifest) = fixture();
let first = tmp.path().join("first.packages");
let second = tmp.path().join("second.packages");
fs::write(&first, "alpha\n\ngamma@0.1\n").expect("write first package file");
fs::write(&second, "beta\n").expect("write second package file");
each(&manifest)
.arg("--package-file")
.arg(&first)
.arg("--package-file")
.arg(&second)
.args(["--package", "delta", "--dry-run", "--", "echo", "{name}"])
.assert()
.success()
.stdout(
predicate::str::contains("echo alpha")
.and(predicate::str::contains("echo beta"))
.and(predicate::str::contains("echo delta"))
.and(predicate::str::contains("echo gamma")),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn package_file_strips_one_leading_utf8_bom() {
let (tmp, manifest) = fixture();
let packages = tmp.path().join("bom.packages");
fs::write(&packages, "\u{feff}alpha\nbeta\n").expect("write BOM-prefixed package file");
each(&manifest)
.arg("--package-file")
.arg(packages)
.args(["--dry-run", "--", "echo", "{name}"])
.assert()
.success()
.stdout(predicate::str::contains("echo alpha").and(predicate::str::contains("echo beta")));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn present_empty_package_file_is_an_explicit_empty_selection() {
let (tmp, manifest) = default_members_fixture();
let packages = tmp.path().join("empty.packages");
fs::write(&packages, "").expect("write empty package file");
each(&manifest)
.arg("--package-file")
.arg(packages)
.args(["--dry-run", "--", "echo", "{name}:{workspace-rust-version}"])
.assert()
.success()
.stdout(predicate::str::is_empty())
.stderr(predicate::str::contains("nothing to do"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn package_file_precedence_matches_the_selection_contract() {
let (tmp, manifest) = fixture();
let packages = tmp.path().join("alpha.packages");
fs::write(&packages, "alpha\n").expect("write package file");
each(&manifest)
.arg("--package-file")
.arg(&packages)
.args(["--workspace", "--dry-run", "--", "echo", "{name}"])
.assert()
.success()
.stdout(
predicate::str::contains("echo alpha")
.and(predicate::str::contains("echo beta"))
.and(predicate::str::contains("echo gamma")),
);
each(&manifest)
.arg("--package-file")
.arg(packages)
.args(["--none", "--dry-run", "--", "echo", "{name}"])
.assert()
.success()
.stdout(predicate::str::is_empty())
.stderr(predicate::str::contains("nothing to do"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn package_file_input_errors_fail_loudly() {
let (tmp, manifest) = fixture();
let invalid_utf8 = tmp.path().join("invalid-utf8.packages");
fs::write(&invalid_utf8, [0xFF, 0xFE]).expect("write invalid UTF-8");
each(&manifest)
.arg("--package-file")
.arg(&invalid_utf8)
.args(["--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("not valid UTF-8"));
let malformed = tmp.path().join("malformed.packages");
fs::write(&malformed, "alpha\n--workspace\n").expect("write malformed package file");
each(&manifest)
.arg("--package-file")
.arg(&malformed)
.args(["--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("line 2").and(predicate::str::contains("command-line tokens")));
let comment = tmp.path().join("comment.packages");
fs::write(&comment, "#alpha\n").expect("write package file comment");
each(&manifest)
.arg("--package-file")
.arg(&comment)
.args(["--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("comments are not supported"));
let unmatched = tmp.path().join("unmatched.packages");
fs::write(&unmatched, "does-not-exist\n").expect("write unmatched package file");
each(&manifest)
.arg("--package-file")
.arg(&unmatched)
.args(["--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("did not match"));
each(&manifest)
.arg("--package-file")
.arg(tmp.path().join("missing.packages"))
.args(["--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("could not read package file"));
}
#[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}",
"{workspace-rust-version}",
])
.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_without_optional_workspace_version_is_a_successful_noop() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--none", "--once", "--dry-run", "--", "echo", "{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 empty_filtered_selection_skips_workspace_rust_version_resolution() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--filter",
"metadata:does-not-exist",
"--dry-run",
"--",
"echo",
"{name}:{workspace-rust-version}",
])
.assert()
.success()
.stdout(predicate::str::is_empty())
.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}", "{workspace-rust-version}"])
.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_negated_filters_intersect() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--filter",
"not bin",
"--filter",
"not metadata:role=script-only",
"--dry-run",
"--",
"echo",
"{name}",
])
.assert()
.success()
.stdout("echo alpha\necho delta\n");
}
#[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 negated_metadata_filter_drops_matching_member() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--filter",
"not metadata:role=script-only",
"--dry-run",
"--",
"tool",
"{name}",
])
.assert()
.success()
.stdout("tool alpha\ntool beta\ntool delta\ntool epsilon\n");
}
#[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("failed to build command plan")
.and(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("failed to resolve package selection").and(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").and(predicate::str::contains("invalid filter expression `nonsense`")),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn metadata_load_error_has_exact_run_context() {
let missing = PathBuf::from("cargo-each-fixture-that-does-not-exist").join("Cargo.toml");
each(&missing)
.args(["--workspace", "--dry-run", "--", "echo", "{name}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("> failed to load workspace"));
}
#[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(windows)]
#[cfg_attr(miri, ignore = "spawns cargo-each and a child process; miri supports neither")]
#[test]
fn fail_fast_propagates_exact_child_status() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha", "--", "cmd", "/C", "exit", "37"])
.assert()
.failure()
.code(37);
}
#[cfg(unix)]
#[cfg_attr(miri, ignore = "spawns cargo-each and a child process; miri supports neither")]
#[test]
fn fail_fast_propagates_exact_child_status() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["-p", "alpha", "--", "sh", "-c", "exit 37"])
.assert()
.failure()
.code(37);
}
#[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_negated_filter_uses_explicit_packages_not_workspace() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args([
"--workspace",
"--filter",
"not metadata:role=script-only",
"--once",
"--dry-run",
"--",
"cargo",
"x",
"{packages}",
])
.assert()
.success()
.stdout("cargo x --package alpha@0.1.0 --package beta@0.1.0 --package delta@1.2.3-beta.1+build --package epsilon@0.1.0\n");
}
#[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()));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn workspace_rust_version_expands_in_all_modes_and_accepts_lower_members() {
let (_tmp, manifest) = rust_version_fixture(Some("1.80"), &[("alpha", Some("1.70")), ("beta", Some("workspace"))]);
each(&manifest)
.args(["--workspace", "--dry-run", "--", "echo", "{name}:{workspace-rust-version}"])
.assert()
.success()
.stdout(predicate::str::contains("echo alpha:1.80").and(predicate::str::contains("echo beta:1.80")));
each(&manifest)
.args([
"--workspace",
"--each-target",
"lib",
"--dry-run",
"--",
"echo",
"{target}:{workspace-rust-version}",
])
.assert()
.success()
.stdout(predicate::str::contains(":1.80"));
each(&manifest)
.args([
"--package",
"alpha",
"--once",
"--dry-run",
"--",
"echo",
"{workspace-rust-version}",
])
.assert()
.success()
.stdout(predicate::str::contains("echo 1.80"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn workspace_rust_version_validation_is_lazy() {
let (_tmp, manifest) = rust_version_fixture(Some("1.80"), &[("alpha", Some("1.70")), ("beta", None)]);
each(&manifest)
.args(["--workspace", "--dry-run", "--", "echo", "{name}"])
.assert()
.success();
each(&manifest)
.args([
"--package",
"alpha",
"--once",
"--dry-run",
"--",
"echo",
"{workspace-rust-version}",
])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("beta").and(predicate::str::contains("rust-version")));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn workspace_rust_version_rejects_newer_members() {
let (_tmp, manifest) = rust_version_fixture(Some("1.80"), &[("alpha", Some("1.81")), ("beta", Some("1.70"))]);
each(&manifest)
.args(["--workspace", "--once", "--dry-run", "--", "echo", "{workspace-rust-version}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("alpha").and(predicate::str::contains("newer than")));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn workspace_rust_version_is_empty_without_a_root_floor_and_rejects_an_invalid_floor() {
let (_missing, missing_manifest) = rust_version_fixture(None, &[("alpha", Some("1.70"))]);
each(&missing_manifest)
.args([
"--workspace",
"--once",
"--dry-run",
"--",
"workspace-rust-version={workspace-rust-version}",
])
.assert()
.success()
.stdout(predicate::eq("workspace-rust-version=\n"))
.stderr(predicate::str::is_empty());
let (_invalid, invalid_manifest) = rust_version_fixture(Some("2.0"), &[("alpha", Some("1.70"))]);
each(&invalid_manifest)
.args(["--workspace", "--dry-run", "--", "echo", "{name}"])
.assert()
.success();
each(&invalid_manifest)
.args(["--workspace", "--once", "--dry-run", "--", "echo", "{workspace-rust-version}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("2.0").and(predicate::str::contains("Rust 1.x")));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn absent_workspace_rust_version_is_empty_even_when_members_have_no_floor() {
let (_tmp, manifest) = rust_version_fixture(None, &[("alpha", None)]);
each(&manifest)
.args([
"--workspace",
"--once",
"--dry-run",
"--",
"workspace-rust-version={workspace-rust-version}",
])
.assert()
.success()
.stdout(predicate::eq("workspace-rust-version=\n"))
.stderr(predicate::str::is_empty());
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn workspace_rust_version_substitutes_when_declared() {
let (_tmp, manifest) = rust_version_fixture(Some("1.80"), &[("alpha", Some("workspace"))]);
each(&manifest)
.args(["--workspace", "--once", "--dry-run", "--", "echo", "{workspace-rust-version}"])
.assert()
.success()
.stdout(predicate::str::contains("echo 1.80"))
.stderr(predicate::str::is_empty());
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn workspace_rust_version_does_not_absorb_invalid_configuration() {
let (_invalid, invalid_manifest) = rust_version_fixture(Some("2.0"), &[("alpha", Some("1.70"))]);
each(&invalid_manifest)
.args(["--workspace", "--once", "--dry-run", "--", "echo", "{workspace-rust-version}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("2.0").and(predicate::str::contains("Rust 1.x")));
let (_missing_member, missing_member_manifest) = rust_version_fixture(Some("1.80"), &[("alpha", Some("workspace")), ("beta", None)]);
each(&missing_member_manifest)
.args(["--workspace", "--once", "--dry-run", "--", "echo", "{workspace-rust-version}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("beta").and(predicate::str::contains("rust-version")));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn workspace_rust_version_uses_root_package_for_single_package_repository() {
let (_tmp, manifest) = single_package_fixture("1.75");
each(&manifest)
.args(["--once", "--dry-run", "--", "echo", "{workspace-rust-version}"])
.assert()
.success()
.stdout(predicate::str::contains("echo 1.75"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn once_rejects_jobs_greater_than_one() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--once", "--jobs", "2", "--dry-run", "--", "echo", "{packages}"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("--jobs").and(predicate::str::contains("--once")));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn jobs_help_documents_auto_and_default() {
Command::cargo_bin("cargo-each")
.expect("binary")
.args(["each", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("--jobs <N|auto>")
.and(predicate::str::contains("available parallelism"))
.and(predicate::str::contains("Defaults to 1")),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary; miri does not support processes")]
#[test]
fn inline_json_records_preserve_order_duplicates_and_substitute_string_fields() {
json_each()
.args([
"--json-lines",
"{\"package\":\"alpha\",\"test\":\"one\"}\n{\"package\":\"beta\",\"test\":\"two\"}\n{\"package\":\"alpha\",\"test\":\"one\"}",
"--dry-run",
"--",
"echo",
"{json:package}:{json:test}",
])
.assert()
.success()
.stdout(predicate::eq("echo alpha:one\necho beta:two\necho alpha:one\n"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary; miri does not support processes")]
#[test]
fn json_dry_run_escapes_control_characters_quotes_and_backslashes() {
json_each()
.args([
"--json-lines",
"{\"value\":\"first\\nsecond\\t\\\"quoted\\\"\\\\path\"}",
"--dry-run",
"--",
"echo",
"{json:value}",
])
.assert()
.success()
.stdout(predicate::eq("echo \"first\\nsecond\\t\\\"quoted\\\"\\\\path\"\n"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary; miri does not support processes")]
#[test]
fn json_line_files_follow_inline_records_and_need_no_workspace() {
let temp = tempfile::tempdir().expect("tempdir");
let records = temp.path().join("records.jsonl");
fs::write(&records, "\u{feff}{\"value\":\"file\"}\n\n").expect("write JSON Lines");
json_each()
.current_dir(temp.path())
.args(["--json-lines", "{\"value\":\"inline\"}", "--json-lines-file"])
.arg(records)
.args(["--dry-run", "--", "echo", "{json:value}"])
.assert()
.success()
.stdout(predicate::eq("echo inline\necho file\n"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary; miri does not support processes")]
#[test]
fn empty_json_input_is_a_successful_noop() {
json_each()
.args(["--json-lines", "\n", "--", "rustc", "--version"])
.assert()
.success()
.stdout(predicate::str::is_empty())
.stderr(predicate::str::contains("JSON input resolved to no work"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary; miri does not support processes")]
#[test]
fn json_input_rejects_invalid_records_fields_and_placeholders_before_execution() {
for (input, command, expected) in [
("{bad", "{json:value}", "invalid JSON"),
("[]", "{json:value}", "must be an object"),
("{\"other\":\"value\"}", "{json:value}", "field is missing"),
("{\"value\":1}", "{json:value}", "field is not a string"),
("{\"value\":\"ok\"}", "{json:value", "missing its closing"),
("{\"value\":\"ok\"}", "{name}", "not valid in JSON-record mode"),
] {
json_each()
.args(["--json-lines", input, "--dry-run", "--", "echo", command])
.assert()
.failure()
.code(2)
.stdout(predicate::str::is_empty())
.stderr(predicate::str::contains(expected));
}
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and a child process; miri supports neither")]
#[test]
fn json_input_rejects_nul_before_running_earlier_records() {
let temp = tempfile::tempdir().expect("tempdir");
let probe = compile_execution_probe(temp.path());
let log = temp.path().join("executed.log");
json_each()
.args(["--json-lines", "{\"value\":\"alpha\"}\n{\"value\":\"\\u0000\"}", "--"])
.arg(probe)
.args(["ordered", "{json:value}"])
.arg(&log)
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("field contains a NUL byte"));
assert!(!log.exists(), "complete plan validation must precede every child invocation");
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary; miri does not support processes")]
#[test]
fn json_input_rejects_workspace_selection_options() {
json_each()
.args([
"--json-lines",
"{\"value\":\"ok\"}",
"--workspace",
"--dry-run",
"--",
"echo",
"{json:value}",
])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("cannot be combined with `--workspace`"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and child processes; miri supports neither")]
#[test]
fn json_records_execute_through_the_bounded_ordered_scheduler() {
let temp = tempfile::tempdir().expect("tempdir");
let probe = compile_execution_probe(temp.path());
let completion_log = temp.path().join("completion.log");
json_each()
.args(["--json-lines", "{\"name\":\"alpha\"}\n{\"name\":\"beta\"}", "--jobs", "2", "--"])
.arg(probe)
.args(["parallel-ordered", "{json:name}"])
.arg(&completion_log)
.assert()
.success()
.stdout(predicate::eq("alpha:start\nalpha:end\nbeta:start\nbeta:end\n"))
.stderr(predicate::str::contains("cargo each: --json-lines #1:1").and(predicate::str::contains("cargo each: --json-lines #1:2")));
let completion = fs::read_to_string(completion_log).expect("completion log");
assert_eq!(completion, "beta\nalpha\n", "the two JSON invocations must run concurrently");
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn auto_jobs_is_accepted() {
let (_tmp, manifest) = fixture();
each(&manifest)
.args(["--workspace", "--jobs", "auto", "--dry-run", "--", "echo", "{name}"])
.assert()
.success();
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn default_jobs_runs_one_invocation_at_a_time() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let completion_log = tmp.path().join("completion.log");
each(&manifest)
.args(["-p", "alpha", "-p", "beta", "--"])
.arg(probe)
.args(["ordered", "{name}"])
.arg(&completion_log)
.assert()
.success();
assert_eq!(
fs::read_to_string(completion_log).expect("completion log"),
"alpha\nbeta\n",
"the default must finish each invocation before launching the next"
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn requested_parallelism_with_one_invocation_preserves_inherited_stdin() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
each(&manifest)
.args(["-p", "alpha", "--jobs", "2", "--"])
.arg(probe)
.args(["stdin", "{name}"])
.write_stdin("inherited-input")
.assert()
.success()
.stdout(predicate::str::contains("alpha:stdin:inherited-input"));
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn genuinely_parallel_children_receive_null_stdin() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
each(&manifest)
.args(["-p", "alpha", "-p", "beta", "--jobs", "2", "--"])
.arg(probe)
.args(["stdin", "{name}"])
.write_stdin("must-not-reach-children")
.assert()
.success()
.stdout(
predicate::str::contains("alpha:stdin:\n")
.and(predicate::str::contains("beta:stdin:\n"))
.and(predicate::str::contains("must-not-reach-children").not()),
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn parallel_output_is_buffered_in_plan_order() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let completion_log = tmp.path().join("completion.log");
let output = each(&manifest)
.args(["-p", "alpha", "-p", "beta", "--jobs", "2", "--"])
.arg(&probe)
.args(["parallel-ordered", "{name}"])
.arg(&completion_log)
.output()
.expect("run cargo-each");
assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr));
let stdout = String::from_utf8(output.stdout).expect("UTF-8 probe output");
assert_eq!(stdout, "alpha:start\nalpha:end\nbeta:start\nbeta:end\n");
assert_eq!(
fs::read_to_string(completion_log).expect("completion log"),
"beta\nalpha\n",
"the probe must finish out of order to prove cargo-each reordered complete blocks"
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn parallel_fail_fast_chooses_failure_by_plan_order() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
each(&manifest)
.args(["-p", "alpha", "-p", "beta", "--jobs", "2", "--"])
.arg(probe)
.args(["fail-order", "{name}"])
.assert()
.failure()
.code(7);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn parallel_fail_fast_stops_launching_new_work() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let sync_dir = tmp.path().join("fail-stop");
fs::create_dir(&sync_dir).expect("create synchronization directory");
each(&manifest)
.args(["--workspace", "--jobs", "2", "--"])
.arg(probe)
.args(["fail-stop", "{name}"])
.arg(&sync_dir)
.assert()
.failure()
.code(7);
assert!(sync_dir.join("alpha.started").exists(), "the first initial worker must start");
assert!(sync_dir.join("beta.started").exists(), "the second initial worker must start");
for name in ["delta", "epsilon", "gamma"] {
assert!(
!sync_dir.join(format!("{name}.started")).exists(),
"{name} must not launch after either synchronized initial worker fails"
);
}
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn parallel_keep_going_runs_the_complete_plan() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let launch_log = tmp.path().join("launch.log");
each(&manifest)
.args(["--workspace", "--jobs", "2", "--keep-going", "--"])
.arg(probe)
.args(["keep-going", "{name}"])
.arg(&launch_log)
.assert()
.failure()
.code(1);
let launched = fs::read_to_string(launch_log).expect("launch log");
for name in ["alpha", "beta", "delta", "epsilon", "gamma"] {
assert!(launched.contains(name), "{name} must run under --keep-going:\n{launched}");
}
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn parallel_tempfile_capture_preserves_large_stdout_and_stderr_in_plan_order() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let output = each(&manifest)
.args(["-p", "alpha", "-p", "beta", "--jobs", "2", "--keep-going", "--"])
.arg(probe)
.args(["large-output", "{name}"])
.output()
.expect("run cargo-each with large output");
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8(output.stdout).expect("probe stdout is ASCII");
let stderr = String::from_utf8(output.stderr).expect("probe stderr is ASCII");
assert!(stdout.find("alpha:stdout").expect("alpha stdout header") < stdout.find("beta:stdout").expect("beta stdout header"));
assert!(stderr.find("alpha:stderr").expect("alpha stderr header") < stderr.find("beta:stderr").expect("beta stderr header"));
assert_eq!(stdout.bytes().filter(|byte| *byte == b'A').count(), 1_100_000);
assert_eq!(stdout.bytes().filter(|byte| *byte == b'B').count(), 1_100_000);
assert_eq!(stderr.bytes().filter(|byte| *byte == b'C').count(), 1_100_000);
assert_eq!(stderr.bytes().filter(|byte| *byte == b'D').count(), 1_100_000);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn parallel_snapshot_reads_do_not_move_descendant_write_offsets() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let ready = tmp.path().join("snapshot-ready");
let release = tmp.path().join("snapshot-release");
let completed = tmp.path().join("snapshot-child-completed");
let mut command = std::process::Command::new(assert_cmd::cargo::cargo_bin!("cargo-each"));
let _ = command
.arg("each")
.arg("--manifest-path")
.arg(&manifest)
.args(["-p", "alpha", "-p", "beta", "--jobs", "2", "--"])
.arg(probe)
.args(["snapshot-offset-parent", "{name}"])
.arg(&ready)
.arg(&release)
.arg(&completed)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let child = command.spawn().expect("spawn cargo-each snapshot-offset probe");
let ready_deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while !ready.exists() && std::time::Instant::now() < ready_deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(ready.exists(), "the snapshot leader did not become ready");
std::thread::sleep(std::time::Duration::from_millis(100));
fs::write(&release, "release").expect("release the background writer");
let output = child.wait_with_output().expect("collect cargo-each snapshot-offset output");
assert!(
output.status.success(),
"snapshot-offset probe failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("snapshot-offset probe output is ASCII");
assert_eq!(stdout.bytes().filter(|byte| *byte == b'A').count(), 1_100_000);
assert!(
!stdout.contains("late descendant output"),
"bytes appended after snapshot finalization leaked into the finite snapshot"
);
let completed_deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
while !completed.exists() && std::time::Instant::now() < completed_deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(completed.exists(), "the released background writer did not complete");
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn parallel_without_timeout_preserves_ordinary_background_descendants() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let marker = tmp.path().join("background-completed");
each(&manifest)
.args(["-p", "alpha", "--jobs", "2", "--"])
.arg(probe)
.arg("background-parent")
.arg(&marker)
.assert()
.success();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
while !marker.exists() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(
marker.exists(),
"parallel execution without --timeout must not kill an ordinary background descendant"
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn parallel_descendant_inheriting_capture_handles_does_not_delay_snapshot() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let marker = tmp.path().join("stubborn-background-completed");
let started = std::time::Instant::now();
let mut command = std::process::Command::new(assert_cmd::cargo::cargo_bin!("cargo-each"));
let _ = command
.arg("each")
.arg("--manifest-path")
.arg(&manifest)
.args(["-p", "alpha", "-p", "beta", "--jobs", "2", "--"])
.arg(probe)
.arg("stubborn-background-parent")
.arg(&marker);
let status = command.status().expect("run cargo-each with descendant-held capture handles");
assert_eq!(status.code(), Some(0));
assert!(
started.elapsed() < std::time::Duration::from_secs(4),
"capture finalization waited for an inherited handle to close: {:?}",
started.elapsed()
);
assert!(
!marker.exists(),
"cargo-each waited for the ordinary background descendant instead of finalizing a file snapshot"
);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(7);
while !marker.exists() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(
marker.exists(),
"untimed execution must preserve the ordinary background descendant"
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn timed_invocation_preserves_leader_success_while_background_descendant_remains() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let marker = tmp.path().join("timed-background-completed");
let output = each(&manifest)
.args(["-p", "alpha", "--timeout", "500ms", "--"])
.arg(probe)
.arg("timed-background-parent")
.arg(&marker)
.output()
.expect("run timed invocation with ordinary background descendant");
assert!(
output.status.success(),
"leader success must win before the deadline; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
while !marker.exists() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(
marker.exists(),
"ordinary background descendant must remain alive after leader success"
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn sequential_timeout_fail_fast_does_not_run_later_members() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let later_marker = tmp.path().join("later-invocation");
let output = each(&manifest)
.args(["-p", "alpha", "-p", "beta", "--timeout", "50ms", "--"])
.arg(probe)
.args(["timeout-fail-fast", "{name}"])
.arg(&later_marker)
.output()
.expect("run cargo-each timeout fail-fast");
assert_eq!(output.status.code(), Some(1), "stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(String::from_utf8_lossy(&output.stderr).contains("timed out after 50ms"));
assert!(!later_marker.exists(), "fail-fast must not launch the later member");
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn sequential_timeout_keep_going_runs_later_members() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let later_marker = tmp.path().join("later-invocation");
let output = each(&manifest)
.args(["-p", "alpha", "-p", "beta", "--timeout", "1s", "--keep-going", "--"])
.arg(probe)
.args(["timeout-keep-going", "{name}"])
.arg(&later_marker)
.output()
.expect("run cargo-each timeout keep-going");
assert_eq!(output.status.code(), Some(1), "stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(String::from_utf8_lossy(&output.stderr).contains("timed out after 1s"));
assert!(
later_marker.exists(),
"--keep-going must launch the member after a timed-out invocation"
);
}
#[cfg_attr(miri, ignore = "spawns the cargo-each binary and cargo subprocesses; miri supports neither")]
#[test]
fn timeout_kills_an_ordinary_descendant_in_the_process_group() {
let (tmp, manifest) = fixture();
let probe = compile_execution_probe(tmp.path());
let marker = tmp.path().join("grandchild-survived");
let output = each(&manifest)
.args(["-p", "alpha", "-p", "beta", "--jobs", "2", "--timeout", "50ms", "--"])
.arg(probe)
.arg("tree-parent")
.arg(&marker)
.output()
.expect("run cargo-each tree timeout");
assert_eq!(output.status.code(), Some(1), "stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(String::from_utf8_lossy(&output.stderr).contains("timed out after 50ms"));
std::thread::sleep(std::time::Duration::from_millis(700));
assert!(
!marker.exists(),
"a timed-out ordinary descendant must be terminated with its process group"
);
}