Files
tines/tines-sim/tests/convergence.rs
T

238 lines
6.4 KiB
Rust

//! Deterministic convergence scenarios for the `Sim` harness.
//!
//! Every scenario is built from a `WorldConfig` + `ContactTrace`, then stepped
//! with `Sim` and asserted against. Because loss/dup/seed are seeded and the
//! contact graph is fixed, the outcomes below are reproducible run to run.
use tines_core::LatestWins;
use tines_sim::{Contact, ContactTrace, Sim, WorldConfig};
/// Build the world for test 1: two nodes, a single `[0, 5000)` contact.
#[test]
fn two_nodes_one_contact_converge() {
let cfg = WorldConfig {
nodes: 2,
mtu: 1500,
loss: 0.0,
dup: 0.0,
seed: 0,
};
let mut trace = ContactTrace::default();
trace.contacts.push(Contact {
start_ms: 0,
end_ms: 5000,
a: 0,
b: 1,
});
let mut sim = Sim::new(cfg, trace);
for i in 0..10 {
sim.observe(0, format!("n0-{}", i).as_bytes(), format!("p{}", i).as_bytes());
}
for i in 0..10 {
sim.observe(1, format!("n1-{}", i).as_bytes(), format!("p{}", i).as_bytes());
}
sim.run_until(5000);
assert!(sim.converged(&[0, 1]), "both nodes must hold every observation");
assert_eq!(sim.id_set(0).len(), 20, "each node stores 10 + 10 obs");
assert_eq!(sim.id_set(1).len(), 20);
}
/// Test 2: a node two hops away must still have the first node's observations
/// even though the two never met directly.
#[test]
fn relay_through_middle_node() {
let cfg = WorldConfig {
nodes: 3,
mtu: 1500,
loss: 0.0,
dup: 0.0,
seed: 0,
};
let mut trace = ContactTrace::default();
trace.contacts.push(Contact {
start_ms: 0,
end_ms: 3000,
a: 0,
b: 1,
});
trace.contacts.push(Contact {
start_ms: 4000,
end_ms: 7000,
a: 1,
b: 2,
});
let mut sim = Sim::new(cfg, trace);
for i in 0..5 {
sim.observe(0, format!("a-{}", i).as_bytes(), format!("a-p{}", i).as_bytes());
}
sim.run_until(7000);
let from_0 = sim.id_set(0);
let node_2 = sim.id_set(2);
// 0 and 2 never shared a contact, yet 2 received everything through 1.
for id in &from_0 {
assert!(node_2.contains(id), "node 2 is missing node 0's observation {id:?}");
}
assert_eq!(from_0.len(), 5, "node 0 observed 5 things");
}
/// Test 3: two isolated components each converge internally but the whole graph
/// does not, and the components are reported exactly.
#[test]
fn disconnected_component_does_not_converge() {
let cfg = WorldConfig {
nodes: 4,
mtu: 1500,
loss: 0.0,
dup: 0.0,
seed: 0,
};
let mut trace = ContactTrace::default();
trace.contacts.push(Contact {
start_ms: 0,
end_ms: 5000,
a: 0,
b: 1,
});
trace.contacts.push(Contact {
start_ms: 0,
end_ms: 5000,
a: 2,
b: 3,
});
let mut sim = Sim::new(cfg, trace);
sim.observe(0, b"comp-A", b"value-A");
sim.observe(2, b"comp-B", b"value-B");
sim.run_until(5000);
assert_eq!(
sim.components(),
vec![vec![0, 1], vec![2, 3]],
"the contact graph splits into two pairs"
);
assert!(!sim.converged(&[0, 1, 2, 3]), "cross-component never converges");
assert!(sim.converged(&[0, 1]), "component A converges");
assert!(sim.converged(&[2, 3]), "component B converges");
}
/// Test 4: a lossy, duplicate-prone link still converges given enough time.
#[test]
fn lossy_link_still_converges_given_time() {
let cfg = WorldConfig {
nodes: 2,
mtu: 1500,
loss: 0.3,
dup: 0.1,
seed: 7,
};
let mut trace = ContactTrace::default();
trace.contacts.push(Contact {
start_ms: 0,
end_ms: 60_000,
a: 0,
b: 1,
});
let mut sim = Sim::new(cfg, trace);
for i in 0..50 {
sim.observe(0, format!("l0-{}", i).as_bytes(), format!("l0-p{}", i).as_bytes());
sim.observe(1, format!("l1-{}", i).as_bytes(), format!("l1-p{}", i).as_bytes());
}
sim.run_until(60_000);
assert!(
sim.converged(&[0, 1]),
"a lossy link converges once the summary-driven retransmits drain the queue"
);
assert_eq!(sim.id_set(0).len(), 100);
assert_eq!(sim.id_set(1).len(), 100);
}
/// Test 5: the belief age tracks true elapsed wall-clock time across two hops.
///
/// The observation is made at `t=1000` and the contact graph is
/// `[0,3000)` between 0-1 and `[4000,7000)` between 1-2, so by `t=7000` node 2
/// should believe `x` is roughly `7000 - 1000 = 6000` ms old.
#[test]
fn age_tracks_true_elapsed_across_hops() {
let cfg = WorldConfig {
nodes: 3,
mtu: 1500,
loss: 0.0,
dup: 0.0,
seed: 0,
};
let mut trace = ContactTrace::default();
trace.contacts.push(Contact {
start_ms: 0,
end_ms: 3000,
a: 0,
b: 1,
});
trace.contacts.push(Contact {
start_ms: 4000,
end_ms: 7000,
a: 1,
b: 2,
});
let mut sim = Sim::new(cfg, trace);
sim.run_until(1000);
sim.observe(0, b"x", b"hello");
sim.run_until(7000);
let belief = sim
.nodes
.get(2)
.and_then(|n| n.belief(b"x", &LatestWins, 7000))
.expect("node 2 should hold a belief for \"x\"");
let age = belief.age_ms;
assert!(
(age as i64 - 6000).abs() <= 20,
"age {age} must be within +/-{} ms of 6000 (true elapsed across two hops)",
2 * sim.step_ms
);
}
/// Test 6: payloads larger than the MTU are fragmented across frames and still
/// converge.
#[test]
fn large_payloads_fragment_over_small_mtu() {
let cfg = WorldConfig {
nodes: 2,
mtu: 256,
loss: 0.0,
dup: 0.0,
seed: 9,
};
let mut trace = ContactTrace::default();
trace.contacts.push(Contact {
start_ms: 0,
end_ms: 20_000,
a: 0,
b: 1,
});
let mut sim = Sim::new(cfg, trace);
for i in 0..20 {
let topic0 = format!("n0-{}", i);
let topic1 = format!("n1-{}", i);
let payload = vec![0xABu8; 1024];
sim.observe(0, topic0.as_bytes(), &payload);
sim.observe(1, topic1.as_bytes(), &payload);
}
sim.run_until(20_000);
assert!(
sim.converged(&[0, 1]),
"1 KiB payloads fragmented over a 256-byte MTU still converge"
);
assert_eq!(sim.id_set(0).len(), 40);
assert_eq!(sim.id_set(1).len(), 40);
}