metrale_bench/
artifacts.rs1use std::path::{Path, PathBuf};
20
21use anyhow::{Context, Result};
22
23mod home;
24
25pub use home::{HomeFault, HomeSource, MetraleHome};
26
27#[derive(Clone, Debug)]
29pub struct ArtifactStore {
30 root: PathBuf,
31}
32
33impl ArtifactStore {
34 pub fn discover() -> Result<Self> {
38 Ok(Self {
39 root: MetraleHome::resolve()?.root,
40 })
41 }
42
43 pub fn discover_with_provenance() -> Result<MetraleHome> {
46 MetraleHome::resolve()
47 }
48
49 pub fn with_root(root: impl Into<PathBuf>) -> Self {
51 Self { root: root.into() }
52 }
53
54 pub fn root(&self) -> &Path {
55 &self.root
56 }
57
58 pub fn plugin_dir(&self, plugin_id: &str) -> Result<PathBuf> {
60 let dir = self.root.join("artifacts").join(plugin_id);
61 std::fs::create_dir_all(&dir)
62 .with_context(|| format!("creating artifact dir {}", dir.display()))?;
63 Ok(dir)
64 }
65
66 pub fn runs_dir(&self, benchmark_id: &str) -> Result<PathBuf> {
68 let dir = self.root.join("runs").join(benchmark_id);
69 std::fs::create_dir_all(&dir)
70 .with_context(|| format!("creating runs dir {}", dir.display()))?;
71 Ok(dir)
72 }
73}
74
75pub fn write_asset(dir: &Path, name: &str, contents: &str) -> Result<bool> {
81 write_asset_bytes(dir, name, contents.as_bytes())
82}
83
84pub fn write_asset_bytes(dir: &Path, name: &str, contents: &[u8]) -> Result<bool> {
89 let path = dir.join(name);
90 if let Ok(existing) = std::fs::read(&path)
91 && existing == contents
92 {
93 return Ok(false);
94 }
95 std::fs::write(&path, contents).with_context(|| format!("writing {}", path.display()))?;
96 Ok(true)
97}
98
99pub struct Stamp {
103 path: PathBuf,
104 expected: String,
105}
106
107impl Stamp {
108 pub fn new(dir: &Path, name: &str, expected: impl Into<String>) -> Self {
109 Self {
110 path: dir.join(name),
111 expected: expected.into(),
112 }
113 }
114
115 pub fn is_current(&self) -> bool {
116 std::fs::read_to_string(&self.path).is_ok_and(|s| s.trim() == self.expected.trim())
117 }
118
119 pub fn commit(&self) -> Result<()> {
123 std::fs::write(&self.path, &self.expected)
124 .with_context(|| format!("writing stamp {}", self.path.display()))
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::home::resolve_from;
131 use super::*;
132
133 fn tmp(name: &str) -> PathBuf {
134 let d =
135 std::env::temp_dir().join(format!("metrale-plugin-test-{name}-{}", std::process::id()));
136 let _ = std::fs::remove_dir_all(&d);
137 std::fs::create_dir_all(&d).unwrap();
138 d
139 }
140
141 #[test]
142 fn dirs_are_created_under_the_configured_root() {
143 let root = tmp("dirs");
144 let store = ArtifactStore::with_root(&root);
145 let p = store.plugin_dir("bfcl").unwrap();
146 let r = store.runs_dir("bfcl-subset").unwrap();
147 assert!(p.is_dir() && r.is_dir());
148 assert_eq!(p, root.join("artifacts/bfcl"));
149 assert_eq!(r, root.join("runs/bfcl-subset"));
150 }
151
152 #[test]
153 fn write_asset_rewrites_only_on_change() {
154 let dir = tmp("asset");
155 let path = dir.join("s.py");
156 assert!(write_asset(&dir, "s.py", "print(1)").unwrap());
157
158 let old = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1);
159 std::fs::File::options()
160 .write(true)
161 .open(&path)
162 .unwrap()
163 .set_times(std::fs::FileTimes::new().set_modified(old))
164 .unwrap();
165 let pinned_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
166
167 assert!(!write_asset(&dir, "s.py", "print(1)").unwrap());
168 assert_eq!(
169 std::fs::metadata(&path).unwrap().modified().unwrap(),
170 pinned_mtime,
171 "unchanged contents must not rewrite the asset"
172 );
173 assert!(write_asset(&dir, "s.py", "print(2)").unwrap());
174 assert_eq!(std::fs::read_to_string(path).unwrap(), "print(2)");
175 }
176
177 #[test]
178 fn binary_assets_are_compared_as_bytes() {
179 let dir = tmp("binary-asset");
180 let path = dir.join("image.bin");
181 let bytes = [0xff, 0x00, 0xfe];
182
183 assert!(write_asset_bytes(&dir, "image.bin", &bytes).unwrap());
184 assert!(!write_asset_bytes(&dir, "image.bin", &bytes).unwrap());
185 assert_eq!(std::fs::read(path).unwrap(), bytes);
186 }
187
188 fn home_with(tag: &str, dirs: &[&str]) -> PathBuf {
192 let home = tmp(tag);
193 for d in dirs {
194 std::fs::create_dir_all(home.join(d)).unwrap();
195 }
196 home
197 }
198
199 fn resolved(home: &Path) -> MetraleHome {
200 resolve_from(None, Some(home.as_os_str().to_owned())).unwrap()
201 }
202
203 #[test]
204 fn a_fresh_box_gets_the_default_home() {
205 let home = home_with("neither", &[]);
206 let got = resolved(&home);
207 assert_eq!(got.root, home.join(".metrale"));
208 assert_eq!(got.source, HomeSource::HomeDefault);
209 }
210
211 #[test]
213 fn metrale_home_wins_over_the_default_directory() {
214 let home = home_with("env-wins", &[".metrale"]);
215 let explicit = home.join("elsewhere");
216 let got = resolve_from(
217 Some(explicit.as_os_str().to_owned()),
218 Some(home.as_os_str().to_owned()),
219 )
220 .unwrap();
221 assert_eq!(got.root, explicit);
222 assert_eq!(got.source, HomeSource::Env);
223 }
224
225 #[test]
227 fn resolving_creates_nothing() {
228 let home = home_with("no-side-effects", &[]);
229 let _ = resolved(&home);
230 assert!(
231 !home.join(".metrale").exists(),
232 "resolve must not create the home it names"
233 );
234 let entries: Vec<_> = std::fs::read_dir(&home)
235 .unwrap()
236 .map(|e| e.unwrap().file_name())
237 .collect();
238 assert!(entries.is_empty(), "expected nothing, got {entries:?}");
239 }
240
241 #[test]
242 fn stamp_is_stale_until_committed_and_tracks_its_inputs() {
243 let dir = tmp("stamp");
244 let s = Stamp::new(&dir, ".provisioned", "v1");
245 assert!(!s.is_current());
246 s.commit().unwrap();
247 assert!(s.is_current());
248 assert!(!Stamp::new(&dir, ".provisioned", "v2").is_current());
249 }
250}