dst
Generate deterministic simulation test scenarios for the current module
Deterministic Simulation Test Generator
You are generating a deterministic simulation test (DST) for a Rust module. DSTs control all sources of non-determinism so that failures are perfectly reproducible from a seed.
Steps
1. Read the target module
Read the file or module the user specified (the argument). If no argument was given, ask which module to target. Understand:
- What I/O dependencies does it have? (network calls, disk reads/writes, timers, random number generation)
- What are its public entry points?
- What invariants should always hold? (e.g., monotonic counters, balanced data structures, consistent replicas)
2. Identify traits needing deterministic implementations
Check which of these the module depends on, directly or transitively:
| Real dependency | Deterministic trait / replacement |
|---|---|
std::time::Instant / SystemTime | Clock trait with SimClock (manual advance) |
tokio::net / std::net | Network trait with SimNetwork (partitions, latency, reordering) |
std::fs / database clients | Storage trait with SimStorage (error injection, delayed flushes) |
rand::Rng | SimRng seeded from the test seed via StdRng::seed_from_u64 |
3. Generate the simulation test
Write a test file (or add a #[cfg(test)] module) with the following structure:
use rand::{Rng, SeedableRng, rngs::StdRng};
/// Fault injection configuration for deterministic simulation.
#[derive(Debug, Clone)]
struct FaultConfig {
/// Probability [0.0, 1.0] that a network call experiences a partition.
network_partition_probability: f64,
/// Probability [0.0, 1.0] that a disk write returns an I/O error.
disk_error_rate: f64,
/// Maximum clock skew injected per tick, in milliseconds.
clock_skew_range_ms: u64,
/// Total number of operations to simulate.
num_operations: u64,
}
impl Default for FaultConfig {
fn default() -> Self {
Self {
network_partition_probability: 0.05,
disk_error_rate: 0.01,
clock_skew_range_ms: 500,
num_operations: 10_000,
}
}
}
#[test]
fn dst_<module_name>() {
// Use a random seed, but print it so failures are reproducible.
let seed: u64 = rand::random();
eprintln!("DST seed: {seed}");
dst_<module_name>_with_seed(seed, FaultConfig::default());
}
fn dst_<module_name>_with_seed(seed: u64, fault_cfg: FaultConfig) {
let mut rng = StdRng::seed_from_u64(seed);
// -- Build deterministic environment --
// let sim_clock = SimClock::new();
// let sim_net = SimNetwork::new(&mut rng, fault_cfg.network_partition_probability);
// let sim_storage = SimStorage::new(&mut rng, fault_cfg.disk_error_rate);
// let mut system = TargetModule::new(sim_clock, sim_net, sim_storage);
for op_index in 0..fault_cfg.num_operations {
// -- Generate a random operation --
// let operation = Operation::random(&mut rng);
// -- Maybe inject a fault --
// if rng.gen_bool(fault_cfg.network_partition_probability) {
// sim_net.partition();
// }
// if rng.gen_bool(fault_cfg.disk_error_rate) {
// sim_storage.fail_next_write();
// }
// sim_clock.advance_by(Duration::from_millis(rng.gen_range(0..=fault_cfg.clock_skew_range_ms)));
// -- Execute --
// let result = system.apply(operation);
// -- Assert invariants after every operation --
// assert!(system.check_invariants(), "Invariant violated at op {op_index}, seed {seed}");
}
}
4. Provide guidance to the user
After generating the test scaffold:
- Point out which traits they need to implement or extract (e.g., "Your module calls
TcpStream::connectdirectly -- you need to extract aNetworktrait so the sim can intercept it"). - Suggest starting with
num_operations: 100and scaling up once the basic harness passes. - Remind them: to reproduce a failure, re-run with the printed seed:
dst_<module_name>_with_seed(FAILING_SEED, FaultConfig::default()). - Recommend adding a
#[test] fn dst_regression_<issue>()for each seed that caught a real bug, so it becomes a permanent regression test.