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 dependencyDeterministic trait / replacement
std::time::Instant / SystemTimeClock trait with SimClock (manual advance)
tokio::net / std::netNetwork trait with SimNetwork (partitions, latency, reordering)
std::fs / database clientsStorage trait with SimStorage (error injection, delayed flushes)
rand::RngSimRng 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::connect directly -- you need to extract a Network trait so the sim can intercept it").
  • Suggest starting with num_operations: 100 and 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.