From ad1699b0259f79741a50b60eb4cb24d2b7dcb004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C5=ABdolfs=20O=C5=A1i=C5=86=C5=A1?= Date: Mon, 26 Oct 2026 11:30:38 +0100 Subject: [PATCH] node/test: Make simulations reproducible A simulation that fails under a random seed can only be investigated if it can be run again with the same seed. Replaying from a seed is also a first step towards the deterministic simulation testing proposed [on Zulip][dst], and one that works with the simulator as it is. The simulator seeds its generator randomly and only logs the seed, so there is no way to run a simulation with it again. Even with a fixed seed, runs would not reliably replay, because partitions were decided on the absolute time, and simulations start at `LocalTime::now()`. Seed simulations from `RAD_SIM_SEED`, mention it next to the logged seed, and decide partitions by the elapsed time. A fixed seed in every test would also make runs reproducible, but would give up the variation that random seeds provide. Message timestamps still differ between runs of tests that start at `LocalTime::now()`, and tests that draw from their own generator, such as `announcement_message_amplification`, are not reproducible from the seed alone yet. [dst]: --- crates/radicle-node/src/test/simulator.rs | 29 +++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/radicle-node/src/test/simulator.rs b/crates/radicle-node/src/test/simulator.rs index 9cf72594a..b3e4f30a4 100644 --- a/crates/radicle-node/src/test/simulator.rs +++ b/crates/radicle-node/src/test/simulator.rs @@ -6,7 +6,7 @@ use std::marker::PhantomData; use std::ops::Range; use std::rc::Rc; use std::sync::Arc; -use std::{fmt, io, net}; +use std::{env, fmt, io, net}; use localtime::{LocalDuration, LocalTime}; use log::*; @@ -190,13 +190,29 @@ impl Simulation where S: WriteStorage + 'static, { - /// Create a new simulation. + /// Environment variable that sets the seed of new simulations. + const SEED_VAR: &str = "RAD_SIM_SEED"; + + /// Create a new simulation, seeded from [`Self::SEED_VAR`] if it is set, + /// and randomly otherwise. pub fn new(time: LocalTime, opts: Options) -> Self { - let rng = fastrand::Rng::new(); + let seed = env::var(Self::SEED_VAR) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or_else(|| fastrand::u64(..)); // Log the seed so that we can reproduce the simulation if needed. - log::info!(target: "sim", "Seed: {}", rng.get_seed()); + log::info!( + target: "sim", + "Simulation seed is {seed}, set {}={seed} to reproduce.", + Self::SEED_VAR + ); + Self::with_seed(time, seed, opts) + } + + /// Create a new simulation with the given seed. + pub fn with_seed(time: LocalTime, seed: u64, opts: Options) -> Self { Self { inbox: Inbox { messages: BTreeMap::new(), @@ -211,7 +227,7 @@ where opts, start_time: time, time, - rng: RefCell::new(rng), + rng: RefCell::new(fastrand::Rng::with_seed(seed)), storage: PhantomData, } } @@ -222,7 +238,6 @@ where } /// Total amount of simulated time elapsed. - #[allow(dead_code)] pub fn elapsed(&self) -> LocalDuration { self.time - self.start_time } @@ -292,7 +307,7 @@ where // between individual nodes. We need to think about more realistic // scenarios. We should also think about creating various network // topologies. - if self.time.as_secs().is_multiple_of(10) { + if self.elapsed().as_secs().is_multiple_of(10) { for (i, x) in nodes.keys().enumerate() { for y in nodes.keys().skip(i + 1) { if self.is_fallible() {