#![cfg(feature = "serde")]
use simulacra::{
Duration, NetConfig, NetEvent, NodeId, TopologyBuilder, TracedNetwork, UniformJitter,
};
fn run_scenario(seed: u64) -> String {
let node_count = 20;
let topology = TopologyBuilder::new(node_count)
.ring(Duration::from_millis(10))
.build();
let latency_model = UniformJitter::new(Duration::from_millis(3));
let mut net = TracedNetwork::<u64, _>::with_latency_model(topology, latency_model, seed);
let _ = NetConfig::default();
for i in 0..node_count as u32 {
let dst = (i + 2) % node_count as u32;
let _ = net.send(NodeId(i), NodeId(dst), i as u64);
}
for i in 0..node_count as u32 {
let dst = (i + 5) % node_count as u32;
let _ = net.send(NodeId(i), NodeId(dst), (i as u64) + 1000);
}
let (_stats, trace) = net.run_traced(|_ctx, _event| {});
trace
.to_json()
.expect("trace should serialize to JSON cleanly")
}
#[test]
fn ring_gossip_scenario_is_byte_equal_across_runs() {
let a = run_scenario(0xDEADBEEF);
let b = run_scenario(0xDEADBEEF);
assert_eq!(
a, b,
"two runs with the same seed produced different JSON traces; \
determinism has regressed"
);
}
#[test]
fn ring_gossip_scenario_differs_across_seeds() {
let a = run_scenario(1);
let b = run_scenario(2);
assert_ne!(
a, b,
"two runs with different seeds produced identical traces; \
the RNG is probably not being consulted"
);
}
fn run_link_failure_scenario(seed: u64) -> String {
let topology = TopologyBuilder::new(3)
.link(0u32, 1u32, Duration::from_millis(5))
.link(1u32, 2u32, Duration::from_millis(5))
.link(0u32, 2u32, Duration::from_millis(50))
.build();
let latency_model = UniformJitter::new(Duration::from_millis(1));
let mut net = TracedNetwork::<u64, _>::with_latency_model(topology, latency_model, seed);
for (i, t) in [(1u64, 0), (2, 30), (3, 60), (4, 90)].iter() {
let _ = net.send_at(simulacra::Time::from_millis(*t), NodeId(0), NodeId(0), *i);
}
let (_stats, trace) = net.run_traced(|ctx, event| {
if let NetEvent::Deliver(msg) = event
&& msg.dst == NodeId(0)
{
match msg.payload {
1 => {
ctx.send(NodeId(0), NodeId(2), 100);
}
2 => {
ctx.fail_link(NodeId(0), NodeId(1));
ctx.send(NodeId(0), NodeId(2), 200);
}
3 => {
ctx.fail_link(NodeId(0), NodeId(2));
ctx.send(NodeId(0), NodeId(2), 300);
}
4 => {
ctx.heal_link(NodeId(0), NodeId(1));
ctx.heal_link(NodeId(0), NodeId(2));
ctx.send(NodeId(0), NodeId(2), 400);
}
_ => {}
}
}
});
trace
.to_json()
.expect("trace should serialize to JSON cleanly")
}
#[test]
fn link_failure_scenario_is_byte_equal_across_runs() {
let a = run_link_failure_scenario(0xC0FFEE);
let b = run_link_failure_scenario(0xC0FFEE);
assert_eq!(
a, b,
"two runs with the same seed and fail/heal sequence produced \
different JSON traces; link-failure determinism has regressed"
);
}
fn run_node_failure_scenario(seed: u64) -> String {
let topology = TopologyBuilder::new(4)
.link(0u32, 1u32, Duration::from_millis(5))
.link(1u32, 3u32, Duration::from_millis(5))
.link(0u32, 2u32, Duration::from_millis(50))
.link(2u32, 3u32, Duration::from_millis(50))
.build();
let latency_model = UniformJitter::new(Duration::from_millis(1));
let mut net = TracedNetwork::<u64, _>::with_latency_model(topology, latency_model, seed);
for (tag, t) in [(1u64, 0u64), (2, 200), (3, 400), (4, 600)].iter() {
let _ = net.send_at(simulacra::Time::from_millis(*t), NodeId(0), NodeId(0), *tag);
}
let (_stats, trace) = net.run_traced(|ctx, event| {
if let NetEvent::Deliver(msg) = event
&& msg.dst == NodeId(0)
{
match msg.payload {
1 => {
ctx.send(NodeId(0), NodeId(3), 100);
}
2 => {
ctx.fail_node(NodeId(1));
ctx.send(NodeId(0), NodeId(3), 200);
}
3 => {
ctx.fail_node(NodeId(2));
ctx.send(NodeId(0), NodeId(3), 300);
}
4 => {
ctx.heal_node(NodeId(1));
ctx.send(NodeId(0), NodeId(3), 400);
}
_ => {}
}
}
});
trace
.to_json()
.expect("trace should serialize to JSON cleanly")
}
#[test]
fn node_failure_scenario_is_byte_equal_across_runs() {
let a = run_node_failure_scenario(0xBEEF);
let b = run_node_failure_scenario(0xBEEF);
assert_eq!(
a, b,
"two runs with the same seed and node fail/heal sequence produced \
different JSON traces; node-failure determinism has regressed"
);
}
#[test]
fn node_failure_scenario_includes_reroute_and_noroute() {
let trace = run_node_failure_scenario(0xBEEF);
assert!(
trace.contains("\"NoRoute\""),
"expected at least one NoRoute drop in the trace"
);
let delivered = trace.matches("\"Delivered\"").count();
assert!(
delivered >= 3,
"expected at least 3 deliveries (ticks + at least one app message); \
got {delivered}"
);
}
#[test]
fn link_failure_scenario_includes_reroute_and_noroute() {
let trace = run_link_failure_scenario(0xC0FFEE);
assert!(
trace.contains("\"NoRoute\""),
"expected at least one NoRoute drop in the trace"
);
let delivered = trace.matches("\"Delivered\"").count();
assert!(
delivered >= 3,
"expected at least 3 deliveries (ticks + at least one app message); \
got {delivered}"
);
}
fn run_drop_in_flight_scenario(seed: u64) -> String {
use simulacra::NetConfig;
let topology = TopologyBuilder::new(3)
.link(0u32, 1u32, Duration::from_millis(50))
.link(1u32, 2u32, Duration::from_millis(50))
.build();
let latency_model = UniformJitter::new(Duration::from_millis(2));
let mut net = TracedNetwork::<u64, _>::with_latency_model(topology, latency_model, seed)
.with_config(NetConfig {
drop_in_flight_on_failure: true,
..Default::default()
});
for i in 0..5u64 {
let _ = net.send_at(simulacra::Time::from_millis(i), NodeId(0), NodeId(2), i);
}
let _ = net.send_at(simulacra::Time::from_millis(20), NodeId(0), NodeId(0), 999);
let (_stats, trace) = net.run_traced(|ctx, event| {
if let NetEvent::Deliver(msg) = event
&& msg.payload == 999
{
ctx.partition(NodeId(0), NodeId(2));
}
});
trace
.to_json()
.expect("trace should serialize to JSON cleanly")
}
#[test]
fn drop_in_flight_scenario_is_byte_equal_across_runs() {
let a = run_drop_in_flight_scenario(0xFEED);
let b = run_drop_in_flight_scenario(0xFEED);
assert_eq!(
a, b,
"two runs with the same seed and partition sequence produced \
different JSON traces"
);
}
#[test]
fn drop_in_flight_scenario_actually_drops_messages() {
let trace = run_drop_in_flight_scenario(0xFEED);
assert_eq!(trace.matches("\"Partitioned\"").count(), 5);
assert_eq!(trace.matches("\"Delivered\"").count(), 1);
}
#[test]
fn ring_gossip_scenario_matches_pinned_shape() {
let trace = run_scenario(0xDEADBEEF);
assert!(
trace.contains("\"seed\":3735928559"),
"seed not found in trace JSON: {}",
&trace[..trace.len().min(200)]
);
let delivered = trace.matches("\"Delivered\"").count();
assert_eq!(
delivered, 40,
"expected 40 Delivered events; got {}",
delivered
);
}