Skip to main content

presentar_terminal/
seed.rs

1//! Random Seed Management for Reproducible Testing
2//!
3//! This module provides deterministic random number generation for testing.
4//! All random operations should use seeds from this module to ensure reproducibility.
5//!
6//! # Environment Variables
7//!
8//! - `PRESENTAR_TEST_SEED`: Main test seed (default: 42)
9//! - `PROPTEST_SEED`: PropTest seed (default: 0xdeadbeef)
10//! - `PRESENTAR_BENCH_SEED`: Benchmark seed (default: 12345)
11//!
12//! # Example
13//!
14//! ```rust
15//! use presentar_terminal::seed::{get_test_seed, deterministic_rng};
16//!
17//! let seed = get_test_seed();
18//! let mut rng = deterministic_rng();
19//! ```
20
21use std::sync::OnceLock;
22
23/// Default test seed for reproducibility
24pub const DEFAULT_TEST_SEED: u64 = 42;
25
26/// Default PropTest seed
27pub const DEFAULT_PROPTEST_SEED: u64 = 0xdeadbeef;
28
29/// Default benchmark seed
30pub const DEFAULT_BENCH_SEED: u64 = 12345;
31
32static TEST_SEED: OnceLock<u64> = OnceLock::new();
33static BENCH_SEED: OnceLock<u64> = OnceLock::new();
34
35/// Get the test seed from environment or use default
36pub fn get_test_seed() -> u64 {
37    *TEST_SEED.get_or_init(|| {
38        std::env::var("PRESENTAR_TEST_SEED")
39            .ok()
40            .and_then(|s| s.parse().ok())
41            .unwrap_or(DEFAULT_TEST_SEED)
42    })
43}
44
45/// Get the benchmark seed from environment or use default
46pub fn get_bench_seed() -> u64 {
47    *BENCH_SEED.get_or_init(|| {
48        std::env::var("PRESENTAR_BENCH_SEED")
49            .ok()
50            .and_then(|s| s.parse().ok())
51            .unwrap_or(DEFAULT_BENCH_SEED)
52    })
53}
54
55/// Simple deterministic PRNG (xorshift64)
56/// Used for test data generation without external dependencies
57#[derive(Clone, Debug)]
58pub struct DeterministicRng {
59    state: u64,
60}
61
62impl DeterministicRng {
63    /// Create new RNG with given seed
64    pub fn new(seed: u64) -> Self {
65        Self { state: seed }
66    }
67
68    /// Create RNG from test seed environment variable
69    pub fn from_test_seed() -> Self {
70        Self::new(get_test_seed())
71    }
72
73    /// Create RNG from benchmark seed environment variable
74    pub fn from_bench_seed() -> Self {
75        Self::new(get_bench_seed())
76    }
77
78    /// Generate next random u64
79    pub fn next_u64(&mut self) -> u64 {
80        // xorshift64 algorithm
81        self.state ^= self.state << 13;
82        self.state ^= self.state >> 7;
83        self.state ^= self.state << 17;
84        self.state
85    }
86
87    /// Generate random f64 in [0, 1)
88    pub fn next_f64(&mut self) -> f64 {
89        (self.next_u64() as f64) / (u64::MAX as f64)
90    }
91
92    /// Generate random f64 in [min, max)
93    pub fn next_f64_range(&mut self, min: f64, max: f64) -> f64 {
94        min + self.next_f64() * (max - min)
95    }
96
97    /// Reset RNG to original seed
98    pub fn reset(&mut self, seed: u64) {
99        self.state = seed;
100    }
101}
102
103/// Create a deterministic RNG for testing
104pub fn deterministic_rng() -> DeterministicRng {
105    DeterministicRng::from_test_seed()
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_deterministic_rng_reproducible() {
114        let mut rng1 = DeterministicRng::new(42);
115        let mut rng2 = DeterministicRng::new(42);
116
117        for _ in 0..100 {
118            assert_eq!(rng1.next_u64(), rng2.next_u64());
119        }
120    }
121
122    #[test]
123    fn test_f64_range() {
124        let mut rng = DeterministicRng::new(42);
125
126        for _ in 0..100 {
127            let v = rng.next_f64_range(0.0, 100.0);
128            assert!((0.0..100.0).contains(&v));
129        }
130    }
131
132    #[test]
133    fn test_seed_from_env() {
134        // Without env var, should use default
135        let seed = get_test_seed();
136        assert_eq!(seed, DEFAULT_TEST_SEED);
137    }
138
139    // Additional tests
140    #[test]
141    fn test_default_constants() {
142        assert_eq!(DEFAULT_TEST_SEED, 42);
143        assert_eq!(DEFAULT_PROPTEST_SEED, 0xdeadbeef);
144        assert_eq!(DEFAULT_BENCH_SEED, 12345);
145    }
146
147    #[test]
148    fn test_get_bench_seed() {
149        let seed = get_bench_seed();
150        assert_eq!(seed, DEFAULT_BENCH_SEED);
151    }
152
153    #[test]
154    fn test_deterministic_rng_from_test_seed() {
155        let mut rng = DeterministicRng::from_test_seed();
156        let val1 = rng.next_u64();
157        // Reset and check
158        rng.reset(get_test_seed());
159        let val2 = rng.next_u64();
160        assert_eq!(val1, val2);
161    }
162
163    #[test]
164    fn test_deterministic_rng_from_bench_seed() {
165        let mut rng = DeterministicRng::from_bench_seed();
166        let _val = rng.next_u64();
167        // Just verify it doesn't panic
168    }
169
170    #[test]
171    fn test_next_f64() {
172        let mut rng = DeterministicRng::new(12345);
173        for _ in 0..100 {
174            let val = rng.next_f64();
175            assert!((0.0..1.0).contains(&val));
176        }
177    }
178
179    #[test]
180    fn test_next_f64_range_negative() {
181        let mut rng = DeterministicRng::new(42);
182        for _ in 0..100 {
183            let val = rng.next_f64_range(-50.0, 50.0);
184            assert!((-50.0..50.0).contains(&val));
185        }
186    }
187
188    #[test]
189    fn test_reset() {
190        let mut rng = DeterministicRng::new(42);
191        let val1 = rng.next_u64();
192        let val2 = rng.next_u64();
193
194        rng.reset(42);
195        let val3 = rng.next_u64();
196        let val4 = rng.next_u64();
197
198        assert_eq!(val1, val3);
199        assert_eq!(val2, val4);
200    }
201
202    #[test]
203    fn test_deterministic_rng_debug() {
204        let rng = DeterministicRng::new(42);
205        let debug = format!("{:?}", rng);
206        assert!(debug.contains("DeterministicRng"));
207    }
208
209    #[test]
210    fn test_deterministic_rng_clone() {
211        let mut rng1 = DeterministicRng::new(42);
212        let _ = rng1.next_u64();
213        let mut rng2 = rng1.clone();
214
215        // Both should produce same value now
216        assert_eq!(rng1.next_u64(), rng2.next_u64());
217    }
218
219    #[test]
220    fn test_deterministic_rng_different_seeds() {
221        let mut rng1 = DeterministicRng::new(1);
222        let mut rng2 = DeterministicRng::new(2);
223
224        // Different seeds should produce different values
225        assert_ne!(rng1.next_u64(), rng2.next_u64());
226    }
227
228    #[test]
229    fn test_deterministic_rng_function() {
230        let mut rng = deterministic_rng();
231        let _val = rng.next_u64();
232        // Verify it works
233    }
234
235    #[test]
236    fn test_next_u64_not_zero() {
237        let mut rng = DeterministicRng::new(42);
238        // After a few iterations, shouldn't be stuck at 0
239        for _ in 0..10 {
240            rng.next_u64();
241        }
242        assert_ne!(rng.next_u64(), 0);
243    }
244
245    #[test]
246    fn test_deterministic_rng_zero_seed() {
247        // Edge case: zero seed
248        let mut rng = DeterministicRng::new(0);
249        // xorshift with 0 stays 0, but that's expected behavior
250        let val = rng.next_u64();
251        assert_eq!(val, 0);
252    }
253
254    #[test]
255    fn test_deterministic_rng_max_seed() {
256        // Edge case: max seed
257        let mut rng = DeterministicRng::new(u64::MAX);
258        let _val = rng.next_u64();
259        // Should not panic
260    }
261}