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).map_err(std::io::Error::other)?;
68 std::fs::write(&path, json)
69 }
70
71 fn manifest_path(cache_dir: &Path) -> PathBuf {
72 cache_dir.join("manifest.json")
73 }
74
75 pub fn diff(&self, current: &BTreeMap<String, String>) -> ManifestDiff {
81 self.diff_with_constraints(current, &BTreeMap::new())
82 }
83
84 pub fn diff_with_constraints(
92 &self,
93 current_fingerprints: &BTreeMap<String, String>,
94 current_constraint_hashes: &BTreeMap<String, String>,
95 ) -> ManifestDiff {
96 let mut unchanged = Vec::new();
97 let mut changed = Vec::new();
98 let mut added = Vec::new();
99 let mut removed = Vec::new();
100 let mut constraint_stale = Vec::new();
101
102 for (path, fingerprint) in current_fingerprints {
104 match self.modules.get(path) {
105 Some(entry) if entry.fingerprint == *fingerprint => {
106 if let Some(cur_hash) = current_constraint_hashes.get(path) {
108 if entry.constraint_hash.as_deref() != Some(cur_hash.as_str()) {
109 constraint_stale.push(path.clone());
110 }
111 }
112 unchanged.push(path.clone());
113 }
114 Some(_) => {
115 changed.push(path.clone());
116 }
117 None => {
118 added.push(path.clone());
119 }
120 }
121 }
122
123 for path in self.modules.keys() {
125 if !current_fingerprints.contains_key(path) {
126 removed.push(path.clone());
127 }
128 }
129
130 ManifestDiff {
131 unchanged,
132 changed,
133 added,
134 removed,
135 constraint_stale,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Default, PartialEq)]
142pub struct ManifestDiff {
143 pub unchanged: Vec<String>,
145
146 pub changed: Vec<String>,
148
149 pub added: Vec<String>,
151
152 pub removed: Vec<String>,
154
155 pub constraint_stale: Vec<String>,
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn empty_manifest_diff_shows_all_added() {
166 let manifest = CacheManifest::default();
167 let mut current = BTreeMap::new();
168 current.insert("a.ll".to_string(), "aaa".to_string());
169 current.insert("b.ll".to_string(), "bbb".to_string());
170
171 let diff = manifest.diff(¤t);
172 assert_eq!(diff.added.len(), 2);
173 assert!(diff.unchanged.is_empty());
174 assert!(diff.changed.is_empty());
175 assert!(diff.removed.is_empty());
176 }
177
178 #[test]
179 fn same_fingerprints_show_unchanged() {
180 let mut manifest = CacheManifest::default();
181 manifest.modules.insert(
182 "a.ll".to_string(),
183 ManifestEntry {
184 module_id: ModuleId::new(1),
185 fingerprint: "aaa".to_string(),
186 input_path: "a.ll".to_string(),
187 constraint_hash: None,
188 },
189 );
190
191 let mut current = BTreeMap::new();
192 current.insert("a.ll".to_string(), "aaa".to_string());
193
194 let diff = manifest.diff(¤t);
195 assert_eq!(diff.unchanged, vec!["a.ll"]);
196 assert!(diff.changed.is_empty());
197 }
198
199 #[test]
200 fn changed_fingerprint_detected() {
201 let mut manifest = CacheManifest::default();
202 manifest.modules.insert(
203 "a.ll".to_string(),
204 ManifestEntry {
205 module_id: ModuleId::new(1),
206 fingerprint: "old".to_string(),
207 input_path: "a.ll".to_string(),
208 constraint_hash: None,
209 },
210 );
211
212 let mut current = BTreeMap::new();
213 current.insert("a.ll".to_string(), "new".to_string());
214
215 let diff = manifest.diff(¤t);
216 assert_eq!(diff.changed, vec!["a.ll"]);
217 assert!(diff.unchanged.is_empty());
218 }
219
220 #[test]
221 fn removed_file_detected() {
222 let mut manifest = CacheManifest::default();
223 manifest.modules.insert(
224 "deleted.ll".to_string(),
225 ManifestEntry {
226 module_id: ModuleId::new(1),
227 fingerprint: "xxx".to_string(),
228 input_path: "deleted.ll".to_string(),
229 constraint_hash: None,
230 },
231 );
232
233 let current = BTreeMap::new(); let diff = manifest.diff(¤t);
236 assert_eq!(diff.removed, vec!["deleted.ll"]);
237 }
238
239 #[test]
240 fn manifest_roundtrip_through_filesystem() {
241 let tmp = tempfile::tempdir().unwrap();
242 let mut manifest = CacheManifest::default();
243 manifest.modules.insert(
244 "test.ll".to_string(),
245 ManifestEntry {
246 module_id: ModuleId::new(42),
247 fingerprint: "deadbeef".to_string(),
248 input_path: "test.ll".to_string(),
249 constraint_hash: Some("abc123".to_string()),
250 },
251 );
252
253 manifest.save(tmp.path()).unwrap();
254 let loaded = CacheManifest::load(tmp.path());
255 assert_eq!(manifest, loaded);
256 }
257
258 #[test]
259 fn same_fingerprint_same_constraint_hash_is_unchanged() {
260 let mut manifest = CacheManifest::default();
261 manifest.modules.insert(
262 "a.ll".to_string(),
263 ManifestEntry {
264 module_id: ModuleId::new(1),
265 fingerprint: "aaa".to_string(),
266 input_path: "a.ll".to_string(),
267 constraint_hash: Some("hash1".to_string()),
268 },
269 );
270
271 let mut fingerprints = BTreeMap::new();
272 fingerprints.insert("a.ll".to_string(), "aaa".to_string());
273
274 let mut constraint_hashes = BTreeMap::new();
275 constraint_hashes.insert("a.ll".to_string(), "hash1".to_string());
276
277 let diff = manifest.diff_with_constraints(&fingerprints, &constraint_hashes);
278 assert_eq!(diff.unchanged, vec!["a.ll"]);
279 assert!(diff.constraint_stale.is_empty());
280 }
281
282 #[test]
283 fn same_fingerprint_different_constraint_hash_is_constraint_stale() {
284 let mut manifest = CacheManifest::default();
285 manifest.modules.insert(
286 "a.ll".to_string(),
287 ManifestEntry {
288 module_id: ModuleId::new(1),
289 fingerprint: "aaa".to_string(),
290 input_path: "a.ll".to_string(),
291 constraint_hash: Some("old_hash".to_string()),
292 },
293 );
294
295 let mut fingerprints = BTreeMap::new();
296 fingerprints.insert("a.ll".to_string(), "aaa".to_string());
297
298 let mut constraint_hashes = BTreeMap::new();
299 constraint_hashes.insert("a.ll".to_string(), "new_hash".to_string());
300
301 let diff = manifest.diff_with_constraints(&fingerprints, &constraint_hashes);
302 assert_eq!(diff.unchanged, vec!["a.ll"]);
304 assert_eq!(diff.constraint_stale, vec!["a.ll"]);
306 }
307
308 #[test]
309 fn no_previous_constraint_hash_with_current_is_stale() {
310 let mut manifest = CacheManifest::default();
311 manifest.modules.insert(
312 "a.ll".to_string(),
313 ManifestEntry {
314 module_id: ModuleId::new(1),
315 fingerprint: "aaa".to_string(),
316 input_path: "a.ll".to_string(),
317 constraint_hash: None,
318 },
319 );
320
321 let mut fingerprints = BTreeMap::new();
322 fingerprints.insert("a.ll".to_string(), "aaa".to_string());
323
324 let mut constraint_hashes = BTreeMap::new();
325 constraint_hashes.insert("a.ll".to_string(), "new_hash".to_string());
326
327 let diff = manifest.diff_with_constraints(&fingerprints, &constraint_hashes);
328 assert_eq!(diff.unchanged, vec!["a.ll"]);
329 assert_eq!(diff.constraint_stale, vec!["a.ll"]);
331 }
332
333 #[test]
334 fn constraint_hash_not_checked_when_fingerprint_changed() {
335 let mut manifest = CacheManifest::default();
336 manifest.modules.insert(
337 "a.ll".to_string(),
338 ManifestEntry {
339 module_id: ModuleId::new(1),
340 fingerprint: "old_fp".to_string(),
341 input_path: "a.ll".to_string(),
342 constraint_hash: Some("old_hash".to_string()),
343 },
344 );
345
346 let mut fingerprints = BTreeMap::new();
347 fingerprints.insert("a.ll".to_string(), "new_fp".to_string());
348
349 let mut constraint_hashes = BTreeMap::new();
350 constraint_hashes.insert("a.ll".to_string(), "new_hash".to_string());
351
352 let diff = manifest.diff_with_constraints(&fingerprints, &constraint_hashes);
353 assert_eq!(diff.changed, vec!["a.ll"]);
354 assert!(diff.constraint_stale.is_empty());
356 }
357
358 #[test]
359 fn constraint_hash_skipped_when_not_in_current() {
360 let mut manifest = CacheManifest::default();
361 manifest.modules.insert(
362 "a.ll".to_string(),
363 ManifestEntry {
364 module_id: ModuleId::new(1),
365 fingerprint: "aaa".to_string(),
366 input_path: "a.ll".to_string(),
367 constraint_hash: Some("old_hash".to_string()),
368 },
369 );
370
371 let mut fingerprints = BTreeMap::new();
372 fingerprints.insert("a.ll".to_string(), "aaa".to_string());
373
374 let diff = manifest.diff_with_constraints(&fingerprints, &BTreeMap::new());
376 assert_eq!(diff.unchanged, vec!["a.ll"]);
377 assert!(diff.constraint_stale.is_empty());
378 }
379}