use crate::git;
use crate::ui::{error_sign, valid_sign, warning_sign};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::OnceLock;
static OVERRIDE: OnceLock<Vec<String>> = OnceLock::new();
static NOT_THE_INDEX: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn override_file_set(files: Vec<String>) {
NOT_THE_INDEX.store(true, std::sync::atomic::Ordering::SeqCst);
let _ = OVERRIDE.set(files);
}
pub fn not_the_index() -> bool {
NOT_THE_INDEX.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn staged_files(exts: &[&str]) -> Vec<String> {
if let Some(all) = OVERRIDE.get() {
return all
.iter()
.filter(|f| exts.is_empty() || exts.iter().any(|e| f.ends_with(e)))
.cloned()
.collect();
}
static INDEX: OnceLock<Vec<String>> = OnceLock::new();
INDEX
.get_or_init(|| {
match git::stdout_paths(&["diff", "--diff-filter=d", "--cached", "--name-only"]) {
Some(files) => files,
None => {
warn(
"git would not list the staged files — the checks are judging \
an EMPTY set, not a verified one",
);
Vec::new()
}
}
})
.iter()
.filter(|f| exts.is_empty() || exts.iter().any(|e| f.ends_with(e)))
.cloned()
.collect()
}
pub fn tracked_files() -> Option<Vec<String>> {
static TRACKED: OnceLock<Option<Vec<String>>> = OnceLock::new();
TRACKED
.get_or_init(|| match git::stdout_paths(&["ls-files"]) {
Some(files) => Some(files),
None => {
warn(
"git would not list the repository's files — opt-in gated checks \
will run rather than be skipped on an unverified answer",
);
None
}
})
.clone()
}
pub fn repo_root() -> String {
static ROOT: OnceLock<String> = OnceLock::new();
ROOT.get_or_init(|| {
git::stdout(&["rev-parse", "--show-toplevel"]).unwrap_or_else(|| ".".into())
})
.clone()
}
pub fn repo_root_checked() -> Result<String, String> {
git::stdout(&["rev-parse", "--show-toplevel"])
.filter(|s| !s.is_empty())
.ok_or_else(|| "not inside a git repository".to_string())
}
pub fn resolve_tool(root: &str, tool: &str) -> Option<Vec<String>> {
if let Some(p) = in_bin_dir(&format!("{root}/node_modules/.bin"), tool) {
return Some(vec![p]);
}
if let Some(common) = git::stdout(&["rev-parse", "--path-format=absolute", "--git-common-dir"])
{
if let Some(main) = Path::new(&common).parent() {
if let Some(p) = in_bin_dir(&main.join("node_modules/.bin").to_string_lossy(), tool) {
return Some(vec![p]);
}
}
}
if let Some(full) = which(tool) {
return Some(vec![full]);
}
if which("npx").is_some()
&& Command::new(program("npx"))
.args(["--no-install", tool, "--version"])
.current_dir(root)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return Some(vec![
program("npx"),
"--no-install".to_string(),
tool.to_string(),
]);
}
None
}
pub fn which(tool: &str) -> Option<String> {
which_on(&std::env::var_os("PATH")?, tool)
}
pub fn which_on(path: &std::ffi::OsStr, tool: &str) -> Option<String> {
let exts: Vec<String> = if cfg!(windows) {
std::env::var("PATHEXT")
.unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".into())
.split(';')
.filter(|e| !e.is_empty())
.map(|e| e.to_lowercase())
.collect()
} else {
Vec::new()
};
for dir in std::env::split_paths(path) {
for e in &exts {
let c = dir.join(format!("{tool}{e}"));
if c.is_file() {
return Some(c.to_string_lossy().into_owned());
}
}
let bare = dir.join(tool);
if bare.is_file() {
return Some(bare.to_string_lossy().into_owned());
}
}
None
}
fn in_bin_dir(dir: &str, tool: &str) -> Option<String> {
let bare = Path::new(dir).join(tool);
if bare.is_file() {
return Some(bare.to_string_lossy().into_owned());
}
if cfg!(windows) {
for e in [".cmd", ".exe", ".bat", ".ps1"] {
let c = Path::new(dir).join(format!("{tool}{e}"));
if c.is_file() {
return Some(c.to_string_lossy().into_owned());
}
}
}
None
}
pub fn program(name: &str) -> String {
which(name).unwrap_or_else(|| name.to_string())
}
pub fn first_existing(root: &str, names: &[&str]) -> Option<String> {
names
.iter()
.find(|n| Path::new(root).join(n).exists())
.map(|n| (*n).to_string())
}
pub fn strip_git_env(cmd: &mut Command) {
for (k, _) in std::env::vars_os() {
let key = k.to_string_lossy();
if key.starts_with("GIT_") {
cmd.env_remove(&k);
}
}
}
pub fn check_timeout(settings: &crate::config::Settings) -> u64 {
*settings.timeout.get_or_init(|| {
crate::config::integer_or(settings, "amont.timeout", 3600, 0..=86_400) as u64
})
}
pub fn idle_timeout(settings: &crate::config::Settings) -> u64 {
*settings.idle.get_or_init(|| {
crate::config::integer_or(settings, "amont.idleTimeout", 120, 0..=86_400) as u64
})
}
pub fn idle_cpu_credit(settings: &crate::config::Settings) -> bool {
*settings
.idle_cpu
.get_or_init(|| crate::config::boolean_or(settings, "amont.idleCpuCredit", true))
}
pub fn human_secs(secs: u64) -> String {
match secs {
s if s < 60 => format!("{s}s"),
s if s < 3600 => format!("{}m{:02}s", s / 60, s % 60),
s => format!("{}h{:02}m", s / 3600, (s % 3600) / 60),
}
}
pub const BUSY_MILLI_CORES: u32 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CpuState {
Off = 0,
Waiting = 1,
Measuring = 2,
Unavailable = 3,
}
pub struct Activity {
base: std::time::Instant,
last_out: std::sync::atomic::AtomicU64,
last_busy: std::sync::atomic::AtomicU64,
measured_since: std::sync::atomic::AtomicU64,
last_measured: std::sync::atomic::AtomicU64,
rate_milli: std::sync::atomic::AtomicU32,
interval_ms: std::sync::atomic::AtomicU32,
cpu: std::sync::atomic::AtomicU8,
}
impl Activity {
pub fn new() -> std::sync::Arc<Activity> {
std::sync::Arc::new(Activity {
base: std::time::Instant::now(),
last_out: Default::default(),
last_busy: Default::default(),
measured_since: Default::default(),
last_measured: Default::default(),
rate_milli: Default::default(),
interval_ms: Default::default(),
cpu: std::sync::atomic::AtomicU8::new(CpuState::Off as u8),
})
}
fn offset(&self, at: std::time::Instant) -> u64 {
u64::try_from(at.saturating_duration_since(self.base).as_nanos()).unwrap_or(u64::MAX)
}
fn now(&self) -> u64 {
self.offset(std::time::Instant::now())
}
fn since(&self, offset: u64) -> std::time::Duration {
std::time::Duration::from_nanos(self.now().saturating_sub(offset))
}
pub fn touch(&self) {
self.last_out
.fetch_max(self.now(), std::sync::atomic::Ordering::Relaxed);
}
pub fn quiet_for(&self) -> std::time::Duration {
self.since(self.last_out.load(std::sync::atomic::Ordering::Relaxed))
}
pub fn still_for(&self) -> std::time::Duration {
let busy = self.last_busy.load(std::sync::atomic::Ordering::Relaxed);
self.quiet_for().min(self.since(busy))
}
pub fn cpu_state(&self) -> CpuState {
match self.cpu.load(std::sync::atomic::Ordering::Relaxed) {
1 => CpuState::Waiting,
2 => CpuState::Measuring,
3 => CpuState::Unavailable,
_ => CpuState::Off,
}
}
fn set_cpu_state(&self, s: CpuState) {
self.cpu
.store(s as u8, std::sync::atomic::Ordering::Relaxed);
}
pub fn fresh_rate(&self) -> Option<u32> {
if self.cpu_state() != CpuState::Measuring {
return None;
}
let end = self
.last_measured
.load(std::sync::atomic::Ordering::Relaxed);
if end == 0 {
return None;
}
let every = u64::from(self.interval_ms.load(std::sync::atomic::Ordering::Relaxed));
let stale = std::time::Duration::from_millis(2 * every.max(1));
(self.since(end - 1) <= stale)
.then(|| self.rate_milli.load(std::sync::atomic::Ordering::Relaxed))
}
pub fn verdict(&self) -> CpuVerdict {
match self.cpu_state() {
CpuState::Off => return CpuVerdict::NotSampled,
CpuState::Waiting | CpuState::Unavailable => return CpuVerdict::Unmeasured,
CpuState::Measuring => {}
}
if let Some(rate) = self.fresh_rate().filter(|r| *r >= BUSY_MILLI_CORES) {
return CpuVerdict::BusyAtKill(rate);
}
let since = self
.measured_since
.load(std::sync::atomic::Ordering::Relaxed);
match (since, self.fresh_rate()) {
(s, Some(_)) if s != 0 => CpuVerdict::MeasuredIdle(self.since(s - 1).as_secs()),
_ => CpuVerdict::Unmeasured,
}
}
pub fn record(
&self,
obs: crate::proctree::Observation,
at: std::time::Instant,
interval: std::time::Duration,
) {
use std::sync::atomic::Ordering::Relaxed;
self.interval_ms.store(
u32::try_from(interval.as_millis()).unwrap_or(u32::MAX),
Relaxed,
);
match obs {
crate::proctree::Observation::Baseline => {
self.measured_since.store(self.offset(at) + 1, Relaxed);
self.set_cpu_state(CpuState::Waiting);
}
crate::proctree::Observation::Window(w) => {
let milli = w.milli_cores();
self.rate_milli.store(milli, Relaxed);
self.last_measured.store(self.offset(w.end) + 1, Relaxed);
if milli >= BUSY_MILLI_CORES {
self.last_busy.fetch_max(self.offset(w.start), Relaxed);
}
self.set_cpu_state(CpuState::Measuring);
}
crate::proctree::Observation::Unmeasured => {
self.measured_since.store(0, Relaxed);
self.set_cpu_state(CpuState::Unavailable);
}
}
}
pub fn enable_cpu(&self) {
self.set_cpu_state(CpuState::Waiting);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CpuVerdict {
NotSampled,
MeasuredIdle(u64),
Unmeasured,
BusyAtKill(u32),
}
pub fn cores(milli: u32) -> String {
format!("~{}.{} cores", milli / 1000, (milli % 1000) / 100)
}
pub fn network_probe_budget(settings: &crate::config::Settings) -> u64 {
match check_timeout(settings) {
0 => 0,
t => t.min(30),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Why {
Ceiling(u64),
Silence(u64),
}
#[derive(Debug, Clone, Copy)]
pub struct Killed {
pub why: Why,
pub ran_secs: u64,
pub quiet_secs: Option<u64>,
pub cpu: CpuVerdict,
pub idle_secs: u64,
}
pub enum Ran {
Status(std::process::ExitStatus),
TimedOut(Killed),
}
pub fn status_within(
settings: &crate::config::Settings,
cmd: &mut Command,
) -> std::io::Result<Ran> {
status_within_secs(cmd, check_timeout(settings))
}
pub fn status_within_secs(cmd: &mut Command, budget_secs: u64) -> std::io::Result<Ran> {
if budget_secs == 0 {
return cmd.status().map(Ran::Status);
}
let mut child = cmd.spawn()?;
wait_within(&mut child, budget_secs, 0, None)
}
fn run_observed(
settings: &crate::config::Settings,
cmd: &mut Command,
on_output: impl Fn(&[u8]) + Send + Sync + 'static,
) -> std::io::Result<Ran> {
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let activity = Activity::new();
let on_output = std::sync::Arc::new(on_output);
let mut child = cmd.spawn()?;
activity.touch();
let mut readers = Vec::new();
for pipe in [
child
.stdout
.take()
.map(|p| Box::new(p) as Box<dyn std::io::Read + Send>),
child
.stderr
.take()
.map(|p| Box::new(p) as Box<dyn std::io::Read + Send>),
]
.into_iter()
.flatten()
{
let activity = std::sync::Arc::clone(&activity);
let on_output = std::sync::Arc::clone(&on_output);
readers.push(std::thread::spawn(move || {
let mut pipe = pipe;
let mut chunk = [0u8; 4096];
loop {
match std::io::Read::read(&mut pipe, &mut chunk) {
Ok(0) | Err(_) => break,
Ok(n) => {
activity.touch();
on_output(&chunk[..n]);
}
}
}
}));
}
let _attached = crate::live::current_sink()
.map(|(stage, idx)| stage.attach(idx, std::sync::Arc::clone(&activity)));
let idle = idle_timeout(settings);
let sampler = (idle > 0 && idle_cpu_credit(settings) && crate::proctree::SUPPORTED)
.then(|| CpuSampler::start(child.id(), std::sync::Arc::clone(&activity), idle));
let ran = wait_within(&mut child, check_timeout(settings), idle, Some(&activity));
if let Some(s) = sampler {
s.stop();
}
for r in readers {
let _ = r.join();
}
ran
}
struct CpuSampler {
stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
handle: std::thread::JoinHandle<()>,
}
impl CpuSampler {
fn start(pid: u32, activity: std::sync::Arc<Activity>, idle_secs: u64) -> CpuSampler {
activity.enable_cpu();
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let flag = std::sync::Arc::clone(&stop);
let handle = std::thread::Builder::new()
.name("amont-cpu".into())
.spawn(move || sample_loop(pid, &activity, idle_secs, &flag))
.expect("spawn the CPU sampler thread");
CpuSampler { stop, handle }
}
fn stop(self) {
self.stop.store(true, std::sync::atomic::Ordering::Relaxed);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
while !self.handle.is_finished() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
if self.handle.is_finished() {
let _ = self.handle.join();
}
}
}
pub fn sampling_schedule(idle_secs: u64) -> (std::time::Duration, std::time::Duration) {
let ms = std::time::Duration::from_millis;
let budget = std::time::Duration::from_secs(idle_secs);
let first = (budget / 3)
.min(std::time::Duration::from_secs(30))
.max(ms(250));
let every = (budget / 4).clamp(ms(250), std::time::Duration::from_secs(10));
(first, every)
}
fn sample_loop(
pid: u32,
activity: &Activity,
idle_secs: u64,
stop: &std::sync::atomic::AtomicBool,
) {
use std::sync::atomic::Ordering::Relaxed;
let (first, every) = sampling_schedule(idle_secs);
let limits = crate::proctree::Limits::default();
let trace = std::env::var_os("AMONT_CPU_TRACE");
let mut tracker = crate::proctree::Tracker::default();
let nap = |d: std::time::Duration| {
let until = std::time::Instant::now() + d;
while !stop.load(Relaxed) && std::time::Instant::now() < until {
std::thread::sleep(std::time::Duration::from_millis(50));
}
};
while !stop.load(Relaxed) {
if activity.quiet_for() < first {
tracker = crate::proctree::Tracker::default();
nap(std::time::Duration::from_millis(100));
continue;
}
let snap = crate::proctree::snapshot(pid, &tracker.seen(), &limits);
let at = std::time::Instant::now();
let traced = trace.as_ref().map(|_| match &snap {
crate::proctree::Snapshot::Complete(procs) => procs.clone(),
_ => Vec::new(),
});
let obs = tracker.observe(at, snap);
if let (Some(path), Some(procs)) = (&trace, traced) {
trace_sample(path, pid, &procs, &obs);
}
activity.record(obs, at, every);
nap(every);
}
}
fn trace_sample(
path: &std::ffi::OsStr,
root: u32,
procs: &[crate::proctree::Proc],
obs: &crate::proctree::Observation,
) {
use std::io::Write;
let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
else {
return;
};
let mut text = String::new();
for p in procs {
text.push_str(&format!(
"{} {} {} {}\n",
p.id.pid, p.id.start, p.ppid, p.cpu_ns
));
}
let obs = match obs {
crate::proctree::Observation::Window(w) => {
format!("window {} milli-cores", w.milli_cores())
}
other => format!("{other:?}").to_lowercase(),
};
text.push_str(&format!("# root {root} {obs}\n"));
let _ = f.write_all(text.as_bytes());
}
pub fn status_streamed(
settings: &crate::config::Settings,
cmd: &mut Command,
) -> std::io::Result<Ran> {
let Some((stage, idx)) = crate::live::current_sink() else {
return status_within(settings, cmd);
};
if crate::live::watching() {
cmd.env("FORCE_COLOR", "1")
.env("CLICOLOR_FORCE", "1")
.env("CARGO_TERM_COLOR", "always");
}
run_observed(settings, cmd, move |bytes| stage.append_raw(idx, bytes))
}
pub fn capture_within(
settings: &crate::config::Settings,
cmd: &mut Command,
) -> Option<(Ran, String)> {
let text = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let sink = std::sync::Arc::clone(&text);
let ran = run_observed(settings, cmd, move |bytes| {
sink.lock()
.unwrap_or_else(|p| p.into_inner())
.push_str(&String::from_utf8_lossy(bytes));
})
.ok()?;
let text = std::sync::Arc::try_unwrap(text)
.map(|m| m.into_inner().unwrap_or_else(|p| p.into_inner()))
.unwrap_or_default();
Some((ran, text))
}
pub(crate) fn wait_within(
child: &mut std::process::Child,
wall_secs: u64,
idle_secs: u64,
activity: Option<&Activity>,
) -> std::io::Result<Ran> {
let started = std::time::Instant::now();
let ceiling = (wall_secs > 0).then(|| started + std::time::Duration::from_secs(wall_secs));
let silence = match activity {
Some(_) if idle_secs > 0 => Some(std::time::Duration::from_secs(idle_secs)),
_ => None,
};
if ceiling.is_none() && silence.is_none() {
return child.wait().map(Ran::Status);
}
loop {
if let Some(status) = child.try_wait()? {
return Ok(Ran::Status(status));
}
let now = std::time::Instant::now();
let quiet = activity.map(|a| a.still_for());
let why = judge(
now.duration_since(started),
quiet,
ceiling.map(|_| wall_secs),
silence.map(|_| idle_secs),
);
if let Some(why) = why {
let _ = child.kill();
let _ = child.wait();
return Ok(Ran::TimedOut(Killed {
why,
ran_secs: now.duration_since(started).as_secs(),
quiet_secs: activity.map(|a| a.quiet_for().as_secs()),
cpu: activity.map_or(CpuVerdict::NotSampled, Activity::verdict),
idle_secs,
}));
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
}
pub fn judge(
ran: std::time::Duration,
quiet: Option<std::time::Duration>,
ceiling: Option<u64>,
silence: Option<u64>,
) -> Option<Why> {
if let Some(wall) = ceiling {
if ran >= std::time::Duration::from_secs(wall) {
return Some(Why::Ceiling(wall));
}
}
if let (Some(idle), Some(q)) = (silence, quiet) {
if q >= std::time::Duration::from_secs(idle) {
return Some(Why::Silence(idle));
}
}
None
}
pub fn say_timed_out(what: &str, k: Killed) {
match k.why {
Why::Silence(budget) => {
fail(&match k.cpu {
CpuVerdict::MeasuredIdle(covered) => format!(
"{} printed nothing for {} and did no measurable CPU work (< 0.1 core) in \
the last {} of it; killed after {} — a tool this idle is stuck, not slow. \
{} raises the silence budget (0 disables)",
hl(what),
human_secs(budget),
human_secs(covered.max(1)),
human_secs(k.ran_secs),
hl("git config amont.idleTimeout <secs>")
),
_ => {
format!(
"{} printed nothing for {}{} and was killed after {} — a tool this quiet is \
usually stuck, not slow. {} raises the silence budget (0 disables)",
hl(what),
human_secs(budget),
if k.cpu == CpuVerdict::Unmeasured { " (CPU not measured)" } else { "" },
human_secs(k.ran_secs),
hl("git config amont.idleTimeout <secs>")
)
}
})
}
Why::Ceiling(budget) => {
let verdict = match (k.quiet_secs, k.cpu) {
(Some(q), CpuVerdict::BusyAtKill(m)) if k.idle_secs > 0 && q >= k.idle_secs => {
format!(
" It printed nothing for the last {} but kept its CPU busy ({}), so the \
silence budget did not stop it: a busy loop, or a tool that prints only \
at the end (give it a per-file reporter). {} kills quiet runs at the \
silence budget whatever their CPU.",
human_secs(q),
cores(m),
hl("git config amont.idleCpuCredit false")
)
}
(Some(q), _) if q < 30 => format!(
" It was still printing ({} since its last line): slow, not stuck.",
human_secs(q)
),
(Some(q), _) => format!(" Its last output was {} ago.", human_secs(q)),
(None, _) => String::new(),
};
fail(&format!(
"{} timed out: ran for {} and was killed at the ceiling. {} raises it \
(0 disables).{verdict}",
hl(what),
human_secs(budget),
hl("git config amont.timeout <secs>")
))
}
}
}
pub fn bounded_success(settings: &crate::config::Settings, cmd: &mut Command, what: &str) -> bool {
match status_streamed(settings, cmd) {
Ok(Ran::Status(s)) => s.success(),
Ok(Ran::TimedOut(b)) => {
say_timed_out(what, b);
false
}
Err(_) => false,
}
}
pub fn run(
settings: &crate::config::Settings,
root: &str,
argv: &[String],
extra: &[String],
) -> bool {
let Some((program, rest)) = argv.split_first() else {
return true;
};
let mut cmd = Command::new(program);
cmd.args(rest)
.args(extra)
.current_dir(root)
.stdin(Stdio::null());
strip_git_env(&mut cmd);
bounded_success(settings, &mut cmd, program)
}
pub fn run_quiet(
settings: &crate::config::Settings,
root: &str,
argv: &[String],
extra: &[String],
) -> bool {
let Some((program, rest)) = argv.split_first() else {
return true;
};
let mut cmd = Command::new(program);
cmd.args(rest)
.args(extra)
.current_dir(root)
.stdin(Stdio::null());
strip_git_env(&mut cmd);
match run_observed(settings, &mut cmd, |_| {}) {
Ok(Ran::Status(s)) => s.success(),
Ok(Ran::TimedOut(b)) => {
say_timed_out(program, b);
false
}
Err(_) => false,
}
}
pub fn fixing_enabled(settings: &crate::config::Settings) -> bool {
!not_the_index() && fixing_requested(settings)
}
pub fn fixing_requested(settings: &crate::config::Settings) -> bool {
*settings
.fixing
.get_or_init(|| crate::config::boolean_or(settings, "amont.fix", false))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Restaged {
Nothing,
Staged,
Failed(Vec<String>),
}
static INDEX_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub fn restage(paths: &[String]) -> Restaged {
if not_the_index() {
return Restaged::Nothing;
}
let changed: Vec<String> = paths
.iter()
.filter(|p| !git::succeeds(&["diff", "--quiet", "--", p]))
.cloned()
.collect();
if changed.is_empty() {
return Restaged::Nothing;
}
let mut args = vec!["add", "--"];
args.extend(changed.iter().map(String::as_str));
let _serialised = INDEX_LOCK.lock().unwrap_or_else(|e| e.into_inner());
const BACKOFF_MS: [u64; 3] = [50, 150, 400];
if git::succeeds(&args) {
return Restaged::Staged;
}
for wait in BACKOFF_MS {
std::thread::sleep(std::time::Duration::from_millis(wait));
if git::succeeds(&args) {
return Restaged::Staged;
}
}
Restaged::Failed(changed)
}
pub fn ok(settings: &crate::config::Settings, msg: &str) {
if crate::live::quiet(settings) {
return;
}
crate::live::say(&format!("{} {msg}", valid_sign()));
}
pub fn fail(msg: &str) {
crate::live::say(&format!("{} {msg}", error_sign()));
}
pub fn warn(msg: &str) {
crate::live::say(&format!("{} {msg}", warning_sign()));
}
pub fn say(msg: &str) {
crate::live::say(msg);
}
pub fn hl(s: &str) -> String {
crate::ui::highlight(s)
}
#[cfg(test)]
mod tests {
fn window(
start: std::time::Instant,
end: std::time::Instant,
milli: u64,
) -> crate::proctree::Observation {
let wall = u64::try_from(end.duration_since(start).as_nanos())
.unwrap_or(u64::MAX)
.max(1);
let gain = wall * milli / 1000;
crate::proctree::Observation::Window(crate::proctree::Window {
start,
end,
gain_ns: gain,
})
}
#[test]
fn the_sampling_schedule_follows_the_budget_within_floors() {
use super::sampling_schedule;
let ms = std::time::Duration::from_millis;
let third = |secs: u64| std::time::Duration::from_secs(secs) / 3;
assert_eq!(sampling_schedule(1), (third(1), ms(250)));
assert_eq!(sampling_schedule(2), (third(2), ms(500)));
assert_eq!(sampling_schedule(120), (ms(30_000), ms(10_000)));
assert_eq!(sampling_schedule(0), (ms(250), ms(250)));
}
#[test]
fn busy_work_resets_the_still_time_but_not_the_output_silence() {
use super::Activity;
let a = Activity::new();
a.touch();
std::thread::sleep(std::time::Duration::from_millis(60));
let quiet = a.quiet_for();
assert!(a.still_for() >= quiet.saturating_sub(std::time::Duration::from_millis(5)));
a.enable_cpu();
let now = std::time::Instant::now();
a.record(
window(now - std::time::Duration::from_millis(10), now, 2000),
now,
std::time::Duration::from_secs(1),
);
assert!(a.still_for() < std::time::Duration::from_millis(40));
assert!(a.quiet_for() >= std::time::Duration::from_millis(60));
}
#[test]
fn the_cpu_verdict_claims_only_what_was_measured() {
use super::{Activity, CpuVerdict};
use crate::proctree::Observation;
let s = std::time::Duration::from_secs;
let every = s(10);
let off = Activity::new();
assert_eq!(off.verdict(), CpuVerdict::NotSampled);
let waiting = Activity::new();
waiting.enable_cpu();
assert_eq!(waiting.verdict(), CpuVerdict::Unmeasured);
let now = std::time::Instant::now();
let before = now - s(1);
let idle = Activity::new();
idle.enable_cpu();
let t0 = std::time::Instant::now();
idle.record(Observation::Baseline, t0, every);
std::thread::sleep(std::time::Duration::from_millis(1050));
let t1 = std::time::Instant::now();
idle.record(window(t0, t1, 20), t1, every);
assert_eq!(idle.verdict(), CpuVerdict::MeasuredIdle(1));
let busy = Activity::new();
busy.enable_cpu();
busy.record(Observation::Baseline, before, every);
busy.record(window(before, now, 3900), now, every);
assert_eq!(busy.verdict(), CpuVerdict::BusyAtKill(3900));
let broken = Activity::new();
broken.enable_cpu();
broken.record(Observation::Baseline, before, every);
broken.record(window(before, now, 20), now, every);
broken.record(Observation::Unmeasured, now, every);
assert_eq!(broken.verdict(), CpuVerdict::Unmeasured);
}
#[test]
fn a_stale_rate_expires() {
use super::{Activity, CpuVerdict};
use crate::proctree::Observation;
let a = Activity::new();
a.enable_cpu();
let then = std::time::Instant::now();
let tick = std::time::Duration::from_millis(5);
let earlier = then - std::time::Duration::from_secs(1);
a.record(Observation::Baseline, earlier, tick);
a.record(window(earlier, then, 3000), then, tick);
std::thread::sleep(std::time::Duration::from_millis(40));
assert_eq!(a.fresh_rate(), None);
assert_eq!(a.verdict(), CpuVerdict::Unmeasured);
}
#[test]
fn cores_read_as_one_decimal() {
assert_eq!(super::cores(3900), "~3.9 cores");
assert_eq!(super::cores(420), "~0.4 cores");
assert_eq!(super::cores(100), "~0.1 cores");
}
#[test]
fn the_clocks_judge_silence_and_ceiling_separately() {
use super::{judge, Why};
use std::time::Duration as D;
let s = D::from_secs;
assert_eq!(judge(s(900), Some(s(0)), Some(3600), Some(5)), None);
assert_eq!(judge(s(900), Some(s(4)), Some(3600), Some(5)), None);
assert_eq!(
judge(s(30), Some(s(5)), Some(3600), Some(5)),
Some(Why::Silence(5))
);
assert_eq!(judge(s(900), None, Some(3600), Some(5)), None);
assert_eq!(
judge(s(3600), Some(s(0)), Some(3600), Some(120)),
Some(Why::Ceiling(3600))
);
assert_eq!(
judge(s(3600), Some(s(600)), Some(3600), Some(120)),
Some(Why::Ceiling(3600))
);
assert_eq!(judge(s(86_400), Some(s(86_400)), None, None), None);
assert_eq!(judge(s(86_400), Some(s(1)), None, Some(120)), None);
}
#[cfg(unix)]
#[test]
fn the_deadline_kills_a_sleeper_and_spares_a_finisher() {
let started = std::time::Instant::now();
let mut slow = Command::new(program("sleep"));
slow.arg("300").stdin(Stdio::null());
match status_within_secs(&mut slow, 1) {
Ok(Ran::TimedOut(Killed {
why: Why::Ceiling(1),
quiet_secs: None,
..
})) => {}
other => panic!("expected TimedOut(1), got {:?}", other.map(|_| "ran")),
}
assert!(
started.elapsed() < std::time::Duration::from_secs(60),
"the kill did not happen at the deadline"
);
let mut quick = Command::new(program("true"));
quick.stdin(Stdio::null());
match status_within_secs(&mut quick, 60) {
Ok(Ran::Status(s)) => assert!(s.success()),
other => panic!("expected a clean exit, got {:?}", other.map(|_| "?")),
}
}
use super::*;
#[test]
fn which_finds_a_real_binary_and_not_a_fake_one() {
assert!(which("git").is_some());
assert!(which("definitely-not-a-real-binary-xyz").is_none());
}
#[test]
#[cfg(windows)]
fn windows_prefers_an_executable_extension_over_a_bare_file() {
let dir = std::env::temp_dir().join("amont-which-order");
let _ = std::fs::create_dir_all(&dir);
std::fs::write(dir.join("faketool"), "#!/bin/sh\n").unwrap();
std::fs::write(dir.join("faketool.cmd"), "@echo off\n").unwrap();
let found = which_on(dir.as_os_str(), "faketool").unwrap();
assert!(found.ends_with(".cmd"), "got {found}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn restage_distinguishes_nothing_from_failure() {
let _cwd = crate::TEST_CWD.lock().unwrap_or_else(|p| p.into_inner());
let outside = std::env::temp_dir()
.join("amont-restage-outside-any-repo")
.to_string_lossy()
.into_owned();
assert_eq!(
restage(std::slice::from_ref(&outside)),
Restaged::Failed(vec![outside]),
"a `git add` git refuses must report Failed, never Nothing"
);
assert_eq!(
restage(&[]),
Restaged::Nothing,
"no paths is nothing to do, and nothing wrong"
);
}
#[test]
fn no_hook_spawns_a_bare_program_name() {
let needle = concat!("Command", "::new(");
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/hooks");
let mut scanned = 0usize;
for entry in std::fs::read_dir(dir).expect("hooks dir").flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
scanned += 1;
let src = std::fs::read_to_string(&path).expect("read a hook module");
for (n, line) in src.lines().enumerate() {
if line.trim_start().starts_with("//") {
continue;
}
let Some(after) = line.split_once(needle) else {
continue;
};
assert!(
!after.1.starts_with('"'),
"{}:{} spawns a bare name — route it through `program()` or \
the path `which()` already resolved: {}",
path.display(),
n + 1,
line.trim()
);
}
}
assert!(
scanned > 10,
"the scan found almost nothing: {scanned} files"
);
}
#[test]
fn first_existing_picks_the_earliest_present_name() {
let dir = std::env::temp_dir().join("amont-first-existing-test");
let _ = std::fs::create_dir_all(&dir);
let root = dir.to_string_lossy().into_owned();
let _ = std::fs::write(dir.join("second"), "x");
assert_eq!(
first_existing(&root, &["first", "second", "third"]).as_deref(),
Some("second")
);
assert_eq!(first_existing(&root, &["nope"]), None);
let _ = std::fs::remove_dir_all(&dir);
}
}