mod common;
use common::Repo;
use std::process::Command;
use std::sync::{Mutex, MutexGuard};
static SEQUENTIAL: Mutex<()> = Mutex::new(());
fn alone() -> MutexGuard<'static, ()> {
SEQUENTIAL.lock().unwrap_or_else(|e| e.into_inner())
}
fn manifest(r: &Repo, body: &str) {
r.stage("amont.conf", body);
let out = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("trust")
.current_dir(r.path(""))
.output()
.expect("amont trust");
assert!(out.status.success(), "could not trust the manifest");
}
#[cfg(unix)]
#[test]
fn a_check_that_outlives_the_budget_is_killed_and_fails() {
let _alone = alone();
let r = Repo::new();
let body = "#!/bin/sh\nexec sleep 300\n";
r.stage("slow.sh", body);
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(r.path("slow.sh"), std::fs::Permissions::from_mode(0o755))
.expect("chmod");
r.git(&["add", "slow.sh"]);
manifest(&r, "pre-commit slowpoke * block ./slow.sh\n");
r.git(&["config", "amont.timeout", "1"]);
let started = std::time::Instant::now();
let run = r.hook("pre-commit", &[]);
assert!(
started.elapsed() < std::time::Duration::from_secs(60),
"the deadline never fired"
);
assert!(!run.passed(), "a killed check must fail:\n{}", run.output());
assert!(
run.says("timed out") && run.says("amont.timeout"),
"must say what happened and how to change the budget:\n{}",
run.output()
);
}
#[cfg(unix)]
#[test]
fn a_silent_check_is_killed_by_the_idle_budget() {
let _alone = alone();
let r = Repo::new();
let body = "#!/bin/sh\nexec sleep 300\n";
r.stage("quiet.sh", body);
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(r.path("quiet.sh"), std::fs::Permissions::from_mode(0o755))
.expect("chmod");
r.git(&["add", "quiet.sh"]);
manifest(&r, "pre-commit quiet * block ./quiet.sh\n");
r.git(&["config", "amont.timeout", "0"]);
r.git(&["config", "amont.idleTimeout", "1"]);
let started = std::time::Instant::now();
let run = r.hook("pre-commit", &[]);
assert!(
started.elapsed() < std::time::Duration::from_secs(60),
"the silence budget never fired"
);
assert!(!run.passed(), "a killed check must fail:\n{}", run.output());
assert!(
run.says("printed nothing") && run.says("amont.idleTimeout"),
"must blame the silence and name its key:\n{}",
run.output()
);
assert!(
!run.says("amont.timeout <secs>"),
"the ceiling was off and must not be blamed:\n{}",
run.output()
);
}
#[cfg(unix)]
#[test]
fn a_chatty_check_outlives_the_idle_budget() {
let _alone = alone();
let r = Repo::new();
let body =
"#!/bin/sh\ni=0\nwhile [ $i -lt 75 ]; do i=$((i+1)); echo tick $i; sleep 0.2; done\n";
r.stage("chatty.sh", body);
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(r.path("chatty.sh"), std::fs::Permissions::from_mode(0o755))
.expect("chmod");
r.git(&["add", "chatty.sh"]);
manifest(&r, "pre-commit chatty * block ./chatty.sh\n");
r.git(&["config", "amont.idleTimeout", "10"]);
let run = r.hook("pre-commit", &[]);
assert!(
run.passed(),
"fifteen seconds of steady output must not trip a ten-second silence budget:\n{}",
run.output()
);
assert!(!run.says("killed"), "{}", run.output());
}
#[cfg(unix)]
#[test]
fn concurrent_checks_emit_contiguous_blocks() {
let _alone = alone();
use std::os::unix::fs::PermissionsExt;
let r = Repo::new();
for name in ["alpha", "beta"] {
let file = format!("{name}.sh");
r.stage(
&file,
&format!("#!/bin/sh\necho {name}-first\nsleep 1\necho {name}-second\nexit 0\n"),
);
std::fs::set_permissions(r.path(&file), std::fs::Permissions::from_mode(0o755))
.expect("chmod");
r.git(&["add", &file]);
}
manifest(
&r,
"pre-commit alpha * warn ./alpha.sh\n\
pre-commit beta * warn ./beta.sh\n",
);
let run = r.hook("pre-commit", &[]);
assert!(run.passed(), "{}", run.output());
let out = run.output();
for (name, other) in [("alpha", "beta"), ("beta", "alpha")] {
let first = out
.find(&format!("{name}-first"))
.unwrap_or_else(|| panic!("{name}-first missing:\n{out}"));
let second = out
.find(&format!("{name}-second"))
.unwrap_or_else(|| panic!("{name}-second missing:\n{out}"));
assert!(first < second, "{name}'s lines arrived reversed:\n{out}");
assert!(
!out[first..second].contains(other),
"{name}'s block was interleaved with {other}'s:\n{out}"
);
}
}