use crate::opslog::OpEntry;
use std::path::{Path, PathBuf};
use std::time::Duration;
pub(super) fn stale_label(age: Duration, stale_after: Duration) -> Option<String> {
(age >= stale_after).then(|| format!("derivation {} s behind", age.as_secs()))
}
pub(super) fn lateness(
started: std::time::Instant,
finished: std::time::Instant,
late_pass: Duration,
) -> Option<u64> {
let took = finished.saturating_duration_since(started);
(took >= late_pass).then_some(took.as_secs())
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Drift {
Desync(PathBuf),
Unannounced(PathBuf),
Unenumerated(PathBuf),
Late(PathBuf, u64),
}
impl Drift {
fn kind(&self) -> &'static str {
match self {
Drift::Desync(_) => "desync",
Drift::Unannounced(_) => "unannounced",
Drift::Unenumerated(_) => "unenumerated",
Drift::Late(..) => "late",
}
}
fn root(&self) -> &Path {
match self {
Drift::Desync(p)
| Drift::Unannounced(p)
| Drift::Unenumerated(p)
| Drift::Late(p, _) => p,
}
}
fn line(&self) -> String {
match self {
Drift::Late(root, secs) => format!("{} ({secs} s pass)\n", root.display()),
other => format!("{}\n", other.root().display()),
}
}
}
pub(super) fn entries(ts: &str, cwd: &str, found: &[Drift]) -> Vec<OpEntry> {
let mut sorted: Vec<&Drift> = found.iter().collect();
sorted.sort();
sorted.dedup();
let mut out: Vec<OpEntry> = Vec::new();
for drift in sorted {
let line = drift.line();
match out.last_mut() {
Some(entry) if entry.argv.last().map(String::as_str) == Some(drift.kind()) => {
entry.stderr.push_str(&line);
}
_ => out.push(OpEntry::drift(
ts.to_string(),
drift.kind(),
cwd.to_string(),
line,
)),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::opslog::{DRIFT_EXIT, YOG_DRIFT};
fn p(s: &str) -> PathBuf {
PathBuf::from(s)
}
#[test]
fn nothing_found_writes_nothing() {
assert!(entries("TS", "/state", &[]).is_empty());
}
#[test]
fn one_line_per_kind_carries_every_root_it_names() {
let found = [
Drift::Unannounced(p("/ws/b")),
Drift::Desync(p("/ws/a")),
Drift::Unannounced(p("/ws/a")),
Drift::Unannounced(p("/ws/b")), ];
let out = entries("TS", "/state", &found);
assert_eq!(out.len(), 2, "two kinds, two lines: {out:?}");
assert_eq!(
out[0].argv,
vec![YOG_DRIFT.to_string(), "desync".to_string()]
);
assert_eq!(out[0].stderr, "/ws/a\n");
assert_eq!(out[0].cwd, "/state");
assert_eq!(out[0].ts, "TS");
assert_eq!(out[0].exit, DRIFT_EXIT);
assert_eq!(
out[1].argv,
vec![YOG_DRIFT.to_string(), "unannounced".to_string()]
);
assert_eq!(out[1].stderr, "/ws/a\n/ws/b\n", "both roots, deduped");
}
#[test]
fn every_kind_names_itself_and_its_root() {
let all = [
Drift::Desync(p("/a")),
Drift::Unannounced(p("/b")),
Drift::Unenumerated(p("/c")),
Drift::Late(p("/state"), 7),
];
let kinds: Vec<&str> = all.iter().map(Drift::kind).collect();
assert_eq!(kinds, ["desync", "unannounced", "unenumerated", "late"]);
let roots: Vec<&Path> = all.iter().map(Drift::root).collect();
assert_eq!(
roots,
[
Path::new("/a"),
Path::new("/b"),
Path::new("/c"),
Path::new("/state")
]
);
assert_eq!(entries("TS", "/state", &all).len(), 4);
}
#[test]
fn a_late_pass_carries_how_late_it_was() {
let out = entries("TS", "/state", &[Drift::Late(p("/state"), 12)]);
assert_eq!(out[0].argv, vec![YOG_DRIFT.to_string(), "late".to_string()]);
assert_eq!(out[0].stderr, "/state (12 s pass)\n");
}
#[test]
fn lateness_fires_only_at_the_cadence_it_promised() {
let late_pass = crate::app::Cadence::default().late_pass();
let t0 = std::time::Instant::now();
let a_hair_early = (t0 + late_pass)
.checked_sub(Duration::from_millis(1))
.unwrap();
assert_eq!(lateness(t0, a_hair_early, late_pass), None);
assert_eq!(
lateness(t0, t0 + late_pass, late_pass),
Some(late_pass.as_secs())
);
assert_eq!(lateness(t0 + late_pass, t0, late_pass), None);
}
#[test]
fn the_staleness_line_is_silent_until_two_full_sweeps_are_missed() {
let stale = crate::app::Cadence::default().stale_after();
assert_eq!(stale_label(Duration::from_secs(0), stale), None);
let a_hair_fresh = stale.checked_sub(Duration::from_millis(1)).unwrap();
assert_eq!(stale_label(a_hair_fresh, stale), None);
assert_eq!(
stale_label(stale, stale),
Some(format!("derivation {} s behind", stale.as_secs()))
);
}
}