#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
pub struct Repo {
pub dir: PathBuf,
}
fn unique() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
format!(
"amont-test-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
)
}
impl Repo {
pub fn new() -> Self {
let dir = std::env::temp_dir().join(unique());
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp repo");
let r = Repo { dir };
r.git(&["init", "-q", "--template=", "--initial-branch=main", "."]);
r.git(&["config", "user.email", "test@example.com"]);
r.git(&["config", "user.name", "test"]);
r.git(&["config", "commit.gpgsign", "false"]);
r.git(&["config", "core.autocrlf", "false"]);
r.git(&["config", "amont.quiet", "never"]);
r
}
pub fn git(&self, args: &[&str]) -> Output {
let mut cmd = Command::new("git");
cmd.args(args).current_dir(&self.dir).stdin(Stdio::null());
Self::strip_git_env_impl(&mut cmd);
cmd.output().expect("run git")
}
pub fn stage(&self, path: &str, content: &str) {
let full = self.dir.join(path);
if let Some(p) = full.parent() {
std::fs::create_dir_all(p).expect("create parent");
}
std::fs::write(&full, content).expect("write file");
self.git(&["add", "--", path]);
}
pub fn write(&self, path: &str, content: &str) {
let full = self.dir.join(path);
if let Some(p) = full.parent() {
std::fs::create_dir_all(p).expect("create parent");
}
std::fs::write(&full, content).expect("write file");
}
pub fn commit(&self, msg: &str) {
self.git(&["commit", "-q", "--no-verify", "-m", msg]);
}
pub fn strip_git_env_impl(cmd: &mut Command) {
for (k, _) in std::env::vars_os() {
if k.to_string_lossy().starts_with("GIT_") {
cmd.env_remove(&k);
}
}
}
pub fn hook(&self, name: &str, args: &[&str]) -> HookRun {
self.hook_at(&self.dir, name, args)
}
pub fn pin_host_env(cmd: &mut Command) {
cmd.env("AMONT_IDLE_LOAD_SCALE", "1");
cmd.env("AMONT_HOST_SLOT", "held");
}
pub fn run(&self, args: &[&str]) -> HookRun {
let mut cmd = Command::new(bin());
cmd.args(args).current_dir(&self.dir).stdin(Stdio::null());
Self::strip_git_env_impl(&mut cmd);
Self::pin_host_env(&mut cmd);
cmd.env("GIT_CONFIG_GLOBAL", self.dir.join("fake-gitconfig"));
let out = cmd.output().expect("run amont");
HookRun {
code: out.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
}
}
pub fn hook_at(&self, cwd: &Path, name: &str, args: &[&str]) -> HookRun {
let mut cmd = Command::new(bin());
cmd.arg("--hooks-dir")
.arg(self.dir.join(".git/hooks"))
.arg(name)
.args(args)
.current_dir(cwd)
.stdin(Stdio::null());
Self::strip_git_env_impl(&mut cmd);
Self::pin_host_env(&mut cmd);
let out = cmd.output().expect("run amont");
HookRun {
code: out.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
}
}
#[cfg(unix)]
pub fn hook_watched(
&self,
name: &str,
env: &[(&str, &std::ffi::OsStr)],
limit: std::time::Duration,
) -> (HookRun, std::time::Duration) {
self.hook_watched_in(&self.dir, name, env, limit, true)
}
#[cfg(unix)]
pub fn hook_watched_unpinned(
&self,
name: &str,
env: &[(&str, &std::ffi::OsStr)],
limit: std::time::Duration,
) -> (HookRun, std::time::Duration) {
self.hook_watched_in(&self.dir, name, env, limit, false)
}
#[cfg(unix)]
fn hook_watched_in(
&self,
cwd: &Path,
name: &str,
env: &[(&str, &std::ffi::OsStr)],
limit: std::time::Duration,
pinned: bool,
) -> (HookRun, std::time::Duration) {
use std::os::unix::process::CommandExt;
let tmp = std::env::temp_dir();
let out_path = tmp.join(format!("{}.out", unique()));
let err_path = tmp.join(format!("{}.err", unique()));
let mut cmd = Command::new(bin());
cmd.arg("--hooks-dir")
.arg(self.dir.join(".git/hooks"))
.arg(name)
.current_dir(cwd)
.stdin(Stdio::null())
.stdout(std::fs::File::create(&out_path).expect("stdout file"))
.stderr(std::fs::File::create(&err_path).expect("stderr file"))
.process_group(0);
Self::strip_git_env_impl(&mut cmd);
if pinned {
Self::pin_host_env(&mut cmd);
} else {
cmd.env_remove("AMONT_HOST_SLOT");
cmd.env_remove("AMONT_IDLE_LOAD_SCALE");
}
for (k, v) in env {
cmd.env(k, v);
}
let started = std::time::Instant::now();
let mut child = cmd.spawn().expect("spawn amont");
let group = child.id();
let kill_group = || {
let _ = Command::new("kill")
.args(["-KILL", "--", &format!("-{group}")])
.stderr(Stdio::null())
.status();
};
let status = loop {
if let Some(s) = child.try_wait().expect("wait amont") {
break Some(s);
}
if started.elapsed() >= limit {
break None;
}
std::thread::sleep(std::time::Duration::from_millis(50));
};
let took = started.elapsed();
kill_group();
let read = |p: &Path| std::fs::read_to_string(p).unwrap_or_default();
let run = HookRun {
code: status.and_then(|s| s.code()).unwrap_or(-1),
stdout: read(&out_path),
stderr: read(&err_path),
};
let _ = child.wait();
let _ = std::fs::remove_file(&out_path);
let _ = std::fs::remove_file(&err_path);
assert!(
status.is_some(),
"the watchdog fired: amont had not returned after {limit:?}:\n{}",
run.output()
);
(run, took)
}
pub fn worktree(&self, name: &str) -> PathBuf {
let path = self.dir.join(name);
self.git(&[
"worktree",
"add",
"-q",
path.to_str().expect("utf8 path"),
"-b",
name,
]);
path
}
pub fn path(&self, rel: &str) -> PathBuf {
self.dir.join(rel)
}
}
impl Drop for Repo {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
pub struct HookRun {
pub code: i32,
pub stdout: String,
pub stderr: String,
}
impl HookRun {
pub fn passed(&self) -> bool {
self.code == 0
}
pub fn output(&self) -> String {
format!("{}{}", self.stdout, self.stderr)
}
pub fn says(&self, needle: &str) -> bool {
self.output().contains(needle)
}
pub fn silent(&self) -> bool {
self.output().trim().is_empty()
}
}
pub const PRIME_ENV: &str = "AMONT_FIXTURE_PRIME";
#[cfg(unix)]
pub fn fixture_exec(p: &Path, body: &str) {
let (shebang, rest) = body
.split_once('\n')
.expect("a fixture script starts with its shebang line");
assert!(shebang.starts_with("#!"), "no shebang in {}", p.display());
std::fs::write(
p,
format!("{shebang}\n[ -n \"${PRIME_ENV}\" ] && exit 0\n{rest}"),
)
.expect("write the fixture executable");
make_executable(p);
prime(p);
}
#[cfg(unix)]
pub fn prime(p: &Path) {
const ETXTBSY: i32 = 26; let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
let ran = Command::new(p)
.env(PRIME_ENV, "1")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match ran {
Ok(s) => {
assert!(s.success(), "priming {} exited {s}", p.display());
return;
}
Err(e) if e.raw_os_error() == Some(ETXTBSY) && std::time::Instant::now() < deadline => {
std::thread::sleep(std::time::Duration::from_millis(20));
}
Err(e) => panic!("priming {} failed: {e}", p.display()),
}
}
}
#[cfg(unix)]
fn make_executable(p: &Path) {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(p, std::fs::Permissions::from_mode(0o755));
}
#[cfg(not(unix))]
fn make_executable(_p: &Path) {}
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_amont")
}
pub fn missing(tool: &str) -> bool {
let found = std::env::var_os("PATH")
.map(|path| {
std::env::split_paths(&path).any(|d| {
d.join(tool).is_file()
|| (cfg!(windows)
&& [".exe", ".cmd", ".bat"]
.iter()
.any(|e| d.join(format!("{tool}{e}")).is_file()))
})
})
.unwrap_or(false);
if !found {
println!(" ! {tool} unavailable — skipping");
}
!found
}
pub fn template_hook(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../templates/hooks")
.join(name)
}
pub fn output_retrying_etxtbsy(cmd: &mut std::process::Command) -> std::io::Result<Output> {
let mut delay = std::time::Duration::from_millis(10);
for tries_left in [4u8, 3, 2, 1, 0] {
match cmd.output() {
Err(e) if tries_left > 0 && e.raw_os_error() == Some(26) => {
std::thread::sleep(delay);
delay *= 2;
}
other => return other,
}
}
unreachable!("the zero-tries arm returns")
}