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";
staged_exec(&r, "slow.sh", body);
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";
staged_exec(&r, "quiet.sh", body);
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]
#[ignore = "real-clock load scenario: CI runs it (`make test-load`), not every commit (ADR-0009)"]
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";
staged_exec(&r, "chatty.sh", body);
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();
let r = Repo::new();
for name in ["alpha", "beta"] {
staged_exec(
&r,
&format!("{name}.sh"),
&format!("#!/bin/sh\necho {name}-first\nsleep 1\necho {name}-second\nexit 0\n"),
);
}
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) {
staged_exec(r, name, &format!("#!/bin/sh\ntrap 'kill 0' EXIT\n{body}"));
}
#[cfg(unix)]
fn staged_exec(r: &Repo, name: &str, body: &str) {
common::fixture_exec(&r.path(name), body);
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]
#[ignore = "real-clock load scenario: CI runs it (`make test-load`), not every commit (ADR-0009)"]
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]
#[ignore = "real-clock load scenario: CI runs it (`make test-load`), not every commit (ADR-0009)"]
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]
#[ignore = "real-clock load scenario: CI runs it (`make test-load`), not every commit (ADR-0009)"]
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]
#[ignore = "real-clock load scenario: CI runs it (`make test-load`), not every commit (ADR-0009)"]
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()
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
const CARGO_WAIT: &str = " Blocking waiting for file lock on build directory";
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
#[ignore = "real-clock load scenario: CI runs it (`make test-load`), not every commit (ADR-0009)"]
fn a_declared_lock_wait_outlives_the_silence_budget() {
let _alone = alone();
let r = Repo::new();
{
use std::os::unix::fs::PermissionsExt;
r.stage(
"wait.sh",
&format!(
"#!/bin/sh\necho '{CARGO_WAIT}' >&2\nsleep 6\necho ' Checking x v0.1.0' >&2\n"
),
);
std::fs::set_permissions(r.path("wait.sh"), std::fs::Permissions::from_mode(0o755))
.expect("chmod");
r.git(&["add", "wait.sh"]);
}
manifest(&r, "pre-commit locked * block sh ./wait.sh\n");
r.git(&["config", "amont.idleTimeout", "2"]);
r.git(&["config", "amont.timeout", "12"]);
let (run, took) = r.hook_watched("pre-commit", &[], WATCHDOG);
assert!(
run.passed(),
"a declared wait is not a hang:\n{}",
run.output()
);
assert!(took < std::time::Duration::from_secs(11), "took {took:?}");
assert!(
run.says("waited") && run.says("the cargo lock on the build directory"),
"a passing check says what it waited for:\n{}",
run.output()
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn a_lock_wait_past_its_budget_is_killed_and_named() {
let _alone = alone();
let r = Repo::new();
script(
&r,
"stuck.sh",
&format!("echo '{CARGO_WAIT}' >&2\nexec sleep 30\n"),
);
manifest(&r, "pre-commit locked * block sh ./stuck.sh\n");
r.git(&["config", "amont.idleTimeout", "60"]);
r.git(&["config", "amont.lockWait", "2"]);
r.git(&["config", "amont.timeout", "12"]);
let (run, took) = r.hook_watched("pre-commit", &[], WATCHDOG);
assert!(!run.passed(), "{}", run.output());
assert!(took < std::time::Duration::from_secs(8), "took {took:?}");
assert!(
run.says("waited 2s for the cargo lock on the build directory")
&& run.says("amont.lockWait"),
"must name the wait, the lock and the key:\n{}",
run.output()
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn a_marker_on_stdout_does_not_pause_the_clock() {
let _alone = alone();
let r = Repo::new();
script(
&r,
"out.sh",
&format!("echo '{CARGO_WAIT}'\nexec sleep 30\n"),
);
manifest(&r, "pre-commit locked * block sh ./out.sh\n");
r.git(&["config", "amont.idleTimeout", "2"]);
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:?}");
assert!(run.says("printed nothing"), "{}", run.output());
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
#[ignore = "real-clock load scenario: CI runs it (`make test-load`), not every commit (ADR-0009)"]
fn an_unmeasured_check_answers_to_the_extended_budget() {
let _alone = alone();
let r = Repo::new();
script(&r, "idle.sh", "sleep 30 >/dev/null 2>&1 &\nwait\n");
manifest(&r, "pre-commit idle * block ./idle.sh\n");
r.git(&["config", "amont.idleTimeout", "2"]);
r.git(&["config", "amont.timeout", "0"]);
let zero = std::ffi::OsString::from("0");
let four = std::ffi::OsString::from("4");
let held = std::ffi::OsString::from("held");
let (run, took) = r.hook_watched_unpinned(
"pre-commit",
&[
("AMONT_CPU_MAX_PROCS", zero.as_os_str()),
("AMONT_IDLE_LOAD_SCALE", four.as_os_str()),
("AMONT_HOST_SLOT", held.as_os_str()),
],
WATCHDOG,
);
assert!(!run.passed(), "{}", run.output());
assert!(
took >= std::time::Duration::from_secs(7) && took < std::time::Duration::from_secs(30),
"took {took:?}: the extended budget is 8 s\n{}",
run.output()
);
assert!(
run.says("CPU could not be measured") && run.says("amont.idleLoadScale"),
"must say why the budget is not the configured one:\n{}",
run.output()
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn slow_clippy_repo() -> Repo {
let r = Repo::new();
r.stage(
"Cargo.toml",
"[package]\nname = \"x\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
);
r.stage("src/lib.rs", "pub fn x() {}\n");
let dir = r.path(".git/toolshims");
std::fs::create_dir_all(&dir).expect("mkdir");
common::fixture_exec(
&dir.join("cargo"),
"#!/bin/sh\ncase \"$1\" in\n --version) echo 'cargo 1.0.0 (fake)';;\n \
clippy) [ \"$2\" = --version ] && { echo clippy; exit 0; }; sleep 5;;\nesac\nexit 0\n",
);
r
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
#[ignore = "real-clock load scenario: CI runs it (`make test-load`), not every commit (ADR-0009)"]
fn heavy_checks_queue_for_a_host_slot() {
let _alone = alone();
let slots = std::env::temp_dir().join(format!("amont-slots-fixture-{}", std::process::id()));
let run_pair = |slots: &std::path::Path| -> (std::time::Duration, Vec<String>) {
let repos = [slow_clippy_repo(), slow_clippy_repo()];
let started = std::time::Instant::now();
let outputs: Vec<String> = std::thread::scope(|s| {
let handles: Vec<_> = repos
.iter()
.map(|r| {
s.spawn(move || {
let path = std::ffi::OsString::from(format!(
"{}:{}",
r.path(".git/toolshims").display(),
std::env::var("PATH").unwrap_or_default()
));
let one = std::ffi::OsString::from("1");
let (run, _) = r.hook_watched_unpinned(
"pre-commit-clippy",
&[
("PATH", path.as_os_str()),
("AMONT_SLOT_DIR", slots.as_os_str()),
("AMONT_HOST_SLOTS", one.as_os_str()),
],
WATCHDOG,
);
assert!(run.passed(), "{}", run.output());
run.output()
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("join"))
.collect()
});
(started.elapsed(), outputs)
};
let _ = std::fs::remove_dir_all(&slots);
let (took, outputs) = run_pair(&slots);
assert!(
took >= std::time::Duration::from_secs(10),
"two 5 s clippys with one slot took {took:?}: they ran together"
);
assert_eq!(
outputs
.iter()
.filter(|o| o.contains("for a host slot"))
.count(),
1,
"exactly one of the two waited, and said so:\n{outputs:#?}"
);
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::remove_dir_all(&slots);
std::fs::create_dir_all(&slots).expect("mkdir");
std::fs::set_permissions(&slots, std::fs::Permissions::from_mode(0o755)).expect("chmod");
let (took, outputs) = run_pair(&slots);
assert!(
took < std::time::Duration::from_millis(8500),
"an unsafe slot directory must not queue: took {took:?}"
);
for out in outputs {
assert!(out.contains("host slots are off"), "{out}");
}
let _ = std::fs::remove_dir_all(&slots);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn waiting_clippy_repo(always: bool) -> Repo {
let r = Repo::new();
r.stage(
"Cargo.toml",
"[package]\nname = \"x\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
);
r.stage("src/lib.rs", "pub fn x() {}\n");
let dir = r.path(".git/toolshims");
std::fs::create_dir_all(&dir).expect("mkdir");
let seen = dir.join("seen");
let guard = if always {
String::new()
} else {
format!("[ -f '{0}' ] && exit 0; touch '{0}'; ", seen.display())
};
common::fixture_exec(
&dir.join("cargo"),
&format!(
"#!/bin/sh\ncase \"$1\" in\n --version) echo 'cargo 1.0.0 (fake)';;\n \
clippy) [ \"$2\" = --version ] && {{ echo clippy; exit 0; }}; {guard}\
echo 'waiting for lock on x' >&2; exec sleep 30;;\nesac\nexit 0\n"
),
);
r.git(&["config", "amont.idleTimeout", "2"]);
r.git(&["config", "amont.timeout", "12"]);
r
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn with_shims(r: &Repo) -> std::ffi::OsString {
std::ffi::OsString::from(format!(
"{}:{}",
r.path(".git/toolshims").display(),
std::env::var("PATH").unwrap_or_default()
))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
#[ignore = "real-clock load scenario: CI runs it (`make test-load`), not every commit (ADR-0009)"]
fn a_wait_like_kill_is_retried_once() {
let _alone = alone();
let r = waiting_clippy_repo(false);
let path = with_shims(&r);
let (run, took) = r.hook_watched("pre-commit", &[("PATH", path.as_os_str())], WATCHDOG);
assert!(run.passed(), "the retry passed:\n{}", run.output());
assert!(took < std::time::Duration::from_secs(8), "took {took:?}");
assert!(
run.says("clippy") && run.says("was killed while it looked like it was waiting"),
"names the check, not the program:\n{}",
run.output()
);
assert_eq!(
run.output().matches("retrying once").count(),
1,
"{}",
run.output()
);
assert!(
!run.says("printed nothing"),
"no failure text:\n{}",
run.output()
);
let r = waiting_clippy_repo(true);
let path = with_shims(&r);
let (run, took) = r.hook_watched("pre-commit", &[("PATH", path.as_os_str())], WATCHDOG);
assert!(!run.passed(), "{}", run.output());
assert!(
took < std::time::Duration::from_secs(12),
"one ceiling: took {took:?}"
);
assert_eq!(
run.output().matches("retrying once").count(),
1,
"{}",
run.output()
);
assert!(run.says("printed nothing"), "{}", run.output());
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn a_declared_check_is_never_retried() {
let _alone = alone();
let r = Repo::new();
script(
&r,
"look.sh",
"echo 'waiting for lock on x' >&2\nexec sleep 30\n",
);
manifest(&r, "pre-commit look * block ./look.sh\n");
r.git(&["config", "amont.idleTimeout", "2"]);
r.git(&["config", "amont.timeout", "12"]);
let (run, _) = r.hook_watched("pre-commit", &[], WATCHDOG);
assert!(!run.passed(), "{}", run.output());
assert!(!run.says("retrying once"), "{}", run.output());
}