use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use anyhow::{Context, Result};
use super::BenchRun;
pub fn append(path: &Path, run: &BenchRun) -> Result<()> {
if run.machine.trim().is_empty() {
return Err(anyhow::anyhow!(
"BenchRun.machine is required for appended runs"
));
}
let line = serde_json::to_string(run).context("serialise bench run")?;
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.with_context(|| format!("open {}", path.display()))?;
writeln!(f, "{line}").context("write bench history line")?;
f.flush()?;
f.sync_all()?;
Ok(())
}
pub fn read_all(path: &Path) -> Result<Vec<BenchRun>> {
if !path.exists() {
return Ok(Vec::new());
}
let f = File::open(path).with_context(|| format!("open {}", path.display()))?;
let mut out = Vec::new();
for (idx, line) in BufReader::new(f).lines().enumerate() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let run: BenchRun = serde_json::from_str(&line)
.with_context(|| format!("parse line {} of {}", idx + 1, path.display()))?;
out.push(run);
}
Ok(out)
}
pub fn last_for_machine<'a>(history: &'a [BenchRun], machine: &str) -> Option<&'a BenchRun> {
history.iter().rev().find(|r| r.machine == machine)
}
pub fn migrate_legacy(legacy: &Path, defaults: &BenchRun) -> Result<std::path::PathBuf> {
let runs = read_all(legacy)?;
let target = legacy.with_extension("jsonl");
let mut out = String::new();
for mut r in runs {
if r.version.is_empty() { r.version = defaults.version.clone(); }
if r.machine.is_empty() { r.machine = defaults.machine.clone(); }
if r.cores == 0 { r.cores = defaults.cores; }
if r.timestamp.is_none() {
r.timestamp = Some(format!("{}T00:00:00Z", r.date));
}
out.push_str(&serde_json::to_string(&r)?);
out.push('\n');
}
std::fs::write(&target, out).with_context(|| format!("write {}", target.display()))?;
Ok(target)
}
#[cfg(test)]
mod tests {
use super::super::{BenchResult, BenchRun, TestOutcome};
#[test]
fn legacy_line_parses_without_timestamp_or_tests() {
let legacy = r#"{"date":"2026-05-27","version":"0.6.4","machine":"ryzen-9-7900","cores":32,"results":[{"name":"rust_crate_st","ops_sec":420072}]}"#;
let run: BenchRun = serde_json::from_str(legacy).unwrap();
assert_eq!(run.date, "2026-05-27");
assert!(run.timestamp.is_none());
assert!(run.tests.is_empty());
assert_eq!(run.results.len(), 1);
assert!(run.all_tests_passed(), "empty tests => passes");
}
#[test]
fn new_line_roundtrips_with_tests() {
let run = BenchRun {
date: "2026-05-30".into(),
timestamp: Some("2026-05-30T21:00:00Z".into()),
version: "0.6.5".into(),
machine: "ryzen-9-7900".into(),
cores: 32,
results: vec![BenchResult {
name: "x".into(),
metrics: serde_json::Map::new(),
}],
tests: vec![
TestOutcome { name: "a".into(), passed: true, duration_ms: Some(1.2), message: None },
TestOutcome { name: "b".into(), passed: false, duration_ms: None, message: Some("oops".into()) },
],
};
let s = serde_json::to_string(&run).unwrap();
let back: BenchRun = serde_json::from_str(&s).unwrap();
assert_eq!(back.failed_tests(), vec!["b"]);
assert!(!back.all_tests_passed());
assert_eq!(back.timestamp.as_deref(), Some("2026-05-30T21:00:00Z"));
}
}