clnrm_core/determinism/
mod.rs1pub mod digest;
24pub mod rng;
25pub mod time;
26
27use crate::config::DeterminismConfig;
28use crate::error::{CleanroomError, Result};
29use chrono::{DateTime, Utc};
30use rand::RngCore;
31use std::sync::{Arc, Mutex};
32
33pub struct DeterminismEngine {
40 config: DeterminismConfig,
42 rng: Option<Arc<Mutex<Box<dyn RngCore + Send>>>>,
44 frozen_time: Option<DateTime<Utc>>,
46}
47
48impl std::fmt::Debug for DeterminismEngine {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("DeterminismEngine")
51 .field("config", &self.config)
52 .field("has_rng", &self.rng.is_some())
53 .field("frozen_time", &self.frozen_time)
54 .finish()
55 }
56}
57
58impl DeterminismEngine {
59 pub fn new(config: DeterminismConfig) -> Result<Self> {
70 let frozen_time = if let Some(ref timestamp_str) = config.freeze_clock {
72 Some(Self::parse_timestamp(timestamp_str)?)
73 } else {
74 None
75 };
76
77 let rng = config.seed.map(|seed| Arc::new(Mutex::new(rng::create_seeded_rng(seed))));
79
80 Ok(Self {
81 config,
82 rng,
83 frozen_time,
84 })
85 }
86
87 fn parse_timestamp(timestamp_str: &str) -> Result<DateTime<Utc>> {
89 DateTime::parse_from_rfc3339(timestamp_str)
90 .map(|dt| dt.with_timezone(&Utc))
91 .map_err(|e| {
92 CleanroomError::deterministic_error(format!(
93 "Invalid freeze_clock timestamp '{}': {}. Expected RFC3339 format (e.g., 2025-01-01T00:00:00Z)",
94 timestamp_str, e
95 ))
96 })
97 }
98
99 pub fn get_timestamp(&self) -> DateTime<Utc> {
104 self.frozen_time.unwrap_or_else(Utc::now)
105 }
106
107 pub fn get_timestamp_rfc3339(&self) -> String {
109 self.get_timestamp().to_rfc3339()
110 }
111
112 pub fn next_u64(&self) -> u64 {
120 if let Some(ref rng_mutex) = self.rng {
121 let mut rng = rng_mutex.lock()
122 .expect("RNG mutex poisoned - this indicates a panic in another thread");
123 rng.next_u64()
124 } else {
125 rand::random()
126 }
127 }
128
129 pub fn next_u32(&self) -> u32 {
131 if let Some(ref rng_mutex) = self.rng {
132 let mut rng = rng_mutex.lock()
133 .expect("RNG mutex poisoned - this indicates a panic in another thread");
134 rng.next_u32()
135 } else {
136 rand::random()
137 }
138 }
139
140 pub fn fill_bytes(&self, dest: &mut [u8]) {
142 if let Some(ref rng_mutex) = self.rng {
143 let mut rng = rng_mutex.lock()
144 .expect("RNG mutex poisoned - this indicates a panic in another thread");
145 rng.fill_bytes(dest);
146 } else {
147 rand::thread_rng().fill_bytes(dest);
148 }
149 }
150
151 pub fn is_deterministic(&self) -> bool {
153 self.config.seed.is_some() || self.config.freeze_clock.is_some()
154 }
155
156 pub fn has_seed(&self) -> bool {
158 self.config.seed.is_some()
159 }
160
161 pub fn has_frozen_clock(&self) -> bool {
163 self.config.freeze_clock.is_some()
164 }
165
166 pub fn get_seed(&self) -> Option<u64> {
168 self.config.seed
169 }
170
171 pub fn get_frozen_clock(&self) -> Option<&str> {
173 self.config.freeze_clock.as_deref()
174 }
175
176 pub fn config(&self) -> &DeterminismConfig {
178 &self.config
179 }
180}
181
182impl Clone for DeterminismEngine {
185 fn clone(&self) -> Self {
186 Self::new(self.config.clone())
187 .expect("Cloning DeterminismEngine with valid config should not fail")
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_determinism_engine_with_no_config() -> Result<()> {
197 let config = DeterminismConfig {
199 seed: None,
200 freeze_clock: None,
201 };
202
203 let engine = DeterminismEngine::new(config)?;
205
206 assert!(!engine.is_deterministic());
208 assert!(!engine.has_seed());
209 assert!(!engine.has_frozen_clock());
210 assert_eq!(engine.get_seed(), None);
211 assert_eq!(engine.get_frozen_clock(), None);
212
213 Ok(())
214 }
215
216 #[test]
217 fn test_determinism_engine_with_seed() -> Result<()> {
218 let config = DeterminismConfig {
220 seed: Some(42),
221 freeze_clock: None,
222 };
223
224 let engine = DeterminismEngine::new(config)?;
226
227 assert!(engine.is_deterministic());
229 assert!(engine.has_seed());
230 assert!(!engine.has_frozen_clock());
231 assert_eq!(engine.get_seed(), Some(42));
232
233 Ok(())
234 }
235
236 #[test]
237 fn test_determinism_engine_with_freeze_clock() -> Result<()> {
238 let config = DeterminismConfig {
240 seed: None,
241 freeze_clock: Some("2025-01-01T00:00:00Z".to_string()),
242 };
243
244 let engine = DeterminismEngine::new(config)?;
246
247 assert!(engine.is_deterministic());
249 assert!(!engine.has_seed());
250 assert!(engine.has_frozen_clock());
251 assert_eq!(engine.get_frozen_clock(), Some("2025-01-01T00:00:00Z"));
252
253 Ok(())
254 }
255
256 #[test]
257 fn test_determinism_engine_with_both() -> Result<()> {
258 let config = DeterminismConfig {
260 seed: Some(42),
261 freeze_clock: Some("2025-01-01T00:00:00Z".to_string()),
262 };
263
264 let engine = DeterminismEngine::new(config)?;
266
267 assert!(engine.is_deterministic());
269 assert!(engine.has_seed());
270 assert!(engine.has_frozen_clock());
271 assert_eq!(engine.get_seed(), Some(42));
272 assert_eq!(engine.get_frozen_clock(), Some("2025-01-01T00:00:00Z"));
273
274 Ok(())
275 }
276
277 #[test]
278 fn test_frozen_timestamp_returns_same_value() -> Result<()> {
279 let config = DeterminismConfig {
281 seed: None,
282 freeze_clock: Some("2025-01-01T00:00:00Z".to_string()),
283 };
284 let engine = DeterminismEngine::new(config)?;
285
286 let ts1 = engine.get_timestamp();
288 std::thread::sleep(std::time::Duration::from_millis(10));
289 let ts2 = engine.get_timestamp();
290
291 assert_eq!(ts1, ts2, "Frozen timestamps should be identical");
293
294 Ok(())
295 }
296
297 #[test]
298 fn test_frozen_timestamp_rfc3339() -> Result<()> {
299 let config = DeterminismConfig {
301 seed: None,
302 freeze_clock: Some("2025-01-01T00:00:00Z".to_string()),
303 };
304 let engine = DeterminismEngine::new(config)?;
305
306 let ts_str = engine.get_timestamp_rfc3339();
308
309 assert!(ts_str.starts_with("2025-01-01"));
311
312 Ok(())
313 }
314
315 #[test]
316 fn test_seeded_rng_produces_deterministic_values() -> Result<()> {
317 let config = DeterminismConfig {
319 seed: Some(42),
320 freeze_clock: None,
321 };
322 let engine1 = DeterminismEngine::new(config.clone())?;
323 let engine2 = DeterminismEngine::new(config)?;
324
325 let val1 = engine1.next_u64();
327 let val2 = engine2.next_u64();
328
329 assert_eq!(val1, val2, "Same seed should produce identical random values");
331
332 Ok(())
333 }
334
335 #[test]
336 fn test_seeded_rng_sequence() -> Result<()> {
337 let config = DeterminismConfig {
339 seed: Some(12345),
340 freeze_clock: None,
341 };
342 let engine = DeterminismEngine::new(config)?;
343
344 let values: Vec<u64> = (0..10).map(|_| engine.next_u64()).collect();
346
347 let unique_count = values.iter().collect::<std::collections::HashSet<_>>().len();
349 assert_eq!(unique_count, 10, "RNG should produce different values in sequence");
350
351 Ok(())
352 }
353
354 #[test]
355 fn test_invalid_freeze_clock_format() {
356 let config = DeterminismConfig {
358 seed: None,
359 freeze_clock: Some("not-a-valid-timestamp".to_string()),
360 };
361
362 let result = DeterminismEngine::new(config);
364
365 assert!(result.is_err());
367 if let Err(e) = result {
368 assert!(e.to_string().contains("Invalid freeze_clock"));
369 }
370 }
371
372 #[test]
373 fn test_various_rfc3339_formats() -> Result<()> {
374 let formats = vec![
376 "2025-01-01T00:00:00Z",
377 "2025-12-31T23:59:59Z",
378 "2025-06-15T12:30:45+00:00",
379 "2025-06-15T12:30:45-05:00",
380 ];
381
382 for format in formats {
383 let config = DeterminismConfig {
384 seed: None,
385 freeze_clock: Some(format.to_string()),
386 };
387 let engine = DeterminismEngine::new(config)?;
388 assert!(engine.has_frozen_clock());
389 }
390
391 Ok(())
392 }
393
394 #[test]
395 fn test_engine_clone_preserves_seed() -> Result<()> {
396 let config = DeterminismConfig {
398 seed: Some(42),
399 freeze_clock: Some("2025-01-01T00:00:00Z".to_string()),
400 };
401 let engine1 = DeterminismEngine::new(config)?;
402
403 let engine2 = engine1.clone();
405
406 assert_eq!(engine1.get_seed(), engine2.get_seed());
408 assert_eq!(engine1.get_frozen_clock(), engine2.get_frozen_clock());
409
410 assert_eq!(engine1.next_u64(), engine2.next_u64());
412
413 Ok(())
414 }
415
416 #[test]
417 fn test_fill_bytes_deterministic() -> Result<()> {
418 let config = DeterminismConfig {
420 seed: Some(999),
421 freeze_clock: None,
422 };
423 let engine1 = DeterminismEngine::new(config.clone())?;
424 let engine2 = DeterminismEngine::new(config)?;
425
426 let mut buf1 = [0u8; 16];
428 let mut buf2 = [0u8; 16];
429 engine1.fill_bytes(&mut buf1);
430 engine2.fill_bytes(&mut buf2);
431
432 assert_eq!(buf1, buf2, "Same seed should produce identical byte sequences");
434
435 Ok(())
436 }
437}