mod common;
use common::{missing, Repo};
use std::process::Command;
fn run(r: &Repo, args: &[&str]) -> (i32, String) {
let out = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("run")
.args(args)
.current_dir(&r.dir)
.output()
.expect("amont run");
(
out.status.code().unwrap_or(-1),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
#[test]
fn run_checks_the_staged_set() {
let r = Repo::new();
r.stage("good.json", "{ \"a\": 1 }\n");
r.commit("chore: seed");
r.write("bad.json", "{ BROKEN\n");
r.stage("good.json", "{ \"a\": 2 }\n");
let (code, out) = run(&r, &[]);
assert_eq!(code, 0, "the staged set is valid:\n{out}");
assert!(
!out.contains("bad.json"),
"it looked outside the index:\n{out}"
);
}
#[test]
fn all_files_sees_what_is_not_staged() {
let r = Repo::new();
r.stage("good.json", "{ \"a\": 1 }\n");
r.commit("chore: seed");
r.stage("bad.json", "{ BROKEN\n");
r.commit("chore: add bad");
let (code, out) = run(&r, &["--all-files"]);
assert_ne!(code, 0, "should have found the broken file:\n{out}");
assert!(out.contains("bad.json"), "{out}");
}
#[test]
fn the_two_modes_answer_different_questions() {
let r = Repo::new();
r.stage("good.json", "{ \"a\": 1 }\n");
r.commit("chore: seed");
r.stage("bad.json", "{ BROKEN\n");
r.commit("chore: add bad");
let (staged, _) = run(&r, &[]);
let (all, _) = run(&r, &["--all-files"]);
assert_eq!(staged, 0, "nothing staged means nothing to judge");
assert_ne!(all, 0, "but the tree still holds a broken file");
}
#[test]
fn run_judges_the_staged_content_not_the_tree() {
if missing("node") {
return;
}
let r = Repo::new();
r.stage("x.json", "{ \"a\": 1 }\n");
r.commit("chore: seed");
r.stage("x.json", "{ \"a\": 2 }\n");
r.write("x.json", "{ THIS IS NOT JSON\n");
let (code, out) = run(&r, &[]);
assert_eq!(code, 0, "it judged the working tree, not the index:\n{out}");
assert_eq!(
std::fs::read_to_string(r.path("x.json")).expect("read"),
"{ THIS IS NOT JSON\n",
"the unstaged work was not put back"
);
}
#[test]
fn run_still_catches_a_broken_staged_change() {
if missing("node") {
return;
}
let r = Repo::new();
r.stage("x.json", "{ \"a\": 1 }\n");
r.commit("chore: seed");
r.stage("x.json", "{ THIS IS NOT JSON\n");
r.write("x.json", "{ \"a\": 2 }\n");
let (code, out) = run(&r, &[]);
assert_ne!(code, 0, "a broken staged change passed:\n{out}");
assert_eq!(
std::fs::read_to_string(r.path("x.json")).expect("read"),
"{ \"a\": 2 }\n",
"the unstaged work was not put back"
);
}
#[cfg(unix)]
#[test]
fn all_files_takes_no_hold() {
let r = Repo::new();
r.stage("x.json", "{ \"a\": 1 }\n");
r.stage("amont.conf", "pre-commit slow * block sleep 5\n");
r.commit("chore: seed");
r.write("x.json", "{ \"a\": 999 }\n");
let trusted = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("trust")
.current_dir(&r.dir)
.output()
.expect("amont trust");
assert!(trusted.status.success(), "could not trust the manifest");
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("run")
.arg("--all-files")
.current_dir(&r.dir)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn amont run --all-files");
let store = r.path(".git/amont-held");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
while std::time::Instant::now() < deadline {
assert!(
!store.exists(),
"--all-files took a hold; it must be stash-free"
);
std::thread::sleep(std::time::Duration::from_millis(20));
}
let _ = child.kill();
let _ = child.wait();
}
#[cfg(unix)]
#[test]
fn a_named_pre_push_check_takes_no_hold() {
let r = Repo::new();
r.stage("x.json", "{ \"a\": 1 }\n");
r.commit("chore: seed");
let origin = r.path(".git/test-origin.git");
r.git(&["init", "-q", "--bare", origin.to_str().unwrap()]);
r.git(&["remote", "add", "origin", origin.to_str().unwrap()]);
r.git(&["push", "-q", "--no-verify", "-u", "origin", "main"]);
r.write("x.json", "{ \"a\": 999 }\n");
let (_, out) = run(&r, &["pre-push-branch-pattern"]);
assert!(
!r.path(".git/amont-held").exists(),
"a pre-push check parked the tree:\n{out}"
);
assert_eq!(
std::fs::read_to_string(r.path("x.json")).expect("read"),
"{ \"a\": 999 }\n",
"a pre-push check modified the working tree:\n{out}"
);
}
#[test]
fn all_files_never_stages_anything() {
let r = Repo::new();
r.stage("a.js", "const a = BAD\n");
r.stage("b.js", "const b = BAD\n");
r.write(
"rewrite.sh",
"#!/bin/sh\nfor f in *.js; do [ -f \"$f\" ] || continue; sed -i.bak 's/BAD/GOOD/' \"$f\"; rm -f \"$f.bak\"; done\nexit 0\n",
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(r.path("rewrite.sh"), std::fs::Permissions::from_mode(0o755))
.expect("chmod");
}
r.git(&["add", "rewrite.sh"]);
r.stage(
"amont.conf",
"pre-commit fmt *.js block fix ./rewrite.sh\n",
);
r.commit("chore: seed");
let trusted = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("trust")
.current_dir(&r.dir)
.output()
.expect("amont trust");
assert!(trusted.status.success(), "could not trust the manifest");
r.git(&["config", "amont.fix", "true"]);
r.write("a.js", "const a = BAD\n");
r.write("b.js", "const b = STILL BAD\n");
let (_, out) = run(&r, &["--all-files"]);
let staged = r.git(&["diff", "--cached", "--name-only"]);
let staged = String::from_utf8_lossy(&staged.stdout);
assert!(
staged.trim().is_empty(),
"--all-files staged files: {staged}\n{out}"
);
assert!(
out.contains("fixing is off"),
"it must say why amont.fix did nothing:\n{out}"
);
}
#[test]
fn a_named_check_runs_alone() {
let r = Repo::new();
r.stage("a.txt", "hello\n");
let (code, out) = run(&r, &["pre-commit-merge-conflict"]);
assert_eq!(code, 0, "{out}");
assert!(out.contains("merge"), "the named check did not run:\n{out}");
assert!(
!out.contains("package.json"),
"it ran the whole stage instead:\n{out}"
);
}
#[test]
fn an_unknown_check_is_an_error_not_a_pass() {
let r = Repo::new();
let (code, out) = run(&r, &["pre-commit-not-a-thing"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("amont list"), "should point somewhere:\n{out}");
}
#[test]
fn a_named_pre_push_check_with_no_upstream_errors_instead_of_passing_empty() {
let r = Repo::new();
r.stage("a.txt", "x\n");
r.commit("init");
let (code, out) = run(&r, &["pre-push-branch-protect"]);
assert_eq!(code, 2, "should refuse, not silently pass:\n{out}");
assert!(out.contains("upstream"), "{out}");
}
#[test]
fn a_named_pre_push_check_uses_the_upstream_as_its_push_ref() {
let r = Repo::new();
r.stage("a.txt", "x\n");
r.commit("init");
let origin = r.path(".git/test-origin.git");
r.git(&["init", "-q", "--bare", origin.to_str().unwrap()]);
r.git(&["remote", "add", "origin", origin.to_str().unwrap()]);
r.git(&["push", "-q", "--no-verify", "-u", "origin", "main"]);
let (code, out) = run(&r, &["pre-push-branch-protect"]);
assert_ne!(
code, 0,
"a synthesized push to main must be blocked:\n{out}"
);
assert!(out.contains("main"), "{out}");
}