use std::error::Error;
use std::fs;
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
type TestResult = Result<(), Box<dyn Error>>;
#[cfg(not(windows))]
const SAMPLE: &str = r#"
update = false
[[target.hello.command]]
name = "say"
cmd = ["echo", "Hello from rake!"]
[target.default]
depends_on = ["hello"]
[[target.default.command]]
name = "say"
cmd = ["echo", "Running default target"]
[[target.boom.command]]
name = "fail"
cmd = ["sh", "-c", "exit 3"]
[[target.skip.command]]
name = "flaky"
cmd = ["sh", "-c", "exit 1"]
skip_on_error = true
[target.after_skip]
depends_on = ["skip"]
[[target.after_skip.command]]
name = "say"
cmd = ["echo", "ran after skip"]
"#;
#[cfg(windows)]
const SAMPLE: &str = r#"
update = false
[[target.hello.command]]
name = "say"
cmd = ["cmd", "/c", "echo", "Hello from rake!"]
[target.default]
depends_on = ["hello"]
[[target.default.command]]
name = "say"
cmd = ["cmd", "/c", "echo", "Running default target"]
[[target.boom.command]]
name = "fail"
cmd = ["cmd", "/c", "exit", "3"]
[[target.skip.command]]
name = "flaky"
cmd = ["cmd", "/c", "exit", "1"]
skip_on_error = true
[target.after_skip]
depends_on = ["skip"]
[[target.after_skip.command]]
name = "say"
cmd = ["cmd", "/c", "echo", "ran after skip"]
"#;
fn rakefile_dir(contents: &str) -> Result<TempDir, Box<dyn Error>> {
let dir = TempDir::new()?;
fs::write(dir.path().join("Rakefile.toml"), contents)?;
Ok(dir)
}
fn cargo_rake(dir: &TempDir) -> Result<Command, Box<dyn Error>> {
let mut cmd = Command::cargo_bin("cargo-rake")?;
let _ = cmd
.arg("rake")
.arg("-f")
.arg(dir.path().join("Rakefile.toml"));
Ok(cmd)
}
#[test]
fn list_prints_targets() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
cargo_rake(&dir)?
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains("hello"))
.stdout(predicate::str::contains(if cfg!(windows) {
"say: cmd /c echo Hello from rake!"
} else {
"say: echo Hello from rake!"
}))
.stdout(predicate::str::contains("depends_on: hello"))
.stdout(predicate::str::contains(if cfg!(windows) {
"flaky: cmd /c exit 1 (skip_on_error)"
} else {
"flaky: sh -c exit 1 (skip_on_error)"
}));
Ok(())
}
#[test]
fn syntax_confirms_valid_rakefile() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
cargo_rake(&dir)?
.arg("syntax")
.assert()
.success()
.stdout(predicate::str::contains("syntax OK"));
Ok(())
}
#[test]
fn syntax_reports_invalid_rakefile() -> TestResult {
let dir = rakefile_dir(
r#"
[[target.broken.command]]
name = "x"
cmd = []
"#,
)?;
cargo_rake(&dir)?
.arg("syntax")
.assert()
.failure()
.stderr(predicate::str::contains("empty 'cmd'"));
Ok(())
}
#[test]
fn version_flag_prints_semver() -> TestResult {
Command::cargo_bin("cargo-rake")?
.args(["rake", "-V"])
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")))
.stdout(predicate::str::contains("rake"));
Ok(())
}
#[test]
fn help_shows_cargo_rake_bin_name() -> TestResult {
Command::cargo_bin("cargo-rake")?
.args(["rake", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("cargo rake"));
Ok(())
}
#[test]
fn runs_named_target() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
cargo_rake(&dir)?
.arg("hello")
.assert()
.success()
.stdout(predicate::str::contains("Hello from rake!"))
.stderr(predicate::str::contains(if cfg!(windows) {
" Running [ rake ] [ say ] cmd /c echo Hello from rake!"
} else {
" Running [ rake ] [ say ] echo Hello from rake!"
}))
.stderr(predicate::str::contains(" Cmd Runtime "))
.stderr(predicate::str::contains(" Runtime "));
Ok(())
}
#[test]
fn runs_multiple_named_targets() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
cargo_rake(&dir)?
.arg("hello")
.arg("after_skip")
.assert()
.success()
.stdout(predicate::str::contains("Hello from rake!"))
.stdout(predicate::str::contains("ran after skip"));
Ok(())
}
#[test]
fn skips_target_with_caret_prefix() -> TestResult {
let (clean_cmd, build_cmd) = if cfg!(windows) {
(
r#"["cmd", "/c", "echo", "CLEANING"]"#,
r#"["cmd", "/c", "echo", "BUILDING"]"#,
)
} else {
(r#"["echo", "CLEANING"]"#, r#"["echo", "BUILDING"]"#)
};
let toml = format!(
"update = false\n\
[[target.clean.command]]\nname = \"wipe\"\ncmd = {clean_cmd}\n\
[[target.build.command]]\nname = \"compile\"\ncmd = {build_cmd}\n\
[target.all]\ndepends_on = [\"clean\", \"build\"]\n"
);
let dir = rakefile_dir(&toml)?;
cargo_rake(&dir)?
.arg("all")
.arg("^clean")
.assert()
.success()
.stdout(predicate::str::contains("BUILDING"))
.stdout(predicate::str::contains("CLEANING").not())
.stderr(predicate::str::contains(
"Skipped [ rake ] [ clean ] skip requested",
));
Ok(())
}
#[test]
fn skip_required_by_other_target_fails_fast() -> TestResult {
let dir = rakefile_dir(
"update = false\n\
[[target.clean.command]]\nname = \"wipe\"\ncmd = [\"echo\", \"CLEANING\"]\n\
[target.build]\ndepends_on = [\"clean\"]\n\
[[target.build.command]]\nname = \"compile\"\ncmd = [\"echo\", \"BUILDING\"]\n\
[target.all]\ndepends_on = [\"build\"]\n",
)?;
cargo_rake(&dir)?
.arg("all")
.arg("^clean")
.assert()
.failure()
.stderr(predicate::str::contains(
"target 'clean' cannot be skipped: required by build",
));
Ok(())
}
#[test]
fn runs_default_target_when_none_given() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
cargo_rake(&dir)?
.assert()
.success()
.stdout(predicate::str::contains("Hello from rake!"))
.stdout(predicate::str::contains("Running default target"));
Ok(())
}
#[test]
fn missing_rakefile_errors() -> TestResult {
Command::cargo_bin("cargo-rake")?
.args(["rake", "-f", "does-not-exist.toml"])
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("unable to read Rakefile"));
Ok(())
}
#[test]
fn invalid_toml_errors() -> TestResult {
let dir = rakefile_dir("oops")?;
cargo_rake(&dir)?
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("unable to parse Rakefile"));
Ok(())
}
#[test]
fn unknown_target_errors() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
cargo_rake(&dir)?
.arg("nope")
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("unknown target 'nope'"));
Ok(())
}
#[test]
fn failing_target_propagates_exit_code() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
cargo_rake(&dir)?.arg("boom").assert().code(3);
Ok(())
}
#[test]
fn spawn_failure_still_prints_runtimes() -> TestResult {
let toml = "update = false\n\
[[target.ghost.command]]\nname = \"missing\"\n\
cmd = [\"this-program-does-not-exist-cargo-rake\"]\n";
let dir = rakefile_dir(toml)?;
cargo_rake(&dir)?
.arg("ghost")
.assert()
.failure()
.stderr(predicate::str::contains(" Cmd Runtime "))
.stderr(predicate::str::contains(" Runtime "))
.stderr(predicate::str::contains("could not launch"));
Ok(())
}
#[test]
fn skip_on_error_continues_chain() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
cargo_rake(&dir)?
.arg("after_skip")
.assert()
.success()
.stdout(predicate::str::contains("ran after skip"));
Ok(())
}
#[cfg(not(windows))]
const AGGREGATOR: &str = r#"
update = false
[[target.one.command]]
name = "say"
cmd = ["echo", "ran one"]
[[target.two.command]]
name = "say"
cmd = ["echo", "ran two"]
[target.all]
depends_on = ["one", "two"]
"#;
#[cfg(windows)]
const AGGREGATOR: &str = r#"
update = false
[[target.one.command]]
name = "say"
cmd = ["cmd", "/c", "echo", "ran one"]
[[target.two.command]]
name = "say"
cmd = ["cmd", "/c", "echo", "ran two"]
[target.all]
depends_on = ["one", "two"]
"#;
#[test]
fn depends_only_target_runs_dependencies() -> TestResult {
let dir = rakefile_dir(AGGREGATOR)?;
cargo_rake(&dir)?
.arg("all")
.assert()
.success()
.stdout(predicate::str::contains("ran one"))
.stdout(predicate::str::contains("ran two"));
Ok(())
}
#[cfg(not(windows))]
const NEEDS_TOOL: &str = r#"
update = false
[tool.cargo.widget]
check = ["false"]
install = ["true"]
[target.build]
tools = ["widget"]
[[target.build.command]]
name = "say"
cmd = ["echo", "built with widget"]
"#;
#[cfg(windows)]
const NEEDS_TOOL: &str = r#"
update = false
[tool.cargo.widget]
check = ["cmd", "/c", "exit", "1"]
install = ["cmd", "/c", "exit", "0"]
[target.build]
tools = ["widget"]
[[target.build.command]]
name = "say"
cmd = ["cmd", "/c", "echo", "built with widget"]
"#;
#[test]
fn missing_tool_is_installed_before_target() -> TestResult {
let dir = rakefile_dir(NEEDS_TOOL)?;
cargo_rake(&dir)?
.arg("build")
.assert()
.success()
.stdout(predicate::str::contains("built with widget"))
.stderr(predicate::str::contains(
"Installing [ rake ] [ check ] widget",
));
Ok(())
}
#[cfg(not(windows))]
const NEEDS_OS_TOOL: &str = r#"
update = false
[tool.os.widget]
check = ["false"]
hint = "install widget from your package manager"
[target.build]
tools = ["widget"]
[[target.build.command]]
name = "say"
cmd = ["echo", "should not run"]
"#;
#[cfg(windows)]
const NEEDS_OS_TOOL: &str = r#"
update = false
[tool.os.widget]
check = ["cmd", "/c", "exit", "1"]
hint = "install widget from your package manager"
[target.build]
tools = ["widget"]
[[target.build.command]]
name = "say"
cmd = ["cmd", "/c", "echo", "should not run"]
"#;
#[test]
fn missing_required_os_tool_aborts() -> TestResult {
let dir = rakefile_dir(NEEDS_OS_TOOL)?;
cargo_rake(&dir)?
.arg("build")
.assert()
.failure()
.code(1)
.stdout(predicate::str::contains("should not run").not())
.stderr(predicate::str::contains(
"the 'widget' tool is required but not installed",
))
.stderr(predicate::str::contains(
"install widget from your package manager",
));
Ok(())
}
#[test]
fn strips_leading_rake_arg() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
Command::cargo_bin("cargo-rake")?
.arg("rake")
.arg("list")
.arg("-f")
.arg(dir.path().join("Rakefile.toml"))
.assert()
.success()
.stdout(predicate::str::contains("hello"));
Ok(())
}
#[test]
fn works_without_rake_prefix() -> TestResult {
let dir = rakefile_dir(SAMPLE)?;
Command::cargo_bin("cargo-rake")?
.arg("list")
.arg("-f")
.arg(dir.path().join("Rakefile.toml"))
.assert()
.success()
.stdout(predicate::str::contains("hello"));
Ok(())
}
#[cfg(not(windows))]
#[test]
fn pre_update_env_var_appears_in_summary() -> TestResult {
let dir = rakefile_dir(
"update = false\n\
[[target.default.command]]\nname = \"ok\"\ncmd = [\"true\"]\n",
)?;
cargo_rake(&dir)?
.env("RAKE_SELF_UPDATED", "0.4.0|0.5.0")
.assert()
.success()
.stderr(predicate::str::contains("Updated"))
.stderr(predicate::str::contains("cargo-rake"))
.stderr(predicate::str::contains("0.4.0"));
Ok(())
}
#[test]
fn basic_subcommand_prints_status() -> TestResult {
Command::cargo_bin("cargo-rake")?
.args(["rake", "basic"])
.assert()
.success()
.stderr(predicate::str::contains("locked").or(predicate::str::contains("enabled for")));
Ok(())
}
#[test]
fn license_subcommand_rejects_malformed_key() -> TestResult {
Command::cargo_bin("cargo-rake")?
.args(["rake", "license", "badkey"])
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("malformed"));
Ok(())
}
#[test]
fn license_remove_no_license_or_no_terminal() -> TestResult {
let output = Command::cargo_bin("cargo-rake")?
.args(["rake", "license", "remove"])
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
let no_file = output.status.success() && stderr.contains("no license key stored");
let no_tty = !output.status.success() && stderr.contains("not a terminal");
assert!(
no_file || no_tty,
"unexpected remove_license output (status={}, stderr={stderr})",
output.status
);
Ok(())
}
#[test]
fn license_info_prints_status() -> TestResult {
Command::cargo_bin("cargo-rake")?
.args(["rake", "license", "info"])
.assert()
.success()
.stderr(
predicate::str::contains("no license active").or(predicate::str::contains("Features")),
);
Ok(())
}