1use 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}