rust_actions/
determinism.rs1use 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}