Skip to main content

forge/bench/
history.rs

1//! Benchmark history persistence (JSON file).
2
3use anyhow::{Context, Result};
4use wowlab_common::output;
5use wowlab_fs::{atomic, directory, file, path::Path};
6
7use super::types::BenchHistory;
8
9pub(super) fn load(path: &Path) -> BenchHistory {
10    match file::read_text(path) {
11        Ok(content) => serde_json::from_str(&content).unwrap_or(BenchHistory { runs: vec![] }),
12        Err(_) => BenchHistory { runs: vec![] },
13    }
14}
15
16pub(super) fn save(path: &Path, history: &BenchHistory) -> Result<()> {
17    if let Some(parent) = path.parent() {
18        directory::ensure(parent)
19            .with_context(|| format!("failed to create directory {}", parent.display()))?;
20    }
21
22    let json = serde_json::to_string_pretty(history).context("failed to serialize history")?;
23
24    atomic::replace(path, json).with_context(|| format!("failed to write {}", path.display()))?;
25    output::detail(&format!("History saved to {}", path.display()));
26
27    Ok(())
28}
29
30#[cfg(test)]
31mod tests {
32    use googletest::prelude::*;
33    use wowlab_fs::{directory, file, temporary::Directory};
34
35    use super::{BenchHistory, load, save};
36
37    #[gtest]
38    fn history_save_is_exact_atomic_and_loadable() -> Result<()> {
39        let temporary = Directory::new().or_fail()?;
40        let path = temporary.path().join("nested/history.json");
41        let history = BenchHistory { runs: Vec::new() };
42        let expected = serde_json::to_string_pretty(&history).or_fail()?;
43
44        save(&path, &history).or_fail()?;
45
46        verify_that!(file::read_text(&path).or_fail()?, eq(&expected))?;
47        verify_that!(load(&path).runs, is_empty())?;
48
49        verify_that!(
50            directory::entries(path.parent().or_fail()?).or_fail()?,
51            len(eq(1))
52        )
53    }
54
55    #[gtest]
56    fn missing_and_malformed_history_remain_compatible_with_an_empty_history() -> Result<()> {
57        let temporary = Directory::new().or_fail()?;
58        let path = temporary.path().join("history.json");
59
60        verify_that!(load(&path).runs, is_empty())?;
61
62        file::write_text(&path, "not json").or_fail()?;
63
64        verify_that!(load(&path).runs, is_empty())
65    }
66
67    #[gtest]
68    fn legacy_history_without_scaling_remains_loadable() -> Result<()> {
69        let temporary = Directory::new().or_fail()?;
70        let path = temporary.path().join("history.json");
71        let legacy = r#"{
72  "runs": [
73    {
74      "engine_version": "1",
75      "timestamp": "2026-01-01T00:00:00Z",
76      "git_hash": "abc123",
77      "git_branch": "main",
78      "rustc_version": "rustc",
79      "system": {
80        "cpu_model": "test",
81        "logical_cores": 1,
82        "physical_cores": 1,
83        "optimal_cores": 1,
84        "p_cores": 1,
85        "e_cores": 0,
86        "total_memory_mb": 1024,
87        "available_memory_mb": 512,
88        "os": "test",
89        "arch": "test"
90      },
91      "results": []
92    }
93  ]
94}"#;
95
96        file::write_text(&path, legacy).or_fail()?;
97
98        let history = load(&path);
99        let run = history.runs.first().or_fail()?;
100
101        verify_that!(run.scaling, is_empty())
102    }
103}