use proptest::prelude::*;
use sz_orm_sharding::enhanced::{CompositeRouter, ConsistentHashRouter, ShardGroup};
proptest! {
#[test]
fn prop_consistent_hash_deterministic(
nodes in prop::collection::vec("[a-z][a-z0-9]{0,7}", 1..10),
vnodes in 1usize..200,
key in "[a-zA-Z0-9_:]{1,20}",
) {
let nodes_ref: Vec<&str> = nodes.iter().map(|s| s.as_str()).collect();
let router = ConsistentHashRouter::new(nodes_ref, vnodes);
let r1 = router.route(&key).unwrap();
let r2 = router.route(&key).unwrap();
let r3 = router.route(&key).unwrap();
prop_assert!(r1 == r2 && r2 == r3, "路由不一致: r1={:?} r2={:?} r3={:?}", r1, r2, r3);
prop_assert!(
nodes.contains(&r1),
"路由结果 {:?} 不在节点列表 {:?} 中",
r1,
nodes
);
}
#[test]
fn prop_consistent_hash_consistency_on_add(
existing_nodes in prop::collection::vec("[a-z]{1,5}", 2..8),
new_node in "[a-z]{1,5}",
keys in prop::collection::vec("[a-zA-Z0-9]{1,15}", 50..200),
) {
prop_assume!(!existing_nodes.contains(&new_node));
let nodes_ref: Vec<&str> = existing_nodes.iter().map(|s| s.as_str()).collect();
let router_before = ConsistentHashRouter::new(nodes_ref.clone(), 50);
let router_after = {
let mut r = ConsistentHashRouter::new(nodes_ref, 50);
r.add_node(&new_node);
r
};
for key in &keys {
let before = router_before.route(key).unwrap();
let after = router_after.route(key).unwrap();
prop_assert!(
after == before || after == new_node,
"key {:?} 路由从 {:?} 变为 {:?}(新节点 {:?}),违反一致性",
key,
before,
after,
new_node
);
}
}
#[test]
fn prop_ring_size_bounds(
node_count in 1usize..20,
vnodes in 1usize..100,
) {
let nodes: Vec<String> = (0..node_count).map(|i| format!("n{}", i)).collect();
let nodes_ref: Vec<&str> = nodes.iter().map(|s| s.as_str()).collect();
let router = ConsistentHashRouter::new(nodes_ref, vnodes);
let expected = node_count * vnodes;
let actual = router.ring_size();
prop_assert!(
actual <= expected,
"ring_size {} 超过预期 {}(nodes={} × vnodes={})",
actual,
expected,
node_count,
vnodes
);
prop_assert!(
actual >= node_count,
"ring_size {} 小于节点数 {}(至少每个节点一个虚拟节点)",
actual,
node_count
);
}
#[test]
fn prop_distribution_uniformity(
node_count in 3usize..8,
) {
let nodes: Vec<String> = (0..node_count).map(|i| format!("node{}", i)).collect();
let nodes_ref: Vec<&str> = nodes.iter().map(|s| s.as_str()).collect();
let router = ConsistentHashRouter::new(nodes_ref, 150);
let key_count = 5000;
let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for i in 0..key_count {
let key = format!("key_{}", i);
let node = router.route(&key).unwrap();
*counts.entry(node).or_insert(0) += 1;
}
let expected = key_count as f64 / node_count as f64;
let tolerance = expected * 0.5; for node in &nodes {
let count = *counts.get(node).unwrap_or(&0) as f64;
let diff = (count - expected).abs();
prop_assert!(
diff <= tolerance,
"节点 {:?} 负载 {:.0} 偏离期望 {:.1} 超过容差 ±{:.1}",
node,
count,
expected,
tolerance
);
}
}
#[test]
fn prop_composite_router_deterministic(
shards in prop::collection::vec("[a-z]{1,8}", 2..10),
key in "[a-zA-Z0-9]{1,20}",
) {
let shards_ref: Vec<&str> = shards.iter().map(|s| s.as_str()).collect();
let group = ShardGroup::new("g1", shards_ref);
let router = CompositeRouter::new().add_group(group);
let r1 = router.route("g1", &key).unwrap();
let r2 = router.route("g1", &key).unwrap();
prop_assert!(r1 == r2, "CompositeRouter 路由不一致: r1={:?} r2={:?}", r1, r2);
prop_assert!(
shards.contains(&r1),
"CompositeRouter 路由结果 {:?} 不在 shards {:?} 中",
r1,
shards
);
}
#[test]
fn prop_composite_router_cache_correctness(
shards in prop::collection::vec("[a-z]{1,8}", 2..10),
vnodes in 1usize..100,
keys in prop::collection::vec("[a-zA-Z0-9]{1,15}", 10..50),
) {
let shards_ref: Vec<&str> = shards.iter().map(|s| s.as_str()).collect();
let composite = CompositeRouter::new()
.with_vnodes(vnodes)
.add_group(ShardGroup::new("g", shards_ref.clone()));
let direct = ConsistentHashRouter::new(shards_ref, vnodes);
for key in &keys {
let r_composite = composite.route("g", key).unwrap();
let r_direct = direct.route(key).unwrap();
prop_assert!(
r_composite == r_direct,
"缓存路由结果 {:?} 与直接路由 {:?} 不一致(key={:?})",
r_composite,
r_direct,
key
);
}
}
}