mod common;
use common::{missing, Repo};
use std::io::Write;
use std::process::{Command, Stdio};
fn push_range(r: &Repo, from: &str, to: &str) -> i32 {
let line = format!("refs/heads/main {to} refs/heads/main {from}\n");
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-run-tests-js")
.current_dir(&r.dir)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.unwrap()
.write_all(line.as_bytes())
.unwrap();
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 runs_a_packages_gate_and_passes_when_it_succeeds() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 0"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: a");
assert_eq!(push_range(&r, &base, &head(&r)), 0);
}
#[test]
fn fails_when_the_gate_fails() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 1"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: a");
assert_ne!(push_range(&r, &base, &head(&r)), 0);
}
#[test]
fn runs_the_gate_in_order_and_never_lint() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"typecheck":"echo typecheck>>ran.txt","lint":"echo lint>>ran.txt","test:unit":"echo unit>>ran.txt","test":"echo test>>ran.txt"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: a");
assert_eq!(push_range(&r, &base, &head(&r)), 0);
let ran = std::fs::read_to_string(r.path("ran.txt")).unwrap_or_default();
assert_eq!(
ran.lines().map(str::trim).collect::<Vec<_>>(),
vec!["typecheck", "unit", "test"]
);
assert!(
!ran.contains("lint"),
"lint belongs to pre-commit, not here"
);
}
#[test]
fn a_failure_skips_the_rest_of_the_gate() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"typecheck":"exit 1","test:unit":"echo unit>>ran.txt","test":"echo test>>ran.txt"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: a");
assert_ne!(push_range(&r, &base, &head(&r)), 0);
assert!(
!r.path("ran.txt").exists(),
"nothing after the failure should run"
);
}
fn push_new_branch(r: &Repo, branch: &str, tip: &str) -> i32 {
let zero = "0".repeat(tip.len());
let line = format!("refs/heads/{branch} {tip} refs/heads/{branch} {zero}\n");
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-run-tests-js")
.current_dir(&r.dir)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.unwrap()
.write_all(line.as_bytes())
.unwrap();
child.wait().expect("wait").code().unwrap_or(-1)
}
fn with_origin(r: &Repo) {
let origin = r.path(".git/test-origin.git");
r.git(&["init", "-q", "--bare", origin.to_str().expect("utf8")]);
r.git(&["remote", "add", "origin", origin.to_str().expect("utf8")]);
r.git(&["push", "-q", "--no-verify", "origin", "main"]);
}
#[test]
fn a_new_branch_is_judged_by_every_commit_not_just_its_tip() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 1"}}"#,
);
r.commit("chore: base");
with_origin(&r);
r.git(&["checkout", "-q", "-b", "feat/x"]);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: the js change");
r.stage("README.md", "docs\n");
r.commit("docs: on top of it");
assert_ne!(
push_new_branch(&r, "feat/x", &head(&r)),
0,
"the failing package was two commits back and the gate never ran"
);
}
#[test]
fn a_file_reverted_later_in_the_same_push_still_selects_its_package() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 1"}}"#,
);
r.stage("src/a.js", "const x = 1;\n");
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 2;\n");
r.commit("feat: touch the js");
r.stage("src/a.js", "const x = 1;\n");
r.commit("revert: put it back");
assert_ne!(
push_range(&r, &base, &head(&r)),
0,
"the endpoints' trees are identical, but a pushed commit touched .js"
);
}
#[test]
fn a_non_js_change_runs_no_gate() {
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 1"}}"#,
);
r.commit("chore: seed");
let base = head(&r);
r.stage("README.md", "docs\n");
r.commit("docs: readme");
assert_eq!(push_range(&r, &base, &head(&r)), 0);
}