mod common;
use common::{missing, Repo};
use std::io::Write;
use std::process::{Command, Stdio};
fn crate_files(r: &Repo, body: &str) {
r.stage(
"Cargo.toml",
"[package]\nname = \"t\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n",
);
r.stage("src/main.rs", body);
}
fn pre_push(r: &Repo, from: &str, to: &str) -> i32 {
let line = format!("refs/heads/feat/x {to} refs/heads/feat/x {from}\n");
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-cargo-test")
.current_dir(&r.dir)
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.expect("stdin")
.write_all(line.as_bytes())
.expect("write");
child.wait().expect("wait").code().unwrap_or(-1)
}
fn head(r: &Repo) -> String {
String::from_utf8_lossy(&r.git(&["rev-parse", "HEAD"]).stdout)
.trim()
.to_string()
}
#[test]
fn a_non_rust_repo_runs_nothing() {
let r = Repo::new();
r.stage("a.py", "x = 1\n");
r.stage("README.md", "hi\n");
assert!(r.hook("pre-commit-cargo-fmt", &[]).passed());
assert!(r.hook("pre-commit-clippy", &[]).passed());
}
#[test]
fn a_rust_file_outside_any_crate_is_skipped() {
let r = Repo::new();
r.stage("scripts/loose.rs", "fn main() {}\n");
let run = r.hook("pre-commit-cargo-fmt", &[]);
assert!(run.passed(), "{}", run.stdout);
assert!(
!run.says("Rust formatting"),
"should not have run cargo at all: {}",
run.stdout
);
}
#[test]
fn cargo_fmt_rejects_unformatted_rust_and_accepts_clean() {
if missing("cargo") {
return;
}
let r = Repo::new();
crate_files(&r, "fn main(){let _x=1;}\n");
assert!(
!r.hook("pre-commit-cargo-fmt", &[]).passed(),
"unformatted code must fail"
);
let r2 = Repo::new();
crate_files(&r2, "fn main() {\n let _x = 1;\n}\n");
let run = r2.hook("pre-commit-cargo-fmt", &[]);
assert!(run.passed(), "formatted code must pass: {}", run.stdout);
assert!(run.says("Rust formatting is clean"));
}
#[test]
fn cargo_fmt_repairs_and_restages_when_asked() {
if missing("cargo") {
return;
}
let r = Repo::new();
r.git(&["config", "amont.fix", "true"]);
crate_files(&r, "fn main(){let _x=1;}\n");
let run = r.hook("pre-commit-cargo-fmt", &[]);
assert!(
run.passed(),
"the repair should let it through: {}",
run.stdout
);
assert!(run.says("reformatted and re-staged"), "{}", run.stdout);
let staged = r.git(&["show", ":src/main.rs"]);
assert_eq!(
String::from_utf8_lossy(&staged.stdout),
"fn main() {\n let _x = 1;\n}\n",
"the repair reached the index, not just the disk"
);
}
#[test]
fn cargo_fmt_leaves_files_alone_when_fixing_is_off() {
if missing("cargo") {
return;
}
let r = Repo::new();
crate_files(&r, "fn main(){let _x=1;}\n");
assert!(!r.hook("pre-commit-cargo-fmt", &[]).passed());
assert_eq!(
std::fs::read_to_string(r.path("src/main.rs")).expect("read"),
"fn main(){let _x=1;}\n",
"it rewrote a file nobody asked it to rewrite"
);
}
#[test]
fn a_repair_does_not_stage_an_unrelated_unformatted_file() {
if missing("cargo") {
return;
}
let r = Repo::new();
r.git(&["config", "amont.fix", "true"]);
crate_files(&r, "mod other;\nfn main() {}\n");
r.stage("src/other.rs", "pub fn q() {}\n");
r.commit("chore: seed");
r.stage("src/main.rs", "mod other;\nfn main(){let _x=1;}\n");
r.write("src/other.rs", "pub fn q(){let _y=2;}\n");
let run = r.hook("pre-commit-cargo-fmt", &[]);
assert!(run.passed(), "{}", run.stdout);
let staged = r.git(&["diff", "--cached", "--name-only"]);
let staged = String::from_utf8_lossy(&staged.stdout);
assert!(staged.contains("src/main.rs"), "{staged}");
assert!(
!staged.contains("src/other.rs"),
"the repair swept in work the author kept back: {staged}"
);
}
#[test]
fn clippy_denies_warnings() {
if missing("cargo") {
return;
}
let r = Repo::new();
crate_files(&r, "fn main() {\n let unused = 1;\n}\n");
assert!(
!r.hook("pre-commit-clippy", &[]).passed(),
"an unused variable must fail under -D warnings"
);
let r2 = Repo::new();
crate_files(&r2, "fn main() {\n println!(\"ok\");\n}\n");
let run = r2.hook("pre-commit-clippy", &[]);
assert!(run.passed(), "clean code must pass: {}", run.stdout);
assert!(run.says("Clippy passed"));
}
#[test]
fn a_manifest_only_change_still_runs_clippy() {
if missing("cargo") {
return;
}
let r = Repo::new();
crate_files(&r, "fn main() {\n println!(\"ok\");\n}\n");
r.commit("feat: base");
r.stage(
"Cargo.toml",
"[package]\nname = \"t\"\nversion = \"0.2.0\"\nedition = \"2021\"\n\n[dependencies]\n",
);
let run = r.hook("pre-commit-clippy", &[]);
assert!(run.passed(), "{}", run.stdout);
assert!(
run.says("Clippy passed"),
"a manifest-only change should still have run clippy: {}",
run.stdout
);
}
#[test]
fn cargo_test_gates_the_push() {
if missing("cargo") {
return;
}
let r = Repo::new();
crate_files(
&r,
"fn main() {}\n\n#[test]\nfn passes() {\n assert_eq!(1, 1);\n}\n",
);
r.commit("feat: base");
let base = head(&r);
r.stage(
"src/main.rs",
"fn main() {}\n\n#[test]\nfn fails() {\n assert_eq!(1, 2);\n}\n",
);
r.commit("feat: a failing test");
assert_ne!(
pre_push(&r, &base, &head(&r)),
0,
"a failing test must abort the push"
);
}
#[test]
fn a_docs_only_push_runs_no_tests() {
let r = Repo::new();
crate_files(&r, "fn main() {}\n");
r.commit("feat: base");
let base = head(&r);
r.stage("README.md", "docs\n");
r.commit("docs: readme");
assert_eq!(pre_push(&r, &base, &head(&r)), 0);
}
#[test]
fn the_test_gate_does_not_leak_git_env_to_the_suite() {
if missing("cargo") {
return;
}
let r = Repo::new();
crate_files(
&r,
"fn main() {}\n\n#[test]\nfn no_git_env() {\n \
for (k, _) in std::env::vars() {\n \
assert!(!k.starts_with(\"GIT_\"), \"leaked {k}\");\n }\n}\n",
);
r.commit("feat: base");
let base = head(&r);
r.stage("src/main.rs", "fn main() {}\n\n#[test]\nfn no_git_env() {\n for (k, _) in std::env::vars() {\n assert!(!k.starts_with(\"GIT_\"), \"leaked {k}\");\n }\n}\n// touch\n");
r.commit("feat: touch");
let line = format!("refs/heads/feat/x {} refs/heads/feat/x {base}\n", head(&r));
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-cargo-test")
.current_dir(&r.dir)
.env("GIT_DIR", r.path(".git"))
.env("GIT_INDEX_FILE", r.path(".git/index"))
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.expect("stdin")
.write_all(line.as_bytes())
.expect("write");
let code = child.wait().expect("wait").code().unwrap_or(-1);
assert_eq!(code, 0, "the suite saw git's environment");
}
#[test]
fn a_new_branch_with_rust_several_commits_back_still_runs_the_suite() {
if missing("cargo") {
return;
}
let r = Repo::new();
r.stage("README.md", "start\n");
r.commit("chore: base");
crate_files(
&r,
"fn main() {}\n\n#[test]\nfn t() {\n assert!(false);\n}\n",
);
r.commit("feat: add a failing crate");
r.stage("README.md", "start\nand more\n");
r.commit("docs: expand readme");
let zero = "0".repeat(40);
let code = pre_push(&r, &zero, &head(&r));
assert_ne!(
code, 0,
"a new branch's failing Rust suite, added before the tip commit, was silently skipped"
);
}
#[test]
fn each_ref_in_a_multi_ref_push_is_tested_against_its_own_tip() {
if missing("cargo") {
return;
}
let r = Repo::new();
r.git(&["config", "amont.testPushedTree", "true"]);
r.stage("README.md", "start\n");
r.commit("chore: base");
let base = head(&r);
r.git(&["checkout", "-q", "-b", "a"]);
r.stage(
"crate/Cargo.toml",
"[package]\nname = \"t\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n",
);
r.stage(
"crate/src/main.rs",
"fn main() {}\n\n#[test]\nfn t() {\n assert!(true);\n}\n",
);
r.commit("feat(a): passing crate");
let tip_a = head(&r);
r.git(&["checkout", "-q", &base]);
r.git(&["checkout", "-q", "-b", "b"]);
r.stage(
"crate/Cargo.toml",
"[package]\nname = \"t\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n",
);
r.stage(
"crate/src/main.rs",
"fn main() {}\n\n#[test]\nfn t() {\n assert!(false);\n}\n",
);
r.commit("feat(b): failing crate");
let tip_b = head(&r);
let zero = "0".repeat(40);
let lines = format!(
"refs/heads/a {tip_a} refs/heads/a {zero}\n\
refs/heads/b {tip_b} refs/heads/b {zero}\n"
);
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-cargo-test")
.current_dir(&r.dir)
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.expect("stdin")
.write_all(lines.as_bytes())
.expect("write");
let code = child.wait().expect("wait").code().unwrap_or(-1);
assert_ne!(
code, 0,
"branch b's failing crate was tested against branch a's tree instead of its own"
);
}