rust_actions/
determinism.rs

1use rand::{Rng, SeedableRng};
2use rand_chacha::ChaCha8Rng;
3use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5use uuid::Uuid;
6
7#[derive(Debug)]
8pub struct SeededRng {
9    rng: ChaCha8Rng,
10    seed: u64,
11}
12
13impl SeededRng {
14    pub fn new() -> Self {
15        Self::with_seed(0)
16    }
17
18    pub fn with_seed(seed: u64) -> Self {
19        Self {
20            rng: ChaCha8Rng::seed_from_u64(seed),
21            seed,
22        }
23    }
24
25    pub fn from_scenario_name(name: &str) -> Self {
26        let mut hasher = DefaultHasher::new();
27        name.hash(&mut hasher);
28        let seed = hasher.finish();
29        Self::with_seed(seed)
30    }
31
32    pub fn seed(&self) -> u64 {
33        self.seed
34    }
35
36    pub fn next_uuid(&mut self) -> Uuid {
37        let bytes: [u8; 16] = self.rng.gen();
38        Uuid::from_bytes(bytes)
39    }
40
41    pub fn next_u64(&mut self) -> u64 {
42        self.rng.gen()
43    }
44
45    pub fn next_u32(&mut self) -> u32 {
46        self.rng.gen()
47    }
48
49    pub fn next_i64(&mut self) -> i64 {
50        self.rng.gen()
51    }
52
53    pub fn next_f64(&mut self) -> f64 {
54        self.rng.gen()
55    }
56
57    pub fn next_bool(&mut self) -> bool {
58        self.rng.gen()
59    }
60
61    pub fn next_string(&mut self, len: usize) -> String {
62        const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
63        (0..len)
64            .map(|_| {
65                let idx = self.rng.gen_range(0..CHARSET.len());
66                CHARSET[idx] as char
67            })
68            .collect()
69    }
70
71    pub fn next_alphanumeric(&mut self, len: usize) -> String {
72        self.next_string(len)
73    }
74
75    pub fn next_hex(&mut self, len: usize) -> String {
76        const CHARSET: &[u8] = b"0123456789abcdef";
77        (0..len)
78            .map(|_| {
79                let idx = self.rng.gen_range(0..CHARSET.len());
80                CHARSET[idx] as char
81            })
82            .collect()
83    }
84
85    pub fn next_range(&mut self, min: u64, max: u64) -> u64 {
86        self.rng.gen_range(min..max)
87    }
88
89    pub fn choose<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T> {
90        if items.is_empty() {
91            None
92        } else {
93            let idx = self.rng.gen_range(0..items.len());
94            Some(&items[idx])
95        }
96    }
97
98    pub fn shuffle<T>(&mut self, items: &mut [T]) {
99        use rand::seq::SliceRandom;
100        items.shuffle(&mut self.rng);
101    }
102}
103
104impl Default for SeededRng {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl Clone for SeededRng {
111    fn clone(&self) -> Self {
112        Self::with_seed(self.seed)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_deterministic_uuid() {
122        let mut rng1 = SeededRng::with_seed(42);
123        let mut rng2 = SeededRng::with_seed(42);
124
125        let uuid1 = rng1.next_uuid();
126        let uuid2 = rng2.next_uuid();
127
128        assert_eq!(uuid1, uuid2);
129    }
130
131    #[test]
132    fn test_deterministic_string() {
133        let mut rng1 = SeededRng::with_seed(123);
134        let mut rng2 = SeededRng::with_seed(123);
135
136        let s1 = rng1.next_string(32);
137        let s2 = rng2.next_string(32);
138
139        assert_eq!(s1, s2);
140    }
141
142    #[test]
143    fn test_from_scenario_name() {
144        let rng1 = SeededRng::from_scenario_name("test scenario");
145        let rng2 = SeededRng::from_scenario_name("test scenario");
146        let rng3 = SeededRng::from_scenario_name("different scenario");
147
148        assert_eq!(rng1.seed(), rng2.seed());
149        assert_ne!(rng1.seed(), rng3.seed());
150    }
151
152    #[test]
153    fn test_sequence_determinism() {
154        let mut rng1 = SeededRng::with_seed(999);
155        let mut rng2 = SeededRng::with_seed(999);
156
157        for _ in 0..100 {
158            assert_eq!(rng1.next_u64(), rng2.next_u64());
159        }
160    }
161}