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}"
);
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
const WATCHDOG: std::time::Duration = std::time::Duration::from_secs(60);
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn burn(secs: u32) -> String {
format!(
"end=$(( $(date +%s) + {secs} ))\n\
echo burning\n\
while :; do i=0; while [ $i -lt 20000 ]; do i=$((i+1)); done; \
[ \"$(date +%s)\" -ge \"$end\" ] && break; done\n"
)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn script(r: &Repo, name: &str, body: &str) {
use std::os::unix::fs::PermissionsExt;
r.stage(name, &format!("#!/bin/sh\ntrap 'kill 0' EXIT\n{body}"));
std::fs::set_permissions(r.path(name), std::fs::Permissions::from_mode(0o755)).expect("chmod");
r.git(&["add", name]);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn busy_budgets(r: &Repo) {
r.git(&["config", "amont.idleTimeout", "4"]);
r.git(&["config", "amont.timeout", "12"]);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn trace_file(tag: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!("amont-cpu-trace-{}-{tag}", std::process::id()));
let _ = std::fs::remove_file(&p);
p
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn killed_at_the_ceiling(
run: &common::HookRun,
took: std::time::Duration,
trace: &std::path::Path,
) {
let seen = std::fs::read_to_string(trace).unwrap_or_default();
let _ = std::fs::remove_file(trace);
assert!(!run.passed(), "a killed check must fail:\n{}", run.output());
assert!(
took >= std::time::Duration::from_secs(11),
"killed after {took:?}: the silence budget fired on a busy check:\n{}\n\
what the sampler saw (pid start ppid cpu_ns):\n{seen}",
run.output()
);
assert!(
run.says("timed out") && run.says("kept its CPU busy"),
"must be the ceiling, naming the busy tree:\n{}\nsampler:\n{seen}",
run.output()
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn a_silent_busy_check_answers_to_the_ceiling_not_the_silence_budget() {
let _alone = alone();
let r = Repo::new();
script(&r, "busy.sh", &burn(24));
manifest(&r, "pre-commit busy * block sh ./busy.sh\n");
busy_budgets(&r);
let trace = trace_file("busy");
let (run, took) = r.hook_watched(
"pre-commit",
&[("AMONT_CPU_TRACE", trace.as_os_str())],
WATCHDOG,
);
killed_at_the_ceiling(&run, took, &trace);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn fork_per_file_work_keeps_a_silent_check_alive() {
let _alone = alone();
let r = Repo::new();
let body = "end=$(( $(date +%s) + 24 ))\n\
echo burning\n\
while [ \"$(date +%s)\" -lt \"$end\" ]; do \
sh -c 'i=0; while [ $i -lt 3000 ]; do i=$((i+1)); done'; done\n";
script(&r, "churn.sh", body);
manifest(&r, "pre-commit churn * block sh ./churn.sh\n");
busy_budgets(&r);
let trace = trace_file("churn");
let (run, took) = r.hook_watched(
"pre-commit",
&[("AMONT_CPU_TRACE", trace.as_os_str())],
WATCHDOG,
);
killed_at_the_ceiling(&run, took, &trace);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn a_seen_worker_keeps_counting_after_its_parent_exits() {
let _alone = alone();
let r = Repo::new();
let trace = trace_file("orphan");
let worker = burn(24).trim_end().replace('\n', "; ");
let body = format!(
"sh -c '({worker}) & w=$!; n=0; \
while [ $n -lt 200 ]; do grep -q \"^$w \" \"$AMONT_CPU_TRACE\" 2>/dev/null && break; \
n=$((n+1)); sleep 0.05; done'\n\
sleep 24\n"
);
script(&r, "orphan.sh", &body);
manifest(&r, "pre-commit orphan * block sh ./orphan.sh\n");
busy_budgets(&r);
let (run, took) = r.hook_watched(
"pre-commit",
&[("AMONT_CPU_TRACE", trace.as_os_str())],
WATCHDOG,
);
killed_at_the_ceiling(&run, took, &trace);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn an_idle_silent_check_still_dies_at_the_budget_and_says_it_was_measured() {
let _alone = alone();
let r = Repo::new();
script(&r, "idle.sh", "exec sleep 30\n");
manifest(&r, "pre-commit idle * block ./idle.sh\n");
r.git(&["config", "amont.idleTimeout", "3"]);
r.git(&["config", "amont.timeout", "0"]);
let (run, took) = r.hook_watched("pre-commit", &[], WATCHDOG);
assert!(!run.passed(), "{}", run.output());
assert!(
took < std::time::Duration::from_secs(10),
"took {took:?}:\n{}",
run.output()
);
assert!(
run.says("printed nothing") && run.says("did no measurable CPU work"),
"must say it was silent AND measured idle:\n{}",
run.output()
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn with_cpu_credit_off_a_busy_silent_check_dies_at_the_budget() {
let _alone = alone();
let r = Repo::new();
script(&r, "busy.sh", &burn(20));
manifest(&r, "pre-commit busy * block sh ./busy.sh\n");
busy_budgets(&r);
r.git(&["config", "amont.idleCpuCredit", "false"]);
let (run, took) = r.hook_watched("pre-commit", &[], WATCHDOG);
assert!(!run.passed(), "{}", run.output());
assert!(
took < std::time::Duration::from_secs(7),
"took {took:?}:\n{}",
run.output()
);
assert!(
run.says("printed nothing for 4s and was killed"),
"{}",
run.output()
);
assert!(
!run.says("CPU"),
"no CPU claim when CPU is not sampled:\n{}",
run.output()
);
}