2026-03-24 13:40:35 +00:00

109 lines
2.5 KiB
Rust

//! Test-only utilities for deterministic behavior in unit tests.
//!
//! This crate is intended to be used as a dev-dependency only. It provides:
//! - Seeded RNG helpers for reproducible randomness in tests.
//! - A tiny deterministic clock abstraction for tests that need to reason about time.
use rand::{RngCore, SeedableRng, rngs::StdRng};
/// Builds a `StdRng` from a u64 seed in a deterministic way.
///
/// This expands the u64 seed into a 32-byte array (little-endian repeated)
/// to initialize `StdRng`.
pub fn rng_from_seed(seed: u64) -> StdRng {
let le = seed.to_le_bytes();
let mut buf = [0u8; 32];
// Repeat the 8-byte seed 4 times to fill 32 bytes
for i in 0..4 {
buf[i * 8..(i + 1) * 8].copy_from_slice(&le);
}
StdRng::from_seed(buf)
}
/// Convenience helper that returns a RNG with a fixed well-known seed.
pub fn deterministic_rng() -> StdRng {
rng_from_seed(0xC0FFEE_5EED)
}
/// Returns the next u64 from the provided RNG.
pub fn next_u64(rng: &mut StdRng) -> u64 {
rng.next_u64()
}
/// Collects `n` u64 values from the RNG.
pub fn take_n_u64(rng: &mut StdRng, n: usize) -> Vec<u64> {
let mut out = Vec::with_capacity(n);
for _ in 0..n {
out.push(rng.next_u64());
}
out
}
/// Simple deterministic clock abstraction for tests.
pub trait Clock {
fn now_millis(&self) -> u64;
}
/// A clock that always returns a fixed instant.
pub struct FixedClock {
now: u64,
}
impl FixedClock {
pub fn new(now: u64) -> Self {
Self { now }
}
}
impl Clock for FixedClock {
fn now_millis(&self) -> u64 {
self.now
}
}
/// A clock that advances by a constant step each tick.
pub struct TickingClock {
now: u64,
step: u64,
}
impl TickingClock {
pub fn new(start: u64, step: u64) -> Self {
Self { now: start, step }
}
pub fn tick(&mut self) {
self.now = self.now.saturating_add(self.step);
}
}
impl Clock for TickingClock {
fn now_millis(&self) -> u64 {
self.now
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rng_reproducible_sequences() {
let mut a = rng_from_seed(42);
let mut b = rng_from_seed(42);
for _ in 0..10 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn ticking_clock_advances_deterministically() {
let mut c = TickingClock::new(1000, 16);
assert_eq!(c.now_millis(), 1000);
c.tick();
assert_eq!(c.now_millis(), 1016);
c.tick();
assert_eq!(c.now_millis(), 1032);
}
}