From ed2547863b734081d297b3c81085acb982e89e9e Mon Sep 17 00:00:00 2001 From: alice Date: Sun, 5 Jul 2026 07:12:01 +0700 Subject: [PATCH] test(setu-vlc): add unit tests for VectorClock and VLCSnapshot The setu-vlc crate (vector logical clocks, the causality primitive used across consensus) shipped with a full public API but no tests, while sibling leaf crates carry 30-67. This adds 32 unit tests plus serde round-trip coverage for both VectorClock and VLCSnapshot: construction, increment/set/merge, happens-before / concurrency semantics (including empty-clock and new-node edge cases), transitivity, dynamic membership (remove/reset), GC (gc_zero_nodes, retain_active_nodes), and the receive() hybrid-logical-clock update rule. Adds serde_json as a dev-dependency for the serialization round-trip tests. No library code changed; behavior-preserving. --- crates/setu-vlc/Cargo.toml | 1 + crates/setu-vlc/src/lib.rs | 358 +++++++++++++++++++++++++++++++++++++ 2 files changed, 359 insertions(+) diff --git a/crates/setu-vlc/Cargo.toml b/crates/setu-vlc/Cargo.toml index 1cb74aeb..0e85cc75 100644 --- a/crates/setu-vlc/Cargo.toml +++ b/crates/setu-vlc/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" serde = { version = "1.0", features = ["derive"] } [dev-dependencies] +serde_json = "1.0" diff --git a/crates/setu-vlc/src/lib.rs b/crates/setu-vlc/src/lib.rs index 63e363f1..139cef4b 100644 --- a/crates/setu-vlc/src/lib.rs +++ b/crates/setu-vlc/src/lib.rs @@ -319,3 +319,361 @@ impl Default for VLCSnapshot { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + + // ---------------------------------------------------------------------- + // VectorClock: construction & basic accessors + // ---------------------------------------------------------------------- + + #[test] + fn new_clock_is_empty() { + let vc = VectorClock::new(); + assert!(vc.is_empty()); + assert_eq!(vc.len(), 0); + assert!(vc.nodes().is_empty()); + } + + #[test] + fn default_equals_new() { + assert_eq!(VectorClock::default(), VectorClock::new()); + } + + #[test] + fn with_node_initializes_to_zero() { + let vc = VectorClock::with_node("a".to_string()); + assert_eq!(vc.len(), 1); + assert_eq!(vc.get("a"), 0); + assert!(!vc.is_empty()); + } + + #[test] + fn get_unknown_node_returns_zero() { + let vc = VectorClock::new(); + assert_eq!(vc.get("ghost"), 0); + } + + // ---------------------------------------------------------------------- + // VectorClock: increment / set + // ---------------------------------------------------------------------- + + #[test] + fn increment_creates_and_bumps() { + let mut vc = VectorClock::new(); + assert_eq!(vc.increment("a"), 1); // auto-creates at 0 then +1 + assert_eq!(vc.increment("a"), 2); + assert_eq!(vc.get("a"), 2); + assert_eq!(vc.len(), 1); + } + + #[test] + fn increment_returns_new_value() { + let mut vc = VectorClock::new(); + let returned = vc.increment("x"); + assert_eq!(returned, vc.get("x")); + } + + #[test] + fn set_overwrites_value() { + let mut vc = VectorClock::new(); + vc.set("a", 42); + assert_eq!(vc.get("a"), 42); + vc.set("a", 7); + assert_eq!(vc.get("a"), 7); + } + + // ---------------------------------------------------------------------- + // VectorClock: merge + // ---------------------------------------------------------------------- + + #[test] + fn merge_takes_elementwise_max_and_unions_nodes() { + let mut a = VectorClock::new(); + a.set("n1", 5); + a.set("n2", 2); + + let mut b = VectorClock::new(); + b.set("n1", 3); + b.set("n2", 9); + b.set("n3", 1); + + a.merge(&b); + assert_eq!(a.get("n1"), 5); // max(5,3) + assert_eq!(a.get("n2"), 9); // max(2,9) + assert_eq!(a.get("n3"), 1); // added from b + assert_eq!(a.len(), 3); + } + + #[test] + fn merge_is_idempotent() { + let mut a = VectorClock::new(); + a.set("n1", 5); + let snapshot = a.clone(); + a.merge(&snapshot); + assert_eq!(a, snapshot); + } + + // ---------------------------------------------------------------------- + // VectorClock: causality (happens_before / is_concurrent) + // ---------------------------------------------------------------------- + + #[test] + fn happens_before_strict_prefix() { + let mut a = VectorClock::new(); + a.set("n1", 1); + let mut b = a.clone(); + b.increment("n1"); // b = {n1:2} + assert!(a.happens_before(&b)); + assert!(!b.happens_before(&a)); + } + + #[test] + fn happens_before_detects_new_node_in_other() { + // self = {n1:1}, other = {n1:1, n2:1} => self < other + let mut a = VectorClock::new(); + a.set("n1", 1); + let mut b = a.clone(); + b.set("n2", 1); + assert!(a.happens_before(&b)); + assert!(!b.happens_before(&a)); + } + + #[test] + fn equal_clocks_do_not_happen_before_each_other() { + let mut a = VectorClock::new(); + a.set("n1", 3); + let b = a.clone(); + assert!(!a.happens_before(&b)); + assert!(!b.happens_before(&a)); + assert!(!a.is_concurrent(&b)); // equal => not concurrent + } + + #[test] + fn concurrent_clocks_have_no_ordering() { + // a = {n1:2, n2:0}, b = {n1:0, n2:2} -> concurrent + let mut a = VectorClock::new(); + a.set("n1", 2); + let mut b = VectorClock::new(); + b.set("n2", 2); + assert!(!a.happens_before(&b)); + assert!(!b.happens_before(&a)); + assert!(a.is_concurrent(&b)); + assert!(b.is_concurrent(&a)); + } + + #[test] + fn empty_happens_before_nonempty() { + let empty = VectorClock::new(); + let mut nonempty = VectorClock::new(); + nonempty.set("n1", 1); + assert!(empty.happens_before(&nonempty)); + assert!(!nonempty.happens_before(&empty)); + } + + #[test] + fn two_empty_clocks_are_neither_ordered_nor_concurrent() { + let a = VectorClock::new(); + let b = VectorClock::new(); + assert!(!a.happens_before(&b)); + assert!(!a.is_concurrent(&b)); // equal (both empty) + } + + #[test] + fn happens_before_is_transitive() { + let mut a = VectorClock::new(); + a.set("n1", 1); + let mut b = VectorClock::new(); + b.set("n1", 2); + let mut c = VectorClock::new(); + c.set("n1", 3); + assert!(a.happens_before(&b)); + assert!(b.happens_before(&c)); + assert!(a.happens_before(&c)); // transitivity + } + + // ---------------------------------------------------------------------- + // VectorClock: dynamic membership & GC + // ---------------------------------------------------------------------- + + #[test] + fn remove_node_returns_previous_value() { + let mut vc = VectorClock::new(); + vc.set("a", 4); + assert_eq!(vc.remove_node("a"), Some(4)); + assert_eq!(vc.remove_node("a"), None); + assert!(vc.is_empty()); + } + + #[test] + fn reset_node_zeroes_existing_only() { + let mut vc = VectorClock::new(); + vc.set("a", 9); + vc.reset_node("a"); + assert_eq!(vc.get("a"), 0); + assert_eq!(vc.len(), 1); // still present + vc.reset_node("absent"); // no-op, must not create + assert_eq!(vc.len(), 1); + } + + #[test] + fn gc_zero_nodes_removes_zeros_and_counts() { + let mut vc = VectorClock::new(); + vc.set("a", 0); + vc.set("b", 3); + vc.set("c", 0); + let removed = vc.gc_zero_nodes(); + assert_eq!(removed, 2); + assert_eq!(vc.len(), 1); + assert_eq!(vc.get("b"), 3); + } + + #[test] + fn retain_active_nodes_drops_the_rest() { + let mut vc = VectorClock::new(); + vc.set("a", 1); + vc.set("b", 2); + vc.set("c", 3); + let removed = vc.retain_active_nodes(&["a".to_string(), "c".to_string()]); + assert_eq!(removed, 1); + assert!(vc.nodes().iter().any(|n| *n == "a")); + assert!(vc.nodes().iter().any(|n| *n == "c")); + assert_eq!(vc.get("b"), 0); // gone + } + + // ---------------------------------------------------------------------- + // VectorClock: serialization round-trip (clocks travel over the wire) + // ---------------------------------------------------------------------- + + #[test] + fn serde_round_trip_preserves_clock() { + let mut vc = VectorClock::new(); + vc.set("n1", 7); + vc.set("n2", 3); + let json = serde_json::to_string(&vc).unwrap(); + let back: VectorClock = serde_json::from_str(&json).unwrap(); + assert_eq!(vc, back); + } + + // ---------------------------------------------------------------------- + // VLCSnapshot + // ---------------------------------------------------------------------- + + #[test] + fn snapshot_new_starts_at_logical_zero() { + let s = VLCSnapshot::new(); + assert_eq!(s.logical_time, 0); + assert!(s.vector_clock.is_empty()); + } + + #[test] + fn snapshot_default_matches_new_shape() { + let s = VLCSnapshot::default(); + assert_eq!(s.logical_time, 0); + assert!(s.vector_clock.is_empty()); + } + + #[test] + fn snapshot_new_with_clock_preserves_clock() { + let mut vc = VectorClock::new(); + vc.set("a", 5); + let s = VLCSnapshot::new_with_clock(vc.clone()); + assert_eq!(s.vector_clock, vc); + } + + #[test] + fn snapshot_for_node_seeds_node() { + let s = VLCSnapshot::for_node("a".to_string()); + assert_eq!(s.vector_clock.get("a"), 0); + assert_eq!(s.vector_clock.len(), 1); + } + + #[test] + fn snapshot_increment_bumps_logical_and_vector() { + let mut s = VLCSnapshot::new(); + s.increment("a"); + assert_eq!(s.logical_time, 1); + assert_eq!(s.vector_clock.get("a"), 1); + s.increment("a"); + assert_eq!(s.logical_time, 2); + assert_eq!(s.vector_clock.get("a"), 2); + } + + #[test] + fn snapshot_receive_merges_and_advances_logical_time() { + // local node "a", remote node "b" + let mut local = VLCSnapshot::for_node("a".to_string()); + local.increment("a"); // logical_time=1, a=1 + + let mut remote = VLCSnapshot::for_node("b".to_string()); + remote.increment("b"); + remote.increment("b"); // remote.logical_time=2, b=2 + + local.receive(&remote, "a"); + + // logical_time = max(1, 2) + 1 = 3 + assert_eq!(local.logical_time, 3); + // merged remote's b=2 + assert_eq!(local.vector_clock.get("b"), 2); + // local node "a" incremented after merge: was 1 -> 2 + assert_eq!(local.vector_clock.get("a"), 2); + } + + #[test] + fn snapshot_receive_establishes_causal_order() { + let mut sender = VLCSnapshot::for_node("s".to_string()); + sender.increment("s"); + let before_send = sender.clone(); + + let mut receiver = VLCSnapshot::for_node("r".to_string()); + receiver.receive(&sender, "r"); + + // The sender's pre-send state must causally precede the receiver's post-receive state. + assert!(before_send.happens_before(&receiver)); + assert!(!receiver.happens_before(&before_send)); + } + + #[test] + fn snapshot_happens_before_uses_logical_time_when_vectors_equal() { + let vc = VectorClock::new(); + let mut earlier = VLCSnapshot::new_with_clock(vc.clone()); + let mut later = VLCSnapshot::new_with_clock(vc); + earlier.logical_time = 1; + later.logical_time = 5; + // Equal vector clocks -> fall back to logical time comparison. + assert!(earlier.happens_before(&later)); + assert!(!later.happens_before(&earlier)); + } + + #[test] + fn snapshot_is_concurrent_delegates_to_vector_clock() { + let mut a = VLCSnapshot::for_node("a".to_string()); + a.increment("a"); + let mut b = VLCSnapshot::for_node("b".to_string()); + b.increment("b"); + assert!(a.is_concurrent(&b)); + assert!(b.is_concurrent(&a)); + } + + #[test] + fn snapshot_gc_inactive_nodes_prunes_vector_clock() { + let mut s = VLCSnapshot::new(); + s.increment("a"); + s.increment("b"); + s.increment("c"); + let removed = s.gc_inactive_nodes(&["a".to_string()]); + assert_eq!(removed, 2); + assert_eq!(s.vector_clock.len(), 1); + assert_eq!(s.vector_clock.get("a"), 1); + } + + #[test] + fn snapshot_serde_round_trip() { + let mut s = VLCSnapshot::for_node("a".to_string()); + s.increment("a"); + let json = serde_json::to_string(&s).unwrap(); + let back: VLCSnapshot = serde_json::from_str(&json).unwrap(); + assert_eq!(s, back); + } +}