diff --git a/src/hasher.rs b/src/hasher.rs index 32cbffb..f917db0 100644 --- a/src/hasher.rs +++ b/src/hasher.rs @@ -112,4 +112,67 @@ mod tests { ); } } + + #[test] + fn test_ring_stability_on_add() { + // Adding a 4th volume to a 3-volume ring should only move ~25% of keys + let volumes: Vec = (1..=3).map(|i| format!("http://vol{i}")).collect(); + let ring_before = Ring::new(&volumes, 100); + + let mut volumes4 = volumes.clone(); + volumes4.push("http://vol4".into()); + let ring_after = Ring::new(&volumes4, 100); + + let total = 10000; + let mut moved = 0; + for i in 0..total { + let key = format!("key-{i}"); + let before = &ring_before.get_volumes(&key, 1)[0]; + let after = &ring_after.get_volumes(&key, 1)[0]; + if before != after { + moved += 1; + } + } + + // Ideal: 1/4 of keys move (25%). Allow 15%-40%. + let pct = moved as f64 / total as f64 * 100.0; + assert!( + pct > 15.0 && pct < 40.0, + "expected ~25% of keys to move, got {pct:.1}% ({moved}/{total})" + ); + } + + #[test] + fn test_ring_stability_on_remove() { + // Removing 1 volume from a 4-volume ring should only move ~25% of keys + let volumes: Vec = (1..=4).map(|i| format!("http://vol{i}")).collect(); + let ring_before = Ring::new(&volumes, 100); + + let volumes3: Vec = (1..=3).map(|i| format!("http://vol{i}")).collect(); + let ring_after = Ring::new(&volumes3, 100); + + let total = 10000; + let mut moved = 0; + for i in 0..total { + let key = format!("key-{i}"); + let before = &ring_before.get_volumes(&key, 1)[0]; + let after = &ring_after.get_volumes(&key, 1)[0]; + if before != after { + moved += 1; + } + } + + // Ideal: 1/4 of keys move (25%). Allow 15%-40%. + let pct = moved as f64 / total as f64 * 100.0; + assert!( + pct > 15.0 && pct < 40.0, + "expected ~25% of keys to move, got {pct:.1}% ({moved}/{total})" + ); + } + + #[test] + fn test_ring_empty() { + let ring = Ring::new(&[], 100); + assert_eq!(ring.get_volumes("key", 1), Vec::::new()); + } }