1use std::collections::BTreeMap;
8use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11
12use crate::ids::{ModuleId, ProgramId};
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct ManifestEntry {
17 pub module_id: ModuleId,
19
20 pub fingerprint: String,
22
23 pub input_path: String,
25
26 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub constraint_hash: Option<String>,
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
38pub struct CacheManifest {
39 pub program_id: Option<ProgramId>,
41
42 pub modules: BTreeMap<String, ManifestEntry>,
44}
45
46impl CacheManifest {
47 pub fn load(cache_dir: &Path) -> Self {
49 let path = Self::manifest_path(cache_dir);
50 match std::fs::read_to_string(&path) {
51 Ok(json) => serde_json::from_str(&json).unwrap_or_default(),
52 Err(_) => Self::default(),
53 }
54 }
55
56 pub fn save(&self, cache_dir: &Path) -> Result<(), std::io::Error> {
63 let path = Self::manifest_path(cache_dir);
64 if let Some(parent) = path.parent() {
65 std::fs::create_dir_all(parent)?;
66 }
67 let json = serde_json::to_string_pretty(self)
68 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
69 std::fs::write(&path, json)
70 }
71
72 fn manifest_path(cache_dir: &Path) -> PathBuf {
73 cache_dir.join("manifest.json")
74 }
75
76 pub fn diff(&self, current: &BTreeMap<String, String>) -> ManifestDiff {
82 self.diff_with_constraints(current, &BTreeMap::new())
83 }
84
85 pub fn diff_with_constraints(
93 &self,
94 current_fingerprints: &BTreeMap<String, String>,
95 current_constraint_hashes: &BTreeMap<String, String>,
96 ) -> ManifestDiff {
97 let mut unchanged = Vec::new();
98 let mut changed = Vec::new();
99 let mut added = Vec::new();
100 let mut removed = Vec::new();
101 let mut constraint_stale = Vec::new();
102
103 for (path, fingerprint) in current_fingerprints {
105 match self.modules.get(path) {
106 Some(entry) if entry.fingerprint == *fingerprint => {
107 if let Some(cur_hash) = current_constraint_hashes.get(path) {
109 if entry.constraint_hash.as_deref() != Some(cur_hash.as_str()) {
110 constraint_stale.push(path.clone());
111 }
112 }
113 unchanged.push(path.clone());
114 }
115 Some(_) => {
116 changed.push(path.clone());
117 }
118 None => {
119 added.push(path.clone());
120 }
121 }
122 }
123
124 for path in self.modules.keys() {
126 if !current_fingerprints.contains_key(path) {
127 removed.push(path.clone());
128 }
129 }
130
131 ManifestDiff {
132 unchanged,
133 changed,
134 added,
135 removed,
136 constraint_stale,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Default, PartialEq)]
143pub struct ManifestDiff {
144 pub unchanged: Vec<String>,
146
147 pub changed: Vec<String>,
149
150 pub added: Vec<String>,
152
153 pub removed: Vec<String>,
155
156 pub constraint_stale: Vec<String>,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn empty_manifest_diff_shows_all_added() {
167 let manifest = CacheManifest::default();
168 let mut current = BTreeMap::new();
169 current.insert("a.ll".to_string(), "aaa".to_string());
170 current.insert("b.ll".to_string(), "bbb".to_string());
171
172 let diff = manifest.diff(¤t);
173 assert_eq!(diff.added.len(), 2);
174 assert!(diff.unchanged.is_empty());
175 assert!(diff.changed.is_empty());
176 assert!(diff.removed.is_empty());
177 }
178
179 #[test]
180 fn same_fingerprints_show_unchanged() {
181 let mut manifest = CacheManifest::default();
182 manifest.modules.insert(
183 "a.ll".to_string(),
184 ManifestEntry {
185 module_id: ModuleId::new(1),
186 fingerprint: "aaa".to_string(),
187 input_path: "a.ll".to_string(),
188 constraint_hash: None,
189 },
190 );
191
192 let mut current = BTreeMap::new();
193 current.insert("a.ll".to_string(), "aaa".to_string());
194
195 let diff = manifest.diff(¤t);
196 assert_eq!(diff.unchanged, vec!["a.ll"]);
197 assert!(diff.changed.is_empty());
198 }
199
200 #[test]
201 fn changed_fingerprint_detected() {
202 let mut manifest = CacheManifest::default();
203 manifest.modules.insert(
204 "a.ll".to_string(),
205 ManifestEntry {
206 module_id: ModuleId::new(1),
207 fingerprint: "old".to_string(),
208 input_path: "a.ll".to_string(),
209 constraint_hash: None,
210 },
211 );
212
213 let mut current = BTreeMap::new();
214 current.insert("a.ll".to_string(), "new".to_string());
215
216 let diff = manifest.diff(¤t);
217 assert_eq!(diff.changed, vec!["a.ll"]);
218 assert!(diff.unchanged.is_empty());
219 }
220
221 #[test]
222 fn removed_file_detected() {
223 let mut manifest = CacheManifest::default();
224 manifest.modules.insert(
225 "deleted.ll".to_string(),
226 ManifestEntry {
227 module_id: ModuleId::new(1),
228 fingerprint: "xxx".to_string(),
229 input_path: "deleted.ll".to_string(),
230 constraint_hash: None,
231 },
232 );
233
234 let current = BTreeMap::new(); let diff = manifest.diff(¤t);
237 assert_eq!(diff.removed, vec!["deleted.ll"]);
238 }
239
240 #[test]
241 fn manifest_roundtrip_through_filesystem() {
242 let tmp = tempfile::tempdir().unwrap();
243 let mut manifest = CacheManifest::default();
244 manifest.modules.insert(
245 "test.ll".to_string(),
246 ManifestEntry {
247 module_id: ModuleId::new(42),
248 fingerprint: "deadbeef".to_string(),
249 input_path: "test.ll".to_string(),
250 constraint_hash: Some("abc123".to_string()),
251 },
252 );
253
254 manifest.save(tmp.path()).unwrap();
255 let loaded = CacheManifest::load(tmp.path());
256 assert_eq!(manifest, loaded);
257 }
258
259 #[test]
260 fn same_fingerprint_same_constraint_hash_is_unchanged() {
261 let mut manifest = CacheManifest::default();
262 manifest.modules.insert(
263 "a.ll".to_string(),
264 ManifestEntry {
265 module_id: ModuleId::new(1),
266 fingerprint: "aaa".to_string(),
267 input_path: "a.ll".to_string(),
268 constraint_hash: Some("hash1".to_string()),
269 },
270 );
271
272 let mut fingerprints = BTreeMap::new();
273 fingerprints.insert("a.ll".to_string(), "aaa".to_string());
274
275 let mut constraint_hashes = BTreeMap::new();
276 constraint_hashes.insert("a.ll".to_string(), "hash1".to_string());
277
278 let diff = manifest.diff_with_constraints(&fingerprints, &constraint_hashes);
279 assert_eq!(diff.unchanged, vec!["a.ll"]);
280 assert!(diff.constraint_stale.is_empty());
281 }
282
283 #[test]
284 fn same_fingerprint_different_constraint_hash_is_constraint_stale() {
285 let mut manifest = CacheManifest::default();
286 manifest.modules.insert(
287 "a.ll".to_string(),
288 ManifestEntry {
289 module_id: ModuleId::new(1),
290 fingerprint: "aaa".to_string(),
291 input_path: "a.ll".to_string(),
292 constraint_hash: Some("old_hash".to_string()),
293 },
294 );
295
296 let mut fingerprints = BTreeMap::new();
297 fingerprints.insert("a.ll".to_string(), "aaa".to_string());
298
299 let mut constraint_hashes = BTreeMap::new();
300 constraint_hashes.insert("a.ll".to_string(), "new_hash".to_string());
301
302 let diff = manifest.diff_with_constraints(&fingerprints, &constraint_hashes);
303 assert_eq!(diff.unchanged, vec!["a.ll"]);
305 assert_eq!(diff.constraint_stale, vec!["a.ll"]);
307 }
308
309 #[test]
310 fn no_previous_constraint_hash_with_current_is_stale() {
311 let mut manifest = CacheManifest::default();
312 manifest.modules.insert(
313 "a.ll".to_string(),
314 ManifestEntry {
315 module_id: ModuleId::new(1),
316 fingerprint: "aaa".to_string(),
317 input_path: "a.ll".to_string(),
318 constraint_hash: None,
319 },
320 );
321
322 let mut fingerprints = BTreeMap::new();
323 fingerprints.insert("a.ll".to_string(), "aaa".to_string());
324
325 let mut constraint_hashes = BTreeMap::new();
326 constraint_hashes.insert("a.ll".to_string(), "new_hash".to_string());
327
328 let diff = manifest.diff_with_constraints(&fingerprints, &constraint_hashes);
329 assert_eq!(diff.unchanged, vec!["a.ll"]);
330 assert_eq!(diff.constraint_stale, vec!["a.ll"]);
332 }
333
334 #[test]
335 fn constraint_hash_not_checked_when_fingerprint_changed() {
336 let mut manifest = CacheManifest::default();
337 manifest.modules.insert(
338 "a.ll".to_string(),
339 ManifestEntry {
340 module_id: ModuleId::new(1),
341 fingerprint: "old_fp".to_string(),
342 input_path: "a.ll".to_string(),
343 constraint_hash: Some("old_hash".to_string()),
344 },
345 );
346
347 let mut fingerprints = BTreeMap::new();
348 fingerprints.insert("a.ll".to_string(), "new_fp".to_string());
349
350 let mut constraint_hashes = BTreeMap::new();
351 constraint_hashes.insert("a.ll".to_string(), "new_hash".to_string());
352
353 let diff = manifest.diff_with_constraints(&fingerprints, &constraint_hashes);
354 assert_eq!(diff.changed, vec!["a.ll"]);
355 assert!(diff.constraint_stale.is_empty());
357 }
358
359 #[test]
360 fn constraint_hash_skipped_when_not_in_current() {
361 let mut manifest = CacheManifest::default();
362 manifest.modules.insert(
363 "a.ll".to_string(),
364 ManifestEntry {
365 module_id: ModuleId::new(1),
366 fingerprint: "aaa".to_string(),
367 input_path: "a.ll".to_string(),
368 constraint_hash: Some("old_hash".to_string()),
369 },
370 );
371
372 let mut fingerprints = BTreeMap::new();
373 fingerprints.insert("a.ll".to_string(), "aaa".to_string());
374
375 let diff = manifest.diff_with_constraints(&fingerprints, &BTreeMap::new());
377 assert_eq!(diff.unchanged, vec!["a.ll"]);
378 assert!(diff.constraint_stale.is_empty());
379 }
380}