From 9c080d623f2cf2d31c9aadb5107b4a9ee7847ebc Mon Sep 17 00:00:00 2001 From: tv Date: Tue, 3 Nov 2026 07:29:19 +0100 Subject: add tree tests --- src/simtest/reach.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) (limited to 'src/simtest') diff --git a/src/simtest/reach.rs b/src/simtest/reach.rs index f501aef..eca0a47 100644 --- a/src/simtest/reach.rs +++ b/src/simtest/reach.rs @@ -1,5 +1,6 @@ -//! Whom neighbours reach (`swarm::reach`): told in STATUSes, as measured; and the -//! writers' trees built on it (`swarm::tree`). +//! Whom neighbours reach (`swarm::reach`): told in STATUSes, as measured, and kept for +//! the session; and the writers' trees built on it (`swarm::tree`), which changes go +//! along however they came. use std::time::Duration; @@ -83,3 +84,109 @@ async fn writer_trees_go_around_links_down() { ) .await; } + +/// What `node` counted: pulls started, changes passed on along writers' trees. +fn counted(node: &AppState) -> (u64, u64) { + let relay = node.stats.snapshot().relay; + (relay.pulls, relay.tree_forwards) +} + +/// x, b, c and d, one full mesh, but nothing passes between c and x, nor between c and +/// d: c's parent in x's tree is b, the only member it reaches. And x's sync messages to +/// b are lost, so b gets x's write pulled from d (after d's slow round), not along the +/// tree: pulled, it says nothing of whose; its author does, so b passes it on to c along +/// x's tree all the same, and c pulls nothing. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn pulled_changes_go_along_their_writers_trees() { + // Long, so x's own STATUS (every third keepalive) doesn't make b pull from x, whose + // answers are lost, before d's slow round. + let slow = |config: &mut crate::node::NodeConfig| { + config.timing = crate::state::Timing::new( + Duration::from_secs(5), + super::SYNC_REPLY_TIMEOUT, + crate::transport::sim::DELIVERY_LIMIT, + ); + }; + let sim = Sim::with_identity(68); + let cut = crate::transport::sim::LinkParams { + loss: 1.0, + ..Default::default() + }; + for (from, to) in [("x", "c"), ("c", "x"), ("d", "c"), ("c", "d")] { + sim.net.set_link(from, to, cut.clone()); + } + sim.net.set_link( + "x", + "b", + crate::transport::sim::LinkParams { + drop: vec![crate::transport::MessageKind::Sync], + ..Default::default() + }, + ); + let b = sim.start_on_with("b", &["sim"], &[], Start::Genesis, slow); + let c = sim.start_on_with("c", &["sim"], &["b"], Start::Join, slow); + let d = sim.start_on_with("d", &["sim"], &["b"], Start::Join, slow); + let x = sim.start_on_with("x", &["sim"], &["b"], Start::Join, slow); + sim.wait_ready(&[&b, &c, &d, &x], SOON).await; + sim.wait_converged(&[&b, &c, &d, &x], SOON * 2).await; + let (b, c, x) = (&b, &c, &x); + let writer = topology::pair_key(x.local_id()); + sim.wait_until("c under b in x's tree", SOON, move || async move { + tree::children(b, writer).await == [c.local_id().to_owned()] + }) + .await; + + let (c_pulls, _) = counted(c); + let (_, b_forwards) = counted(b); + super::write(x, "y", serde_json::json!(1)).await; + sim.wait_until("c has x's write", SOON, move || async move { + super::read(c, "y").await == Some(serde_json::json!(1)) + }) + .await; + assert!(counted(b).1 > b_forwards, "b passed it on along x's tree"); + assert_eq!(counted(c).0, c_pulls, "c pulled nothing"); +} + +/// x, b and c, nothing between x and c: x's changes reach c through b, along x's tree. +/// x writes every 100 ms for 4 s, many keepalive intervals: the pairs are busy, so no +/// keepalive goes, and no STATUS. b still knows whom c reaches (it said so once, and +/// nothing changed), and passes every write on to c: c pulls none. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn trees_hold_through_continuous_writes() { + let sim = Sim::with_identity(69); + let cut = crate::transport::sim::LinkParams { + loss: 1.0, + ..Default::default() + }; + sim.net.set_link("x", "c", cut.clone()); + sim.net.set_link("c", "x", cut); + let b = sim.start_on("b", &["sim"], &[], Start::Genesis); + let c = sim.start_on("c", &["sim"], &["b"], Start::Join); + let x = sim.start_on("x", &["sim"], &["b"], Start::Join); + sim.wait_ready(&[&b, &c, &x], SOON).await; + sim.wait_converged(&[&b, &c, &x], SOON).await; + let (b, c, x) = (&b, &c, &x); + let writer = topology::pair_key(x.local_id()); + sim.wait_until("c under b in x's tree", SOON, move || async move { + heard(b, c, b).await && tree::children(b, writer).await == [c.local_id().to_owned()] + }) + .await; + + let (c_pulls, _) = counted(c); + let (_, b_forwards) = counted(b); + let writes = 40; + for i in 0..writes { + super::write(x, &format!("w/{i}"), serde_json::json!(i)).await; + tokio::time::sleep(Duration::from_millis(100)).await; + } + sim.wait_until("c has x's last write", SOON, move || async move { + super::read(c, &format!("w/{}", writes - 1)).await == Some(serde_json::json!(writes - 1)) + }) + .await; + // Some may go together, if b had several to pass on at once. + assert!( + counted(b).1 >= b_forwards + writes as u64 / 2, + "b passed the writes on along x's tree" + ); + assert_eq!(counted(c).0, c_pulls, "c pulled nothing"); +} -- cgit v1.3.1