use std::path::{Path, PathBuf};
use amont_runtime::gate_evidence::{self, GateReport, Thresholds};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Document {
pub thresholds: ThresholdsJson,
pub repos: Vec<RepoGates>,
}
impl Document {
pub fn new(t: &Thresholds, repos: Vec<RepoGates>) -> Document {
Document {
thresholds: ThresholdsJson {
window_days: t.window_days,
min_runs: t.min_runs,
noop_ratio_percent: t.noop_ratio_percent,
noop_median_secs: t.noop_median_secs,
fast_pass_ms: t.fast_pass_ms,
stale_pushes: t.stale_runs,
stale_days: t.stale_days,
},
repos,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct ThresholdsJson {
pub window_days: u64,
pub min_runs: usize,
pub noop_ratio_percent: u64,
pub noop_median_secs: u64,
pub fast_pass_ms: u64,
pub stale_pushes: usize,
pub stale_days: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RepoGates {
pub repo: PathBuf,
pub gates: Vec<GateRow>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GateRow {
pub gate: String,
pub runs: usize,
pub passed: usize,
pub failed: usize,
pub unavailable: usize,
pub median_ms: u64,
pub last_ms: u64,
pub last_at: u64,
pub last_age_days: u64,
pub last_outcome: Option<String>,
pub missed_pushes: usize,
pub fingerprints: usize,
pub flags: Vec<FlagRow>,
pub insufficient_history: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct FlagRow {
pub flag: String,
pub why: String,
}
fn row(r: &GateReport, now: u64) -> GateRow {
GateRow {
gate: amont_runtime::ui::sanitize(&r.gate),
runs: r.runs,
passed: r.passed,
failed: r.failed,
unavailable: r.unavailable,
median_ms: r.median_ms,
last_ms: r.last_ms,
last_at: r.last_at,
last_age_days: r.last_age_days(now),
last_outcome: r.last_outcome.map(|o| o.as_str().to_string()),
missed_pushes: r.missed,
fingerprints: r.fingerprints,
flags: r
.flags
.iter()
.map(|f| FlagRow {
flag: f.kind.as_str().to_string(),
why: amont_runtime::ui::sanitize(&f.why),
})
.collect(),
insufficient_history: r.abstained.clone(),
}
}
pub fn collect(
root: &Path,
repos: &[PathBuf],
now: u64,
t: &Thresholds,
on: &mut dyn FnMut(&Path),
) -> Vec<RepoGates> {
repos
.iter()
.map(|repo| {
on(repo);
let history = gate_evidence::history_in(&root.join(repo));
RepoGates {
repo: repo.clone(),
gates: gate_evidence::summarise(&history, now, t)
.iter()
.map(|r| row(r, now))
.collect(),
}
})
.collect()
}
pub fn thresholds_line(t: &Thresholds) -> String {
format!(
"window {} days · no-op suspect below {}% of a median of {}s+, or a pass under {} from a gate that never was that fast · stale after {} missed pushes or {} days · under {} verdicts nothing is flagged",
t.window_days,
t.noop_ratio_percent,
t.noop_median_secs,
gate_evidence::duration(t.fast_pass_ms),
t.stale_runs,
t.stale_days,
t.min_runs,
)
}
pub fn report(all: &[RepoGates], t: &Thresholds, shown: impl Fn(&Path) -> String) {
println!("{}", thresholds_line(t));
println!();
let with_history: Vec<&RepoGates> = all.iter().filter(|r| !r.gates.is_empty()).collect();
let mut flagged = 0usize;
let mut gates = 0usize;
for repo in &with_history {
let name = shown(&repo.repo);
println!("{}", if name.is_empty() { "." } else { &name });
let width = repo
.gates
.iter()
.map(|g| g.gate.chars().count())
.max()
.unwrap_or(0)
.max(4);
for g in &repo.gates {
gates += 1;
println!(
" {:width$} {:>4} runs {:>3} pass {:>3} fail median {:>9} last {:>9} {:>4}d ago",
g.gate,
g.runs,
g.passed,
g.failed,
gate_evidence::duration(g.median_ms),
gate_evidence::duration(g.last_ms),
g.last_age_days,
);
if g.unavailable > 0 {
println!(
" {:width$} {} run(s) could not judge anything — not counted as passes",
"", g.unavailable
);
}
for f in &g.flags {
flagged += 1;
println!(" {:width$} {}: {}", "", f.flag.to_uppercase(), f.why);
}
if let Some(why) = &g.insufficient_history {
let withheld = if g.flags.is_empty() {
"no flag computed"
} else {
"nothing judged about its cost or its flakiness"
};
println!(" {:width$} {why} — {withheld}", "");
}
}
println!();
}
let quiet = all.len() - with_history.len();
println!(
"{} gate(s) with a record across {} repositor{}; {} finding(s)",
gates,
with_history.len(),
if with_history.len() == 1 { "y" } else { "ies" },
flagged,
);
if quiet > 0 {
println!(
"{quiet} repositor{} have pushed nothing through a gate yet (or their notes have been \
pruned) — no record, so nothing is claimed about them",
if quiet == 1 { "y" } else { "ies" }
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use amont_runtime::gate_stamp::{Run, RunOutcome};
fn history(rows: Vec<(&str, u64, &str, RunOutcome, u64)>) -> gate_evidence::History {
gate_evidence::History {
runs: rows
.into_iter()
.map(|(fp, at, gate, outcome, ms)| {
(
fp.to_string(),
Run {
at,
gate: gate.to_string(),
outcome,
ms,
},
)
})
.collect(),
}
}
#[test]
fn the_rows_are_the_runtimes_report() {
let now = 1_800_000_000;
let h = history(vec![
(
"t0",
now - 3 * 86_400,
"pre-push-cargo-test",
RunOutcome::Passed,
600_000,
),
(
"t1",
now - 86_400,
"pre-push-cargo-test",
RunOutcome::Failed,
590_000,
),
]);
let theirs = gate_evidence::summarise(&h, now, &Thresholds::default());
let ours: Vec<GateRow> = theirs.iter().map(|r| row(r, now)).collect();
assert_eq!(ours.len(), 1);
assert_eq!(
(
ours[0].runs,
ours[0].passed,
ours[0].failed,
ours[0].median_ms
),
(
theirs[0].runs,
theirs[0].passed,
theirs[0].failed,
theirs[0].median_ms
)
);
assert_eq!(
ours[0].insufficient_history.as_deref(),
Some("insufficient history (2 verdicts)"),
"and the abstention travels with them"
);
}
#[test]
fn a_repository_controlled_gate_id_cannot_move_the_cursor() {
let now = 1_800_000_000;
let h = history(vec![(
"t0",
now - 86_400,
"pre-push-\u{1b}[2Kevil",
RunOutcome::Passed,
10,
)]);
let reports = gate_evidence::summarise(&h, now, &Thresholds::default());
let r = row(&reports[0], now);
assert!(!r.gate.contains('\u{1b}'), "{:?}", r.gate);
}
#[test]
fn the_thresholds_line_states_the_numbers_it_used() {
let t = Thresholds {
window_days: 7,
noop_ratio_percent: 25,
..Thresholds::default()
};
let line = thresholds_line(&t);
assert!(line.contains("window 7 days"), "{line}");
assert!(line.contains("25%"), "{line}");
}
}