use std::collections::HashMap;
use chrono::DateTime;
use chrono::FixedOffset;
use super::status;
use super::types::LintRun;
use super::types::LintRunStatus;
use super::types::LintStatus;
#[derive(Clone, Debug, Default)]
pub struct LintRuns {
runs: Vec<LintRun>,
status: LintStatus,
archive_bytes: HashMap<String, u64>,
}
impl LintRuns {
pub fn runs(&self) -> &[LintRun] { &self.runs }
pub const fn status(&self) -> &LintStatus { &self.status }
pub fn last_started_at(&self) -> Option<DateTime<FixedOffset>> {
self.runs
.iter()
.find(|run| !matches!(run.status, LintRunStatus::Running))
.and_then(|run| status::parse_timestamp(&run.started_at))
}
pub fn archive_bytes(&self, run_id: &str) -> Option<u64> {
self.archive_bytes.get(run_id).copied()
}
pub fn set_runs(&mut self, runs: Vec<LintRun>) {
self.status = runs.first().map_or(LintStatus::NoLog, status::parse_run);
self.archive_bytes = runs
.iter()
.map(|run| (run.run_id.clone(), run.archive_bytes))
.collect();
self.runs = runs;
}
pub fn set_hydrated_runs(&mut self, runs: Vec<LintRun>) {
let live_status =
matches!(self.status, LintStatus::Running(_)).then(|| self.status.clone());
self.set_runs(runs);
if let Some(status) = live_status {
self.status = status;
}
}
pub const fn set_status(&mut self, status: LintStatus) { self.status = status; }
pub fn clear_runs(&mut self) {
self.runs.clear();
self.archive_bytes.clear();
self.status = LintStatus::NoLog;
}
#[cfg(test)]
pub fn has_archive_entry(&self, run_id: &str) -> bool {
self.archive_bytes.contains_key(run_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lint::types::LintRunStatus;
fn make_run(run_id: &str) -> LintRun {
LintRun {
run_id: run_id.to_string(),
started_at: "2026-04-24T12:00:00Z".to_string(),
finished_at: Some("2026-04-24T12:00:01Z".to_string()),
duration_ms: Some(1000),
status: LintRunStatus::Passed,
commands: Vec::new(),
archive_bytes: 0,
}
}
#[test]
fn set_runs_populates_archive_entry_per_run() {
let mut lr = LintRuns::default();
lr.set_runs(vec![make_run("a"), make_run("b")]);
assert!(lr.has_archive_entry("a"));
assert!(lr.has_archive_entry("b"));
assert!(!lr.has_archive_entry("c"));
}
#[test]
fn archive_bytes_returns_none_for_unknown_run_id() {
let lr = LintRuns::default();
assert_eq!(lr.archive_bytes("nonexistent"), None);
}
#[test]
fn archive_bytes_returns_the_size_persisted_on_the_run() {
let mut lr = LintRuns::default();
lr.set_runs(vec![make_run("a")]);
assert_eq!(lr.archive_bytes("a"), Some(0));
assert_eq!(lr.archive_bytes("not-a-real-run"), None);
}
#[test]
fn clear_runs_empties_archive_entries() {
let mut lr = LintRuns::default();
lr.set_runs(vec![make_run("a")]);
assert!(lr.has_archive_entry("a"));
lr.clear_runs();
assert!(!lr.has_archive_entry("a"));
assert!(lr.runs().is_empty());
}
#[test]
fn set_runs_replaces_previous_archive_entries() {
let mut lr = LintRuns::default();
lr.set_runs(vec![make_run("a")]);
lr.set_runs(vec![make_run("b")]);
assert!(!lr.has_archive_entry("a"), "old run's entry should be gone");
assert!(lr.has_archive_entry("b"));
}
#[test]
fn last_started_at_skips_running_and_parses_newest_terminal() {
fn run_at(run_id: &str, started_at: &str, status: LintRunStatus) -> LintRun {
LintRun {
run_id: run_id.to_string(),
started_at: started_at.to_string(),
finished_at: Some(started_at.to_string()),
duration_ms: Some(1),
status,
commands: Vec::new(),
archive_bytes: 0,
}
}
let mut lr = LintRuns::default();
assert_eq!(lr.last_started_at(), None);
lr.set_runs(vec![
run_at("c", "2026-04-24T12:00:05Z", LintRunStatus::Running),
run_at("b", "2026-04-24T12:00:03Z", LintRunStatus::Passed),
run_at("a", "2026-04-24T12:00:00Z", LintRunStatus::Failed),
]);
let expected = DateTime::parse_from_rfc3339("2026-04-24T12:00:03Z")
.ok()
.map(|ts| ts.timestamp());
assert_eq!(lr.last_started_at().map(|ts| ts.timestamp()), expected);
}
}