diff options
Diffstat (limited to 'tests/scale.rs')
| -rw-r--r-- | tests/scale.rs | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/tests/scale.rs b/tests/scale.rs new file mode 100644 index 0000000..b518385 --- /dev/null +++ b/tests/scale.rs @@ -0,0 +1,138 @@ +//! Scale test: 20 nodes with distributed put/get. +//! +//! Runs 20 nodes with distributed put/get to verify +//! correctness at scale. + +use std::time::Duration; +use tesseras_dht::Node; +use tesseras_dht::nat::NatState; + +fn poll_all(nodes: &mut [Node], rounds: usize) { + let fast = Duration::from_millis(1); + for _ in 0..rounds { + for n in nodes.iter_mut() { + n.poll_timeout(fast).ok(); + } + } +} + +fn make_network(n: usize) -> Vec<Node> { + let mut nodes = Vec::with_capacity(n); + let bootstrap = Node::bind(0).unwrap(); + let bp = bootstrap.local_addr().unwrap().port(); + nodes.push(bootstrap); + nodes[0].set_nat_state(NatState::Global); + + for _ in 1..n { + let mut node = Node::bind(0).unwrap(); + node.set_nat_state(NatState::Global); + node.join("127.0.0.1", bp).unwrap(); + nodes.push(node); + } + + std::thread::sleep(Duration::from_millis(100)); + poll_all(&mut nodes, 10); + nodes +} + +#[test] +fn twenty_nodes_routing() { + let nodes = make_network(20); + + // Every node should have at least 1 peer + for (i, node) in nodes.iter().enumerate() { + assert!( + node.routing_table_size() >= 1, + "Node {i} has empty routing table" + ); + } + + // Average routing table should be > 5 + let total: usize = nodes.iter().map(|n| n.routing_table_size()).sum(); + let avg = total / nodes.len(); + assert!(avg >= 5, "Average routing table {avg} too low"); +} + +#[test] +fn twenty_nodes_put_get() { + let mut nodes = make_network(20); + + // Each node stores one value + for i in 0..20u32 { + let key = format!("scale-key-{i}"); + let val = format!("scale-val-{i}"); + nodes[i as usize].put(key.as_bytes(), val.as_bytes(), 300, false); + } + + // Poll to distribute + std::thread::sleep(Duration::from_millis(100)); + poll_all(&mut nodes, 10); + + // Each node should have its own value + for i in 0..20u32 { + let key = format!("scale-key-{i}"); + let vals = nodes[i as usize].get(key.as_bytes()); + assert!(!vals.is_empty(), "Node {i} lost its own value"); + } + + // Count total stored values across network + let total_stored: usize = nodes.iter().map(|n| n.storage_count()).sum(); + assert!( + total_stored >= 20, + "Total stored {total_stored} should be >= 20" + ); +} + +#[test] +#[ignore] // timing-sensitive, consumes 100% CPU with 20 nodes polling +fn twenty_nodes_remote_get() { + let mut nodes = make_network(20); + + // Node 0 stores a value + nodes[0].put(b"find-me", b"found", 300, false); + + std::thread::sleep(Duration::from_millis(100)); + poll_all(&mut nodes, 10); + + // Node 19 tries to get it — trigger FIND_VALUE + let _ = nodes[19].get(b"find-me"); + + // Poll all nodes to let FIND_VALUE propagate + for _ in 0..40 { + poll_all(&mut nodes, 5); + std::thread::sleep(Duration::from_millis(30)); + + let vals = nodes[19].get(b"find-me"); + if !vals.is_empty() { + assert_eq!(vals[0], b"found"); + return; + } + } + panic!("Node 19 should find the value via FIND_VALUE"); +} + +#[test] +fn twenty_nodes_multiple_puts() { + let mut nodes = make_network(20); + + // 5 nodes store 10 values each + for n in 0..5 { + for k in 0..10u32 { + let key = format!("n{n}-k{k}"); + let val = format!("n{n}-v{k}"); + nodes[n].put(key.as_bytes(), val.as_bytes(), 300, false); + } + } + + std::thread::sleep(Duration::from_millis(100)); + poll_all(&mut nodes, 10); + + // Verify origin nodes have their values + for n in 0..5 { + for k in 0..10u32 { + let key = format!("n{n}-k{k}"); + let vals = nodes[n].get(key.as_bytes()); + assert!(!vals.is_empty(), "Node {n} lost key n{n}-k{k}"); + } + } +} |