1use std::collections::{HashMap, VecDeque};
28
29const AUDIT_LOG_CAPACITY: usize = 1000;
31
32#[inline]
36pub fn fnv1a_64(data: &[u8]) -> u64 {
37 const OFFSET: u64 = 14_695_981_039_346_656_037;
38 const PRIME: u64 = 1_099_511_628_211;
39 data.iter()
40 .fold(OFFSET, |acc, &b| (acc ^ u64::from(b)).wrapping_mul(PRIME))
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
47pub struct MirrorId(pub String);
48
49impl MirrorId {
50 pub fn new(id: impl Into<String>) -> Self {
52 Self(id.into())
53 }
54
55 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59}
60
61impl std::fmt::Display for MirrorId {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 write!(f, "{}", self.0)
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct SyncItem {
72 pub cid: String,
74 pub checksum: u64,
76 pub size_bytes: u64,
78 pub version: u64,
80 pub last_modified: u64,
82 pub mirror_id: MirrorId,
84}
85
86impl SyncItem {
87 pub fn new(cid: String, size_bytes: u64, last_modified: u64, mirror_id: MirrorId) -> Self {
89 let checksum = fnv1a_64(cid.as_bytes());
90 Self {
91 cid,
92 checksum,
93 size_bytes,
94 version: 1,
95 last_modified,
96 mirror_id,
97 }
98 }
99
100 pub fn with_version(
102 cid: String,
103 size_bytes: u64,
104 version: u64,
105 last_modified: u64,
106 mirror_id: MirrorId,
107 ) -> Self {
108 let checksum = fnv1a_64(cid.as_bytes());
109 Self {
110 cid,
111 checksum,
112 size_bytes,
113 version,
114 last_modified,
115 mirror_id,
116 }
117 }
118
119 pub fn with_checksum(
121 cid: String,
122 checksum: u64,
123 size_bytes: u64,
124 version: u64,
125 last_modified: u64,
126 mirror_id: MirrorId,
127 ) -> Self {
128 Self {
129 cid,
130 checksum,
131 size_bytes,
132 version,
133 last_modified,
134 mirror_id,
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum ConflictType {
144 VersionConflict,
146 ChecksumMismatch,
148 SizeConflict,
150 DeleteVsModify,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum MsConflictResolution {
162 TakeLocal,
164 TakeRemote,
166 TakeNewest,
168 TakeLargest,
170 Skip,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct SyncConflict {
179 pub cid: String,
181 pub local_item: SyncItem,
183 pub remote_item: SyncItem,
185 pub conflict_type: ConflictType,
187}
188
189impl SyncConflict {
190 pub fn classify(local_item: SyncItem, remote_item: SyncItem) -> Self {
192 let conflict_type = if local_item.checksum != remote_item.checksum {
193 ConflictType::ChecksumMismatch
194 } else if local_item.version != remote_item.version {
195 ConflictType::VersionConflict
196 } else if local_item.size_bytes != remote_item.size_bytes {
197 ConflictType::SizeConflict
198 } else {
199 ConflictType::ChecksumMismatch
200 };
201 let cid = local_item.cid.clone();
202 Self {
203 cid,
204 local_item,
205 remote_item,
206 conflict_type,
207 }
208 }
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
215pub enum SyncOperation {
216 Upload {
218 cid: String,
220 to_mirror: MirrorId,
222 },
223 Download {
225 cid: String,
227 from_mirror: MirrorId,
229 },
230 Delete {
232 cid: String,
234 from_mirror: MirrorId,
236 },
237 Resolve {
239 conflict: SyncConflict,
241 resolution: MsConflictResolution,
243 },
244}
245
246#[derive(Debug, Clone)]
250pub struct SyncPlan {
251 pub operations: Vec<SyncOperation>,
253 pub conflicts: Vec<SyncConflict>,
255 pub estimated_bytes: u64,
257}
258
259impl SyncPlan {
260 pub fn is_empty(&self) -> bool {
262 self.operations.is_empty() && self.conflicts.is_empty()
263 }
264}
265
266#[derive(Debug, Clone, Default)]
273pub struct MsSyncResult {
274 pub operations_executed: usize,
276 pub bytes_transferred: u64,
278 pub conflicts_resolved: usize,
280 pub errors: Vec<String>,
282 pub duration_ms: u64,
284}
285
286#[derive(Debug, Clone, Default)]
290pub struct MirrorSyncStats {
291 pub local_items: usize,
293 pub remote_mirrors: usize,
295 pub total_conflicts_detected: u64,
297 pub total_operations: u64,
299 pub bytes_transferred: u64,
301}
302
303pub struct StorageMirrorSync {
310 pub local_id: MirrorId,
312 pub local_state: HashMap<String, SyncItem>,
314 pub remote_states: HashMap<MirrorId, HashMap<String, SyncItem>>,
316 pub default_resolution: MsConflictResolution,
318 pub audit_log: VecDeque<SyncOperation>,
320
321 total_conflicts_detected: u64,
323 total_operations: u64,
324 total_bytes_transferred: u64,
325}
326
327impl StorageMirrorSync {
328 pub fn new(local_id: MirrorId, default_resolution: MsConflictResolution) -> Self {
333 Self {
334 local_id,
335 local_state: HashMap::new(),
336 remote_states: HashMap::new(),
337 default_resolution,
338 audit_log: VecDeque::new(),
339 total_conflicts_detected: 0,
340 total_operations: 0,
341 total_bytes_transferred: 0,
342 }
343 }
344
345 pub fn register_mirror(&mut self, mirror_id: MirrorId) {
349 self.remote_states.entry(mirror_id).or_default();
350 }
351
352 pub fn update_local(&mut self, item: SyncItem) -> bool {
358 let is_new = !self.local_state.contains_key(&item.cid);
359 self.local_state.insert(item.cid.clone(), item);
360 is_new
361 }
362
363 pub fn remove_local(&mut self, cid: &str) -> bool {
367 self.local_state.remove(cid).is_some()
368 }
369
370 pub fn update_remote(&mut self, mirror_id: &MirrorId, item: SyncItem) -> bool {
377 match self.remote_states.get_mut(mirror_id) {
378 Some(state) => {
379 let is_new = !state.contains_key(&item.cid);
380 state.insert(item.cid.clone(), item);
381 is_new
382 }
383 None => false,
384 }
385 }
386
387 pub fn diff(&self, mirror_id: &MirrorId) -> SyncPlan {
393 let remote_state = match self.remote_states.get(mirror_id) {
394 Some(s) => s,
395 None => {
396 return SyncPlan {
397 operations: Vec::new(),
398 conflicts: Vec::new(),
399 estimated_bytes: 0,
400 }
401 }
402 };
403
404 let mut operations: Vec<SyncOperation> = Vec::new();
405 let mut conflicts: Vec<SyncConflict> = Vec::new();
406 let mut estimated_bytes: u64 = 0;
407
408 for (cid, local_item) in &self.local_state {
410 if !remote_state.contains_key(cid) {
411 estimated_bytes = estimated_bytes.saturating_add(local_item.size_bytes);
412 operations.push(SyncOperation::Upload {
413 cid: cid.clone(),
414 to_mirror: mirror_id.clone(),
415 });
416 }
417 }
418
419 for (cid, remote_item) in remote_state {
422 match self.local_state.get(cid) {
423 None => {
424 estimated_bytes = estimated_bytes.saturating_add(remote_item.size_bytes);
425 operations.push(SyncOperation::Download {
426 cid: cid.clone(),
427 from_mirror: mirror_id.clone(),
428 });
429 }
430 Some(local_item) => {
431 if local_item.checksum != remote_item.checksum {
432 let conflict =
433 SyncConflict::classify(local_item.clone(), remote_item.clone());
434 let resolution = self.resolve_conflict(&conflict);
436 match &resolution {
437 MsConflictResolution::TakeLocal => {
438 estimated_bytes =
439 estimated_bytes.saturating_add(local_item.size_bytes);
440 operations.push(SyncOperation::Resolve {
441 conflict: conflict.clone(),
442 resolution: MsConflictResolution::TakeLocal,
443 });
444 }
445 MsConflictResolution::TakeRemote => {
446 estimated_bytes =
447 estimated_bytes.saturating_add(remote_item.size_bytes);
448 operations.push(SyncOperation::Resolve {
449 conflict: conflict.clone(),
450 resolution: MsConflictResolution::TakeRemote,
451 });
452 }
453 MsConflictResolution::TakeNewest => {
454 let winner_bytes =
455 if local_item.last_modified >= remote_item.last_modified {
456 local_item.size_bytes
457 } else {
458 remote_item.size_bytes
459 };
460 estimated_bytes = estimated_bytes.saturating_add(winner_bytes);
461 operations.push(SyncOperation::Resolve {
462 conflict: conflict.clone(),
463 resolution: MsConflictResolution::TakeNewest,
464 });
465 }
466 MsConflictResolution::TakeLargest => {
467 let winner_bytes =
468 local_item.size_bytes.max(remote_item.size_bytes);
469 estimated_bytes = estimated_bytes.saturating_add(winner_bytes);
470 operations.push(SyncOperation::Resolve {
471 conflict: conflict.clone(),
472 resolution: MsConflictResolution::TakeLargest,
473 });
474 }
475 MsConflictResolution::Skip => {
476 conflicts.push(conflict);
477 }
478 }
479 }
480 }
481 }
482 }
483
484 SyncPlan {
485 operations,
486 conflicts,
487 estimated_bytes,
488 }
489 }
490
491 fn resolve_conflict(&self, _conflict: &SyncConflict) -> MsConflictResolution {
493 self.default_resolution.clone()
494 }
495
496 pub fn apply_plan(&mut self, plan: &SyncPlan, now: u64) -> MsSyncResult {
502 let start = now;
503 let mut result = MsSyncResult::default();
504
505 for op in &plan.operations {
506 match op {
507 SyncOperation::Upload { cid, to_mirror } => {
508 if let Some(local_item) = self.local_state.get(cid).cloned() {
509 let size = local_item.size_bytes;
510 let mut remote_item = local_item;
511 remote_item.mirror_id = to_mirror.clone();
512 if let Some(remote_state) = self.remote_states.get_mut(to_mirror) {
513 remote_state.insert(cid.clone(), remote_item);
514 } else {
515 result.errors.push(format!(
516 "Upload failed: mirror '{}' not registered",
517 to_mirror
518 ));
519 continue;
520 }
521 result.bytes_transferred = result.bytes_transferred.saturating_add(size);
522 } else {
523 result
524 .errors
525 .push(format!("Upload failed: CID '{}' not in local state", cid));
526 continue;
527 }
528 }
529
530 SyncOperation::Download { cid, from_mirror } => {
531 let remote_item_opt = self
533 .remote_states
534 .get(from_mirror)
535 .and_then(|s| s.get(cid))
536 .cloned();
537
538 if let Some(remote_item) = remote_item_opt {
539 let size = remote_item.size_bytes;
540 let mut local_item = remote_item;
541 local_item.mirror_id = self.local_id.clone();
542 self.local_state.insert(cid.clone(), local_item);
543 result.bytes_transferred = result.bytes_transferred.saturating_add(size);
544 } else {
545 result.errors.push(format!(
546 "Download failed: CID '{}' not in remote mirror '{}'",
547 cid, from_mirror
548 ));
549 continue;
550 }
551 }
552
553 SyncOperation::Delete { cid, from_mirror } => {
554 let removed = if *from_mirror == self.local_id {
555 self.local_state.remove(cid).is_some()
556 } else if let Some(remote_state) = self.remote_states.get_mut(from_mirror) {
557 remote_state.remove(cid).is_some()
558 } else {
559 false
560 };
561 if !removed {
562 result.errors.push(format!(
563 "Delete failed: CID '{}' not found in mirror '{}'",
564 cid, from_mirror
565 ));
566 continue;
567 }
568 }
569
570 SyncOperation::Resolve {
571 conflict,
572 resolution,
573 } => {
574 match resolution {
575 MsConflictResolution::TakeLocal => {
576 let local_opt = self.local_state.get(&conflict.cid).cloned();
578 if let Some(local_item) = local_opt {
579 let size = local_item.size_bytes;
580 let remote_mirror = conflict.remote_item.mirror_id.clone();
581 if let Some(remote_state) =
582 self.remote_states.get_mut(&remote_mirror)
583 {
584 let mut item = local_item;
585 item.mirror_id = remote_mirror.clone();
586 remote_state.insert(conflict.cid.clone(), item);
587 }
588 result.bytes_transferred =
589 result.bytes_transferred.saturating_add(size);
590 }
591 }
592 MsConflictResolution::TakeRemote => {
593 let remote_item_opt = self
595 .remote_states
596 .get(&conflict.remote_item.mirror_id)
597 .and_then(|s| s.get(&conflict.cid))
598 .cloned();
599 if let Some(remote_item) = remote_item_opt {
600 let size = remote_item.size_bytes;
601 let mut item = remote_item;
602 item.mirror_id = self.local_id.clone();
603 self.local_state.insert(conflict.cid.clone(), item);
604 result.bytes_transferred =
605 result.bytes_transferred.saturating_add(size);
606 }
607 }
608 MsConflictResolution::TakeNewest => {
609 let local_item = self.local_state.get(&conflict.cid).cloned();
610 let remote_item = self
611 .remote_states
612 .get(&conflict.remote_item.mirror_id)
613 .and_then(|s| s.get(&conflict.cid))
614 .cloned();
615
616 match (local_item, remote_item) {
617 (Some(l), Some(r)) => {
618 if l.last_modified >= r.last_modified {
619 let size = l.size_bytes;
621 let remote_mirror = r.mirror_id.clone();
622 if let Some(rs) = self.remote_states.get_mut(&remote_mirror)
623 {
624 let mut item = l;
625 item.mirror_id = remote_mirror.clone();
626 rs.insert(conflict.cid.clone(), item);
627 }
628 result.bytes_transferred =
629 result.bytes_transferred.saturating_add(size);
630 } else {
631 let size = r.size_bytes;
633 let mut item = r;
634 item.mirror_id = self.local_id.clone();
635 self.local_state.insert(conflict.cid.clone(), item);
636 result.bytes_transferred =
637 result.bytes_transferred.saturating_add(size);
638 }
639 }
640 _ => {
641 result.errors.push(format!(
642 "TakeNewest: item '{}' missing from one side",
643 conflict.cid
644 ));
645 continue;
646 }
647 }
648 }
649 MsConflictResolution::TakeLargest => {
650 let local_item = self.local_state.get(&conflict.cid).cloned();
651 let remote_item = self
652 .remote_states
653 .get(&conflict.remote_item.mirror_id)
654 .and_then(|s| s.get(&conflict.cid))
655 .cloned();
656
657 match (local_item, remote_item) {
658 (Some(l), Some(r)) => {
659 if l.size_bytes >= r.size_bytes {
660 let size = l.size_bytes;
662 let remote_mirror = r.mirror_id.clone();
663 if let Some(rs) = self.remote_states.get_mut(&remote_mirror)
664 {
665 let mut item = l;
666 item.mirror_id = remote_mirror.clone();
667 rs.insert(conflict.cid.clone(), item);
668 }
669 result.bytes_transferred =
670 result.bytes_transferred.saturating_add(size);
671 } else {
672 let size = r.size_bytes;
674 let mut item = r;
675 item.mirror_id = self.local_id.clone();
676 self.local_state.insert(conflict.cid.clone(), item);
677 result.bytes_transferred =
678 result.bytes_transferred.saturating_add(size);
679 }
680 }
681 _ => {
682 result.errors.push(format!(
683 "TakeLargest: item '{}' missing from one side",
684 conflict.cid
685 ));
686 continue;
687 }
688 }
689 }
690 MsConflictResolution::Skip => {
691 }
693 }
694
695 result.conflicts_resolved += 1;
696 }
697 }
698
699 self.audit_log.push_back(op.clone());
701 if self.audit_log.len() > AUDIT_LOG_CAPACITY {
702 self.audit_log.pop_front();
703 }
704 result.operations_executed += 1;
705 }
706
707 self.total_conflicts_detected = self
709 .total_conflicts_detected
710 .saturating_add(plan.conflicts.len() as u64);
711 self.total_operations = self
712 .total_operations
713 .saturating_add(result.operations_executed as u64);
714 self.total_bytes_transferred = self
715 .total_bytes_transferred
716 .saturating_add(result.bytes_transferred);
717
718 result.duration_ms = now.saturating_sub(start); result
720 }
721
722 pub fn detect_conflicts(&self, mirror_id: &MirrorId) -> Vec<SyncConflict> {
726 let remote_state = match self.remote_states.get(mirror_id) {
727 Some(s) => s,
728 None => return Vec::new(),
729 };
730
731 let mut result: Vec<SyncConflict> = Vec::new();
732 for (cid, remote_item) in remote_state {
733 if let Some(local_item) = self.local_state.get(cid) {
734 if local_item.checksum != remote_item.checksum {
735 result.push(SyncConflict::classify(
736 local_item.clone(),
737 remote_item.clone(),
738 ));
739 }
740 }
741 }
742 result.sort_by(|a, b| a.cid.cmp(&b.cid));
743 result
744 }
745
746 pub fn local_only_cids(&self, mirror_id: &MirrorId) -> Vec<&str> {
750 let remote_state = match self.remote_states.get(mirror_id) {
751 Some(s) => s,
752 None => {
753 let mut all: Vec<&str> = self.local_state.keys().map(|s| s.as_str()).collect();
754 all.sort_unstable();
755 return all;
756 }
757 };
758
759 let mut result: Vec<&str> = self
760 .local_state
761 .keys()
762 .filter(|cid| !remote_state.contains_key(*cid))
763 .map(|s| s.as_str())
764 .collect();
765 result.sort_unstable();
766 result
767 }
768
769 pub fn remote_only_cids(&self, mirror_id: &MirrorId) -> Vec<&str> {
771 let remote_state = match self.remote_states.get(mirror_id) {
772 Some(s) => s,
773 None => return Vec::new(),
774 };
775
776 let mut result: Vec<&str> = remote_state
777 .keys()
778 .filter(|cid| !self.local_state.contains_key(*cid))
779 .map(|s| s.as_str())
780 .collect();
781 result.sort_unstable();
782 result
783 }
784
785 pub fn in_sync_with(&self, mirror_id: &MirrorId) -> bool {
791 let remote_state = match self.remote_states.get(mirror_id) {
792 Some(s) => s,
793 None => return false,
794 };
795
796 if self.local_state.len() != remote_state.len() {
797 return false;
798 }
799
800 for (cid, local_item) in &self.local_state {
801 match remote_state.get(cid) {
802 Some(remote_item) if remote_item.checksum == local_item.checksum => {}
803 _ => return false,
804 }
805 }
806 true
807 }
808
809 pub fn audit_log_tail(&self, n: usize) -> Vec<&SyncOperation> {
813 let len = self.audit_log.len();
814 let skip = len.saturating_sub(n);
815 self.audit_log.iter().skip(skip).collect()
816 }
817
818 pub fn stats(&self) -> MirrorSyncStats {
822 MirrorSyncStats {
823 local_items: self.local_state.len(),
824 remote_mirrors: self.remote_states.len(),
825 total_conflicts_detected: self.total_conflicts_detected,
826 total_operations: self.total_operations,
827 bytes_transferred: self.total_bytes_transferred,
828 }
829 }
830}
831
832#[cfg(test)]
837mod tests {
838 use crate::mirror_sync::{
839 fnv1a_64, ConflictType, MirrorId, MirrorSyncStats, MsConflictResolution, MsSyncResult,
840 StorageMirrorSync, SyncConflict, SyncItem, SyncOperation, SyncPlan,
841 };
842
843 fn local_id() -> MirrorId {
846 MirrorId::new("local")
847 }
848
849 fn remote_id() -> MirrorId {
850 MirrorId::new("remote-1")
851 }
852
853 fn make_item(cid: &str, size: u64, ts: u64, mirror: MirrorId) -> SyncItem {
854 SyncItem::new(cid.to_string(), size, ts, mirror)
855 }
856
857 fn make_item_v(cid: &str, size: u64, version: u64, ts: u64, mirror: MirrorId) -> SyncItem {
858 SyncItem::with_version(cid.to_string(), size, version, ts, mirror)
859 }
860
861 fn default_sync() -> StorageMirrorSync {
862 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::TakeNewest);
863 s.register_mirror(remote_id());
864 s
865 }
866
867 #[test]
870 fn test_fnv1a_empty() {
871 let h = fnv1a_64(b"");
872 assert_eq!(h, 14_695_981_039_346_656_037);
873 }
874
875 #[test]
876 fn test_fnv1a_known_value() {
877 let h = fnv1a_64(b"hello");
878 assert_eq!(h, 0xa430d84680aabd0b);
880 }
881
882 #[test]
883 fn test_fnv1a_differs_by_input() {
884 assert_ne!(fnv1a_64(b"abc"), fnv1a_64(b"xyz"));
885 }
886
887 #[test]
890 fn test_mirror_id_new_and_as_str() {
891 let id = MirrorId::new("mirror-a");
892 assert_eq!(id.as_str(), "mirror-a");
893 }
894
895 #[test]
896 fn test_mirror_id_display() {
897 let id = MirrorId::new("m1");
898 assert_eq!(format!("{}", id), "m1");
899 }
900
901 #[test]
902 fn test_mirror_id_equality() {
903 assert_eq!(MirrorId::new("x"), MirrorId::new("x"));
904 assert_ne!(MirrorId::new("x"), MirrorId::new("y"));
905 }
906
907 #[test]
910 fn test_sync_item_checksum_matches_fnv1a() {
911 let item = make_item("QmTest", 128, 0, local_id());
912 assert_eq!(item.checksum, fnv1a_64(b"QmTest"));
913 }
914
915 #[test]
916 fn test_sync_item_with_version() {
917 let item = make_item_v("Qm1", 64, 5, 100, local_id());
918 assert_eq!(item.version, 5);
919 assert_eq!(item.last_modified, 100);
920 }
921
922 #[test]
923 fn test_sync_item_with_checksum() {
924 let item = SyncItem::with_checksum("Qm2".to_string(), 999, 32, 2, 200, local_id());
925 assert_eq!(item.checksum, 999);
926 }
927
928 #[test]
931 fn test_register_mirror_creates_empty_state() {
932 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::TakeLocal);
933 let rid = MirrorId::new("remote");
934 s.register_mirror(rid.clone());
935 assert!(s.remote_states.contains_key(&rid));
936 assert!(s.remote_states[&rid].is_empty());
937 }
938
939 #[test]
940 fn test_register_mirror_idempotent() {
941 let mut s = default_sync();
942 s.update_remote(&remote_id(), make_item("Qm1", 10, 0, remote_id()));
943 s.register_mirror(remote_id()); assert!(s.remote_states[&remote_id()].contains_key("Qm1"));
945 }
946
947 #[test]
950 fn test_update_local_returns_true_for_new() {
951 let mut s = default_sync();
952 let item = make_item("Qm1", 10, 0, local_id());
953 assert!(s.update_local(item));
954 }
955
956 #[test]
957 fn test_update_local_returns_false_for_update() {
958 let mut s = default_sync();
959 s.update_local(make_item("Qm1", 10, 0, local_id()));
960 assert!(!s.update_local(make_item("Qm1", 20, 1, local_id())));
961 }
962
963 #[test]
964 fn test_remove_local_returns_true_when_present() {
965 let mut s = default_sync();
966 s.update_local(make_item("Qm1", 10, 0, local_id()));
967 assert!(s.remove_local("Qm1"));
968 assert!(!s.local_state.contains_key("Qm1"));
969 }
970
971 #[test]
972 fn test_remove_local_returns_false_when_absent() {
973 let mut s = default_sync();
974 assert!(!s.remove_local("nonexistent"));
975 }
976
977 #[test]
980 fn test_update_remote_returns_false_for_unknown_mirror() {
981 let mut s = default_sync();
982 let unknown = MirrorId::new("ghost");
983 assert!(!s.update_remote(&unknown, make_item("Qm1", 10, 0, unknown.clone())));
984 }
985
986 #[test]
987 fn test_update_remote_inserts_item() {
988 let mut s = default_sync();
989 s.update_remote(&remote_id(), make_item("Qm1", 10, 0, remote_id()));
990 assert!(s.remote_states[&remote_id()].contains_key("Qm1"));
991 }
992
993 #[test]
996 fn test_diff_local_only_generates_upload() {
997 let mut s = default_sync();
998 s.update_local(make_item("QmA", 50, 0, local_id()));
999 let plan = s.diff(&remote_id());
1000 assert_eq!(plan.operations.len(), 1);
1001 assert!(matches!(
1002 &plan.operations[0],
1003 SyncOperation::Upload { cid, .. } if cid == "QmA"
1004 ));
1005 assert_eq!(plan.estimated_bytes, 50);
1006 }
1007
1008 #[test]
1011 fn test_diff_remote_only_generates_download() {
1012 let mut s = default_sync();
1013 s.update_remote(&remote_id(), make_item("QmB", 80, 0, remote_id()));
1014 let plan = s.diff(&remote_id());
1015 assert_eq!(plan.operations.len(), 1);
1016 assert!(matches!(
1017 &plan.operations[0],
1018 SyncOperation::Download { cid, .. } if cid == "QmB"
1019 ));
1020 assert_eq!(plan.estimated_bytes, 80);
1021 }
1022
1023 #[test]
1026 fn test_diff_in_sync_produces_no_ops() {
1027 let mut s = default_sync();
1028 s.update_local(make_item("QmC", 10, 1, local_id()));
1029 s.update_remote(&remote_id(), make_item("QmC", 10, 1, remote_id()));
1030 let plan = s.diff(&remote_id());
1031 assert!(plan.is_empty());
1032 }
1033
1034 #[test]
1037 fn test_diff_conflict_take_newest_local_wins() {
1038 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::TakeNewest);
1039 s.register_mirror(remote_id());
1040
1041 let local = SyncItem::with_checksum("Qm1".to_string(), 11, 100, 1, 200, local_id());
1042 let remote = SyncItem::with_checksum("Qm1".to_string(), 22, 80, 1, 100, remote_id());
1043 s.update_local(local);
1044 s.update_remote(&remote_id(), remote);
1045
1046 let plan = s.diff(&remote_id());
1047 assert_eq!(plan.operations.len(), 1);
1048 assert!(matches!(
1049 &plan.operations[0],
1050 SyncOperation::Resolve { resolution, .. }
1051 if *resolution == MsConflictResolution::TakeNewest
1052 ));
1053 }
1054
1055 #[test]
1058 fn test_diff_conflict_skip() {
1059 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::Skip);
1060 s.register_mirror(remote_id());
1061
1062 let local = SyncItem::with_checksum("Qm2".to_string(), 11, 100, 1, 0, local_id());
1063 let remote = SyncItem::with_checksum("Qm2".to_string(), 22, 80, 1, 0, remote_id());
1064 s.update_local(local);
1065 s.update_remote(&remote_id(), remote);
1066
1067 let plan = s.diff(&remote_id());
1068 assert!(plan.operations.is_empty());
1069 assert_eq!(plan.conflicts.len(), 1);
1070 }
1071
1072 #[test]
1075 fn test_diff_unknown_mirror_returns_empty() {
1076 let s = default_sync();
1077 let plan = s.diff(&MirrorId::new("ghost"));
1078 assert!(plan.is_empty());
1079 }
1080
1081 #[test]
1084 fn test_apply_plan_upload_updates_remote() {
1085 let mut s = default_sync();
1086 s.update_local(make_item("QmU", 64, 0, local_id()));
1087 let plan = s.diff(&remote_id());
1088 let res = s.apply_plan(&plan, 0);
1089 assert_eq!(res.operations_executed, 1);
1090 assert_eq!(res.bytes_transferred, 64);
1091 assert!(s.remote_states[&remote_id()].contains_key("QmU"));
1092 }
1093
1094 #[test]
1097 fn test_apply_plan_download_updates_local() {
1098 let mut s = default_sync();
1099 s.update_remote(&remote_id(), make_item("QmD", 32, 0, remote_id()));
1100 let plan = s.diff(&remote_id());
1101 let res = s.apply_plan(&plan, 0);
1102 assert_eq!(res.operations_executed, 1);
1103 assert_eq!(res.bytes_transferred, 32);
1104 assert!(s.local_state.contains_key("QmD"));
1105 }
1106
1107 #[test]
1110 fn test_in_sync_after_apply() {
1111 let mut s = default_sync();
1112 s.update_local(make_item("QmE", 16, 0, local_id()));
1113 let plan = s.diff(&remote_id());
1114 s.apply_plan(&plan, 0);
1115 assert!(s.in_sync_with(&remote_id()));
1116 }
1117
1118 #[test]
1121 fn test_apply_plan_audit_log_grows() {
1122 let mut s = default_sync();
1123 s.update_local(make_item("Qm1", 10, 0, local_id()));
1124 s.update_local(make_item("Qm2", 10, 0, local_id()));
1125 let plan = s.diff(&remote_id());
1126 s.apply_plan(&plan, 0);
1127 assert_eq!(s.audit_log.len(), 2);
1128 }
1129
1130 #[test]
1133 fn test_audit_log_tail_n_zero() {
1134 let s = default_sync();
1135 assert!(s.audit_log_tail(0).is_empty());
1136 }
1137
1138 #[test]
1139 fn test_audit_log_tail_returns_last_n() {
1140 let mut s = default_sync();
1141 for i in 0..5u64 {
1142 let cid = format!("Qm{}", i);
1143 s.update_local(make_item(&cid, 10, i, local_id()));
1144 }
1145 let plan = s.diff(&remote_id());
1146 s.apply_plan(&plan, 0);
1147 let tail = s.audit_log_tail(3);
1148 assert_eq!(tail.len(), 3);
1149 }
1150
1151 #[test]
1154 fn test_audit_log_capped_at_1000() {
1155 let mut s = default_sync();
1156 for i in 0..1100u64 {
1158 let cid = format!("QmCap{}", i);
1159 s.update_local(make_item(&cid, 1, i, local_id()));
1160 s.update_remote(&remote_id(), make_item(&cid, 999, i, remote_id()));
1161 }
1163 let mut s2 = default_sync();
1165 for i in 0..1100u64 {
1166 let cid = format!("QmFresh{}", i);
1167 s2.update_local(make_item(&cid, 1, i, local_id()));
1168 }
1169 let plan = s2.diff(&remote_id());
1170 s2.apply_plan(&plan, 0);
1171 assert!(s2.audit_log.len() <= 1000);
1172 }
1173
1174 #[test]
1177 fn test_detect_conflicts_finds_checksum_mismatch() {
1178 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::Skip);
1179 s.register_mirror(remote_id());
1180
1181 let local = SyncItem::with_checksum("QmX".to_string(), 1, 100, 1, 0, local_id());
1182 let remote = SyncItem::with_checksum("QmX".to_string(), 2, 80, 1, 0, remote_id());
1183 s.update_local(local);
1184 s.update_remote(&remote_id(), remote);
1185
1186 let conflicts = s.detect_conflicts(&remote_id());
1187 assert_eq!(conflicts.len(), 1);
1188 assert_eq!(conflicts[0].cid, "QmX");
1189 assert_eq!(conflicts[0].conflict_type, ConflictType::ChecksumMismatch);
1190 }
1191
1192 #[test]
1193 fn test_detect_conflicts_none_when_in_sync() {
1194 let mut s = default_sync();
1195 s.update_local(make_item("Qm1", 10, 0, local_id()));
1196 s.update_remote(&remote_id(), make_item("Qm1", 10, 0, remote_id()));
1197 assert!(s.detect_conflicts(&remote_id()).is_empty());
1198 }
1199
1200 #[test]
1201 fn test_detect_conflicts_sorted_by_cid() {
1202 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::Skip);
1203 s.register_mirror(remote_id());
1204
1205 for ch in ["zzz", "aaa", "mmm"] {
1206 let local = SyncItem::with_checksum(ch.to_string(), 1, 100, 1, 0, local_id());
1207 let remote = SyncItem::with_checksum(ch.to_string(), 2, 80, 1, 0, remote_id());
1208 s.update_local(local);
1209 s.update_remote(&remote_id(), remote);
1210 }
1211 let conflicts = s.detect_conflicts(&remote_id());
1212 assert_eq!(conflicts[0].cid, "aaa");
1213 assert_eq!(conflicts[1].cid, "mmm");
1214 assert_eq!(conflicts[2].cid, "zzz");
1215 }
1216
1217 #[test]
1220 fn test_local_only_cids_sorted() {
1221 let mut s = default_sync();
1222 s.update_local(make_item("z", 1, 0, local_id()));
1223 s.update_local(make_item("a", 1, 0, local_id()));
1224 s.update_local(make_item("m", 1, 0, local_id()));
1225 let cids = s.local_only_cids(&remote_id());
1226 assert_eq!(cids, vec!["a", "m", "z"]);
1227 }
1228
1229 #[test]
1230 fn test_local_only_cids_excludes_shared() {
1231 let mut s = default_sync();
1232 s.update_local(make_item("shared", 1, 0, local_id()));
1233 s.update_local(make_item("local-only", 1, 0, local_id()));
1234 s.update_remote(&remote_id(), make_item("shared", 1, 0, remote_id()));
1235 let cids = s.local_only_cids(&remote_id());
1236 assert_eq!(cids, vec!["local-only"]);
1237 }
1238
1239 #[test]
1242 fn test_remote_only_cids_sorted() {
1243 let mut s = default_sync();
1244 s.update_remote(&remote_id(), make_item("z", 1, 0, remote_id()));
1245 s.update_remote(&remote_id(), make_item("a", 1, 0, remote_id()));
1246 let cids = s.remote_only_cids(&remote_id());
1247 assert_eq!(cids, vec!["a", "z"]);
1248 }
1249
1250 #[test]
1251 fn test_remote_only_cids_unknown_mirror() {
1252 let s = default_sync();
1253 assert!(s.remote_only_cids(&MirrorId::new("ghost")).is_empty());
1254 }
1255
1256 #[test]
1259 fn test_in_sync_with_empty_states() {
1260 let s = default_sync();
1261 assert!(s.in_sync_with(&remote_id()));
1262 }
1263
1264 #[test]
1265 fn test_in_sync_with_false_different_size_sets() {
1266 let mut s = default_sync();
1267 s.update_local(make_item("Qm1", 10, 0, local_id()));
1268 assert!(!s.in_sync_with(&remote_id()));
1269 }
1270
1271 #[test]
1272 fn test_in_sync_with_false_for_unknown_mirror() {
1273 let s = default_sync();
1274 assert!(!s.in_sync_with(&MirrorId::new("ghost")));
1275 }
1276
1277 #[test]
1280 fn test_stats_initial() {
1281 let s = default_sync();
1282 let st = s.stats();
1283 assert_eq!(st.local_items, 0);
1284 assert_eq!(st.remote_mirrors, 1);
1285 assert_eq!(st.total_operations, 0);
1286 assert_eq!(st.bytes_transferred, 0);
1287 }
1288
1289 #[test]
1290 fn test_stats_after_apply() {
1291 let mut s = default_sync();
1292 s.update_local(make_item("Qm1", 50, 0, local_id()));
1293 let plan = s.diff(&remote_id());
1294 s.apply_plan(&plan, 0);
1295 let st = s.stats();
1296 assert_eq!(st.total_operations, 1);
1297 assert_eq!(st.bytes_transferred, 50);
1298 assert_eq!(st.local_items, 1);
1299 }
1300
1301 #[test]
1302 fn test_stats_conflicts_tracked() {
1303 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::Skip);
1304 s.register_mirror(remote_id());
1305
1306 let local = SyncItem::with_checksum("Qm1".to_string(), 1, 100, 1, 0, local_id());
1307 let remote = SyncItem::with_checksum("Qm1".to_string(), 2, 80, 1, 0, remote_id());
1308 s.update_local(local);
1309 s.update_remote(&remote_id(), remote);
1310
1311 let plan = s.diff(&remote_id());
1312 s.apply_plan(&plan, 0);
1313 let st = s.stats();
1314 assert_eq!(st.total_conflicts_detected, 1);
1315 }
1316
1317 #[test]
1320 fn test_resolve_take_local_pushes_to_remote() {
1321 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::TakeLocal);
1322 s.register_mirror(remote_id());
1323
1324 let local = SyncItem::with_checksum("Qm1".to_string(), 1, 100, 1, 0, local_id());
1325 let remote = SyncItem::with_checksum("Qm1".to_string(), 2, 80, 1, 0, remote_id());
1326 s.update_local(local);
1327 s.update_remote(&remote_id(), remote);
1328
1329 let plan = s.diff(&remote_id());
1330 s.apply_plan(&plan, 0);
1331
1332 let remote_item = &s.remote_states[&remote_id()]["Qm1"];
1334 assert_eq!(remote_item.checksum, 1);
1335 }
1336
1337 #[test]
1340 fn test_resolve_take_remote_pulls_to_local() {
1341 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::TakeRemote);
1342 s.register_mirror(remote_id());
1343
1344 let local = SyncItem::with_checksum("Qm1".to_string(), 1, 100, 1, 0, local_id());
1345 let remote = SyncItem::with_checksum("Qm1".to_string(), 2, 80, 1, 0, remote_id());
1346 s.update_local(local);
1347 s.update_remote(&remote_id(), remote);
1348
1349 let plan = s.diff(&remote_id());
1350 s.apply_plan(&plan, 0);
1351
1352 let local_item = &s.local_state["Qm1"];
1353 assert_eq!(local_item.checksum, 2);
1354 }
1355
1356 #[test]
1359 fn test_resolve_take_largest_selects_bigger() {
1360 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::TakeLargest);
1361 s.register_mirror(remote_id());
1362
1363 let local = SyncItem::with_checksum("Qm1".to_string(), 1, 200, 1, 0, local_id());
1365 let remote = SyncItem::with_checksum("Qm1".to_string(), 2, 100, 1, 0, remote_id());
1366 s.update_local(local);
1367 s.update_remote(&remote_id(), remote);
1368
1369 let plan = s.diff(&remote_id());
1370 s.apply_plan(&plan, 0);
1371
1372 let remote_item = &s.remote_states[&remote_id()]["Qm1"];
1374 assert_eq!(remote_item.size_bytes, 200);
1375 }
1376
1377 #[test]
1380 fn test_apply_delete_from_remote() {
1381 let mut s = default_sync();
1382 s.update_remote(&remote_id(), make_item("QmDel", 10, 0, remote_id()));
1383
1384 let plan = SyncPlan {
1385 operations: vec![SyncOperation::Delete {
1386 cid: "QmDel".to_string(),
1387 from_mirror: remote_id(),
1388 }],
1389 conflicts: Vec::new(),
1390 estimated_bytes: 0,
1391 };
1392 let res = s.apply_plan(&plan, 0);
1393 assert_eq!(res.operations_executed, 1);
1394 assert!(!s.remote_states[&remote_id()].contains_key("QmDel"));
1395 }
1396
1397 #[test]
1398 fn test_apply_delete_from_local() {
1399 let mut s = default_sync();
1400 s.update_local(make_item("QmDelLocal", 10, 0, local_id()));
1401
1402 let plan = SyncPlan {
1403 operations: vec![SyncOperation::Delete {
1404 cid: "QmDelLocal".to_string(),
1405 from_mirror: local_id(),
1406 }],
1407 conflicts: Vec::new(),
1408 estimated_bytes: 0,
1409 };
1410 s.apply_plan(&plan, 0);
1411 assert!(!s.local_state.contains_key("QmDelLocal"));
1412 }
1413
1414 #[test]
1417 fn test_multiple_mirrors_independent() {
1418 let mut s = StorageMirrorSync::new(local_id(), MsConflictResolution::TakeLocal);
1419 let r2 = MirrorId::new("remote-2");
1420 s.register_mirror(remote_id());
1421 s.register_mirror(r2.clone());
1422
1423 s.update_local(make_item("QmM", 10, 0, local_id()));
1424 s.update_remote(&r2, make_item("QmM", 10, 0, r2.clone()));
1425
1426 let plan1 = s.diff(&remote_id());
1427 let plan2 = s.diff(&r2);
1428 assert_eq!(plan1.operations.len(), 1); assert!(plan2.is_empty()); }
1431
1432 #[test]
1435 fn test_classify_checksum_mismatch() {
1436 let local = SyncItem::with_checksum("Qm".to_string(), 10, 50, 1, 0, local_id());
1437 let remote = SyncItem::with_checksum("Qm".to_string(), 20, 50, 1, 0, remote_id());
1438 let c = SyncConflict::classify(local, remote);
1439 assert_eq!(c.conflict_type, ConflictType::ChecksumMismatch);
1440 }
1441
1442 #[test]
1443 fn test_classify_version_conflict() {
1444 let local = SyncItem::with_checksum("Qm".to_string(), 10, 50, 1, 0, local_id());
1446 let mut remote = SyncItem::with_checksum("Qm".to_string(), 10, 50, 2, 0, remote_id());
1447 remote.checksum = local.checksum; let c = SyncConflict::classify(local, remote);
1449 assert_eq!(c.conflict_type, ConflictType::VersionConflict);
1450 }
1451
1452 #[test]
1455 fn test_plan_is_empty_true() {
1456 let plan = SyncPlan {
1457 operations: Vec::new(),
1458 conflicts: Vec::new(),
1459 estimated_bytes: 0,
1460 };
1461 assert!(plan.is_empty());
1462 }
1463
1464 #[test]
1465 fn test_plan_is_empty_false_when_ops() {
1466 let plan = SyncPlan {
1467 operations: vec![SyncOperation::Upload {
1468 cid: "x".to_string(),
1469 to_mirror: remote_id(),
1470 }],
1471 conflicts: Vec::new(),
1472 estimated_bytes: 0,
1473 };
1474 assert!(!plan.is_empty());
1475 }
1476
1477 #[test]
1480 fn test_ms_sync_result_default() {
1481 let r = MsSyncResult::default();
1482 assert_eq!(r.operations_executed, 0);
1483 assert_eq!(r.bytes_transferred, 0);
1484 assert!(r.errors.is_empty());
1485 }
1486
1487 #[test]
1490 fn test_mirror_sync_stats_default() {
1491 let st = MirrorSyncStats::default();
1492 assert_eq!(st.local_items, 0);
1493 assert_eq!(st.remote_mirrors, 0);
1494 }
1495}