1use std::collections::HashMap;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct SnapshotEntry {
17 pub cid: String,
19 pub size_bytes: u64,
21 pub tick: u64,
23}
24
25impl SnapshotEntry {
26 pub fn new(cid: impl Into<String>, size_bytes: u64, tick: u64) -> Self {
28 Self {
29 cid: cid.into(),
30 size_bytes,
31 tick,
32 }
33 }
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum DiffKind {
43 Added,
45 Removed,
47 Modified,
49 Unchanged,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct DiffEntry {
60 pub cid: String,
62 pub kind: DiffKind,
64 pub old_entry: Option<SnapshotEntry>,
66 pub new_entry: Option<SnapshotEntry>,
68}
69
70impl DiffEntry {
71 pub fn size_delta(&self) -> i64 {
78 let new_sz = self.new_entry.as_ref().map(|e| e.size_bytes).unwrap_or(0) as i64;
79 let old_sz = self.old_entry.as_ref().map(|e| e.size_bytes).unwrap_or(0) as i64;
80 new_sz - old_sz
81 }
82}
83
84#[derive(Clone, Debug, Default)]
90pub struct SnapshotDiffResult {
91 pub added: Vec<DiffEntry>,
93 pub removed: Vec<DiffEntry>,
95 pub modified: Vec<DiffEntry>,
97 pub unchanged: Vec<DiffEntry>,
99}
100
101impl SnapshotDiffResult {
102 pub fn total_size_delta(&self) -> i64 {
104 let sum_group = |v: &[DiffEntry]| -> i64 { v.iter().map(|e| e.size_delta()).sum() };
105 sum_group(&self.added) + sum_group(&self.modified) + sum_group(&self.removed)
106 }
107
108 pub fn has_changes(&self) -> bool {
110 !self.added.is_empty() || !self.removed.is_empty() || !self.modified.is_empty()
111 }
112
113 pub fn change_count(&self) -> usize {
115 self.added.len() + self.removed.len() + self.modified.len()
116 }
117}
118
119#[derive(Clone, Debug, Default)]
126pub struct DiffStats {
127 pub total_diffs_computed: u64,
129 pub total_entries_compared: u64,
131 pub total_changes_found: u64,
133}
134
135#[derive(Debug, Default)]
144pub struct StorageSnapshotDiff {
145 stats: DiffStats,
146}
147
148impl StorageSnapshotDiff {
149 pub fn new() -> Self {
151 Self::default()
152 }
153
154 pub fn diff(
160 &mut self,
161 old: &HashMap<String, SnapshotEntry>,
162 new: &HashMap<String, SnapshotEntry>,
163 ) -> SnapshotDiffResult {
164 let mut added = Vec::new();
165 let mut removed = Vec::new();
166 let mut modified = Vec::new();
167 let mut unchanged = Vec::new();
168
169 for (cid, old_entry) in old {
171 match new.get(cid) {
172 Some(new_entry) => {
173 let kind = if old_entry.size_bytes == new_entry.size_bytes
174 && old_entry.tick == new_entry.tick
175 {
176 DiffKind::Unchanged
177 } else {
178 DiffKind::Modified
179 };
180 let entry = DiffEntry {
181 cid: cid.clone(),
182 kind: kind.clone(),
183 old_entry: Some(old_entry.clone()),
184 new_entry: Some(new_entry.clone()),
185 };
186 if kind == DiffKind::Unchanged {
187 unchanged.push(entry);
188 } else {
189 modified.push(entry);
190 }
191 }
192 None => removed.push(DiffEntry {
193 cid: cid.clone(),
194 kind: DiffKind::Removed,
195 old_entry: Some(old_entry.clone()),
196 new_entry: None,
197 }),
198 }
199 }
200
201 for (cid, new_entry) in new {
203 if !old.contains_key(cid) {
204 added.push(DiffEntry {
205 cid: cid.clone(),
206 kind: DiffKind::Added,
207 old_entry: None,
208 new_entry: Some(new_entry.clone()),
209 });
210 }
211 }
212
213 added.sort_by(|a, b| a.cid.cmp(&b.cid));
215 removed.sort_by(|a, b| a.cid.cmp(&b.cid));
216 modified.sort_by(|a, b| a.cid.cmp(&b.cid));
217 unchanged.sort_by(|a, b| a.cid.cmp(&b.cid));
218
219 let unique_cids = {
221 let mut set: std::collections::HashSet<&str> = old.keys().map(String::as_str).collect();
222 set.extend(new.keys().map(String::as_str));
223 set.len() as u64
224 };
225
226 let changes = (added.len() + removed.len() + modified.len()) as u64;
227
228 self.stats.total_diffs_computed += 1;
229 self.stats.total_entries_compared += unique_cids;
230 self.stats.total_changes_found += changes;
231
232 SnapshotDiffResult {
233 added,
234 removed,
235 modified,
236 unchanged,
237 }
238 }
239
240 pub fn apply_patch(
248 &self,
249 base: &mut HashMap<String, SnapshotEntry>,
250 diff: &SnapshotDiffResult,
251 ) {
252 for entry in &diff.added {
253 if let Some(new_e) = &entry.new_entry {
254 base.insert(entry.cid.clone(), new_e.clone());
255 }
256 }
257 for entry in &diff.removed {
258 base.remove(&entry.cid);
259 }
260 for entry in &diff.modified {
261 if let Some(new_e) = &entry.new_entry {
262 base.insert(entry.cid.clone(), new_e.clone());
263 }
264 }
265 }
267
268 pub fn stats(&self) -> &DiffStats {
270 &self.stats
271 }
272}
273
274#[cfg(test)]
279mod tests {
280 use super::*;
281 use std::collections::HashMap;
282
283 fn make_entry(cid: &str, size: u64, tick: u64) -> SnapshotEntry {
288 SnapshotEntry::new(cid, size, tick)
289 }
290
291 fn single_entry_map(cid: &str, size: u64, tick: u64) -> HashMap<String, SnapshotEntry> {
292 let mut m = HashMap::new();
293 m.insert(cid.to_owned(), make_entry(cid, size, tick));
294 m
295 }
296
297 #[test]
302 fn test_diff_added_detected() {
303 let old: HashMap<String, SnapshotEntry> = HashMap::new();
304 let new = single_entry_map("bafy001", 100, 1);
305 let mut differ = StorageSnapshotDiff::new();
306 let result = differ.diff(&old, &new);
307 assert_eq!(result.added.len(), 1);
308 assert_eq!(result.added[0].cid, "bafy001");
309 assert_eq!(result.added[0].kind, DiffKind::Added);
310 assert!(result.added[0].old_entry.is_none());
311 assert!(result.added[0].new_entry.is_some());
312 }
313
314 #[test]
319 fn test_diff_removed_detected() {
320 let old = single_entry_map("bafy001", 100, 1);
321 let new: HashMap<String, SnapshotEntry> = HashMap::new();
322 let mut differ = StorageSnapshotDiff::new();
323 let result = differ.diff(&old, &new);
324 assert_eq!(result.removed.len(), 1);
325 assert_eq!(result.removed[0].cid, "bafy001");
326 assert_eq!(result.removed[0].kind, DiffKind::Removed);
327 assert!(result.removed[0].old_entry.is_some());
328 assert!(result.removed[0].new_entry.is_none());
329 }
330
331 #[test]
336 fn test_diff_modified_size_changed() {
337 let old = single_entry_map("bafy001", 100, 5);
338 let new = single_entry_map("bafy001", 200, 5);
339 let mut differ = StorageSnapshotDiff::new();
340 let result = differ.diff(&old, &new);
341 assert_eq!(result.modified.len(), 1);
342 assert_eq!(result.modified[0].kind, DiffKind::Modified);
343 }
344
345 #[test]
350 fn test_diff_modified_tick_changed() {
351 let old = single_entry_map("bafy001", 100, 5);
352 let new = single_entry_map("bafy001", 100, 6);
353 let mut differ = StorageSnapshotDiff::new();
354 let result = differ.diff(&old, &new);
355 assert_eq!(result.modified.len(), 1);
356 assert_eq!(result.modified[0].kind, DiffKind::Modified);
357 assert!(result.modified[0].old_entry.is_some());
358 assert!(result.modified[0].new_entry.is_some());
359 }
360
361 #[test]
366 fn test_diff_unchanged_detected() {
367 let old = single_entry_map("bafy001", 100, 5);
368 let new = single_entry_map("bafy001", 100, 5);
369 let mut differ = StorageSnapshotDiff::new();
370 let result = differ.diff(&old, &new);
371 assert_eq!(result.unchanged.len(), 1);
372 assert_eq!(result.unchanged[0].kind, DiffKind::Unchanged);
373 assert!(result.removed.is_empty());
374 assert!(result.added.is_empty());
375 assert!(result.modified.is_empty());
376 }
377
378 #[test]
383 fn test_diff_empty_old_all_added() {
384 let old: HashMap<String, SnapshotEntry> = HashMap::new();
385 let mut new = HashMap::new();
386 new.insert("a".to_owned(), make_entry("a", 10, 1));
387 new.insert("b".to_owned(), make_entry("b", 20, 2));
388 new.insert("c".to_owned(), make_entry("c", 30, 3));
389 let mut differ = StorageSnapshotDiff::new();
390 let result = differ.diff(&old, &new);
391 assert_eq!(result.added.len(), 3);
392 assert!(result.removed.is_empty());
393 assert!(result.modified.is_empty());
394 assert!(result.unchanged.is_empty());
395 }
396
397 #[test]
402 fn test_diff_empty_new_all_removed() {
403 let mut old = HashMap::new();
404 old.insert("a".to_owned(), make_entry("a", 10, 1));
405 old.insert("b".to_owned(), make_entry("b", 20, 2));
406 let new: HashMap<String, SnapshotEntry> = HashMap::new();
407 let mut differ = StorageSnapshotDiff::new();
408 let result = differ.diff(&old, &new);
409 assert_eq!(result.removed.len(), 2);
410 assert!(result.added.is_empty());
411 assert!(result.modified.is_empty());
412 assert!(result.unchanged.is_empty());
413 }
414
415 #[test]
420 fn test_diff_identical_snapshots_all_unchanged() {
421 let mut snap = HashMap::new();
422 snap.insert("a".to_owned(), make_entry("a", 10, 1));
423 snap.insert("b".to_owned(), make_entry("b", 20, 2));
424 let mut differ = StorageSnapshotDiff::new();
425 let result = differ.diff(&snap, &snap);
426 assert_eq!(result.unchanged.len(), 2);
427 assert!(!result.has_changes());
428 }
429
430 #[test]
435 fn test_total_size_delta_positive() {
436 let old: HashMap<String, SnapshotEntry> = HashMap::new();
437 let new = single_entry_map("bafy001", 500, 1);
438 let mut differ = StorageSnapshotDiff::new();
439 let result = differ.diff(&old, &new);
440 assert_eq!(result.total_size_delta(), 500);
441 }
442
443 #[test]
448 fn test_total_size_delta_negative() {
449 let old = single_entry_map("bafy001", 500, 1);
450 let new: HashMap<String, SnapshotEntry> = HashMap::new();
451 let mut differ = StorageSnapshotDiff::new();
452 let result = differ.diff(&old, &new);
453 assert_eq!(result.total_size_delta(), -500);
454 }
455
456 #[test]
461 fn test_total_size_delta_mixed() {
462 let mut old = HashMap::new();
463 old.insert("a".to_owned(), make_entry("a", 100, 1));
464 old.insert("b".to_owned(), make_entry("b", 200, 1));
465 let mut new = HashMap::new();
466 new.insert("b".to_owned(), make_entry("b", 300, 2)); new.insert("c".to_owned(), make_entry("c", 50, 1)); let mut differ = StorageSnapshotDiff::new();
470 let result = differ.diff(&old, &new);
471 assert_eq!(result.total_size_delta(), 50);
473 }
474
475 #[test]
480 fn test_has_changes_true() {
481 let old: HashMap<String, SnapshotEntry> = HashMap::new();
482 let new = single_entry_map("bafy001", 100, 1);
483 let mut differ = StorageSnapshotDiff::new();
484 let result = differ.diff(&old, &new);
485 assert!(result.has_changes());
486 }
487
488 #[test]
493 fn test_has_changes_false() {
494 let snap = single_entry_map("bafy001", 100, 1);
495 let mut differ = StorageSnapshotDiff::new();
496 let result = differ.diff(&snap, &snap);
497 assert!(!result.has_changes());
498 }
499
500 #[test]
505 fn test_change_count_correct() {
506 let mut old = HashMap::new();
507 old.insert("a".to_owned(), make_entry("a", 100, 1)); old.insert("b".to_owned(), make_entry("b", 200, 1)); let mut new = HashMap::new();
510 new.insert("b".to_owned(), make_entry("b", 300, 2));
511 new.insert("c".to_owned(), make_entry("c", 50, 1)); let mut differ = StorageSnapshotDiff::new();
513 let result = differ.diff(&old, &new);
514 assert_eq!(result.change_count(), 3);
516 }
517
518 #[test]
523 fn test_apply_patch_adds_entries() {
524 let old: HashMap<String, SnapshotEntry> = HashMap::new();
525 let new = single_entry_map("bafy001", 100, 1);
526 let mut differ = StorageSnapshotDiff::new();
527 let diff = differ.diff(&old, &new);
528 let mut base: HashMap<String, SnapshotEntry> = HashMap::new();
529 differ.apply_patch(&mut base, &diff);
530 assert!(base.contains_key("bafy001"));
531 assert_eq!(base["bafy001"].size_bytes, 100);
532 }
533
534 #[test]
539 fn test_apply_patch_removes_entries() {
540 let old = single_entry_map("bafy001", 100, 1);
541 let new: HashMap<String, SnapshotEntry> = HashMap::new();
542 let mut differ = StorageSnapshotDiff::new();
543 let diff = differ.diff(&old, &new);
544 let mut base = old.clone();
545 differ.apply_patch(&mut base, &diff);
546 assert!(!base.contains_key("bafy001"));
547 }
548
549 #[test]
554 fn test_apply_patch_modifies_entries() {
555 let old = single_entry_map("bafy001", 100, 1);
556 let new = single_entry_map("bafy001", 999, 2);
557 let mut differ = StorageSnapshotDiff::new();
558 let diff = differ.diff(&old, &new);
559 let mut base = old.clone();
560 differ.apply_patch(&mut base, &diff);
561 assert_eq!(base["bafy001"].size_bytes, 999);
562 assert_eq!(base["bafy001"].tick, 2);
563 }
564
565 #[test]
570 fn test_apply_patch_unchanged_stays() {
571 let snap = single_entry_map("bafy001", 100, 1);
572 let mut differ = StorageSnapshotDiff::new();
573 let diff = differ.diff(&snap, &snap);
574 let mut base = snap.clone();
575 differ.apply_patch(&mut base, &diff);
576 assert_eq!(base["bafy001"].size_bytes, 100);
577 }
578
579 #[test]
584 fn test_apply_patch_combined() {
585 let mut old = HashMap::new();
586 old.insert("keep".to_owned(), make_entry("keep", 10, 1));
587 old.insert("del".to_owned(), make_entry("del", 20, 1));
588 old.insert("upd".to_owned(), make_entry("upd", 30, 1));
589 let mut new = HashMap::new();
590 new.insert("keep".to_owned(), make_entry("keep", 10, 1));
591 new.insert("upd".to_owned(), make_entry("upd", 99, 2));
592 new.insert("fresh".to_owned(), make_entry("fresh", 50, 3));
593 let mut differ = StorageSnapshotDiff::new();
594 let diff = differ.diff(&old, &new);
595 let mut base = old;
596 differ.apply_patch(&mut base, &diff);
597 assert!(!base.contains_key("del"));
599 assert_eq!(base["keep"].size_bytes, 10);
601 assert_eq!(base["upd"].size_bytes, 99);
603 assert_eq!(base["fresh"].size_bytes, 50);
605 }
606
607 #[test]
612 fn test_added_sorted_by_cid() {
613 let old: HashMap<String, SnapshotEntry> = HashMap::new();
614 let mut new = HashMap::new();
615 for &cid in &["zzz", "aaa", "mmm", "bbb"] {
616 new.insert(cid.to_owned(), make_entry(cid, 1, 1));
617 }
618 let mut differ = StorageSnapshotDiff::new();
619 let result = differ.diff(&old, &new);
620 let cids: Vec<&str> = result.added.iter().map(|e| e.cid.as_str()).collect();
621 let mut sorted = cids.clone();
622 sorted.sort_unstable();
623 assert_eq!(cids, sorted);
624 }
625
626 #[test]
631 fn test_removed_sorted_by_cid() {
632 let mut old = HashMap::new();
633 for &cid in &["zzz", "aaa", "mmm"] {
634 old.insert(cid.to_owned(), make_entry(cid, 1, 1));
635 }
636 let new: HashMap<String, SnapshotEntry> = HashMap::new();
637 let mut differ = StorageSnapshotDiff::new();
638 let result = differ.diff(&old, &new);
639 let cids: Vec<&str> = result.removed.iter().map(|e| e.cid.as_str()).collect();
640 let mut sorted = cids.clone();
641 sorted.sort_unstable();
642 assert_eq!(cids, sorted);
643 }
644
645 #[test]
650 fn test_modified_sorted_by_cid() {
651 let mut old = HashMap::new();
652 let mut new = HashMap::new();
653 for &cid in &["zzz", "aaa", "mmm"] {
654 old.insert(cid.to_owned(), make_entry(cid, 1, 1));
655 new.insert(cid.to_owned(), make_entry(cid, 2, 1));
656 }
657 let mut differ = StorageSnapshotDiff::new();
658 let result = differ.diff(&old, &new);
659 let cids: Vec<&str> = result.modified.iter().map(|e| e.cid.as_str()).collect();
660 let mut sorted = cids.clone();
661 sorted.sort_unstable();
662 assert_eq!(cids, sorted);
663 }
664
665 #[test]
670 fn test_unchanged_sorted_by_cid() {
671 let mut snap = HashMap::new();
672 for &cid in &["zzz", "aaa", "mmm"] {
673 snap.insert(cid.to_owned(), make_entry(cid, 1, 1));
674 }
675 let mut differ = StorageSnapshotDiff::new();
676 let result = differ.diff(&snap, &snap);
677 let cids: Vec<&str> = result.unchanged.iter().map(|e| e.cid.as_str()).collect();
678 let mut sorted = cids.clone();
679 sorted.sort_unstable();
680 assert_eq!(cids, sorted);
681 }
682
683 #[test]
688 fn test_stats_accumulate() {
689 let mut differ = StorageSnapshotDiff::new();
690
691 let old1: HashMap<String, SnapshotEntry> = HashMap::new();
693 let new1 = single_entry_map("a", 10, 1);
694 differ.diff(&old1, &new1);
695
696 let mut old2 = HashMap::new();
698 old2.insert("b".to_owned(), make_entry("b", 20, 1));
699 old2.insert("c".to_owned(), make_entry("c", 30, 1));
700 let mut new2 = HashMap::new();
701 new2.insert("c".to_owned(), make_entry("c", 30, 1));
702 differ.diff(&old2, &new2);
703
704 let s = differ.stats();
705 assert_eq!(s.total_diffs_computed, 2);
706 assert_eq!(s.total_entries_compared, 3);
708 assert_eq!(s.total_changes_found, 2);
710 }
711
712 #[test]
717 fn test_stats_initial_state() {
718 let differ = StorageSnapshotDiff::new();
719 let s = differ.stats();
720 assert_eq!(s.total_diffs_computed, 0);
721 assert_eq!(s.total_entries_compared, 0);
722 assert_eq!(s.total_changes_found, 0);
723 }
724
725 #[test]
730 fn test_size_delta_added() {
731 let entry = DiffEntry {
732 cid: "x".to_owned(),
733 kind: DiffKind::Added,
734 old_entry: None,
735 new_entry: Some(make_entry("x", 300, 1)),
736 };
737 assert_eq!(entry.size_delta(), 300);
738 }
739
740 #[test]
745 fn test_size_delta_removed() {
746 let entry = DiffEntry {
747 cid: "x".to_owned(),
748 kind: DiffKind::Removed,
749 old_entry: Some(make_entry("x", 300, 1)),
750 new_entry: None,
751 };
752 assert_eq!(entry.size_delta(), -300);
753 }
754
755 #[test]
760 fn test_change_count_zero_identical() {
761 let snap = single_entry_map("a", 1, 1);
762 let mut differ = StorageSnapshotDiff::new();
763 let result = differ.diff(&snap, &snap);
764 assert_eq!(result.change_count(), 0);
765 }
766}