clnrm_core/determinism/
mod.rs

1//! Determinism engine for reproducible test execution
2//!
3//! Provides infrastructure for deterministic test execution with:
4//! - Fixed random seeds for reproducible random number generation
5//! - Frozen clock timestamps for deterministic time operations
6//! - SHA-256 digest generation for trace verification
7//!
8//! # Examples
9//!
10//! ```no_run
11//! use clnrm_core::determinism::{DeterminismEngine, DeterminismConfig};
12//!
13//! let config = DeterminismConfig {
14//!     seed: Some(42),
15//!     freeze_clock: Some("2025-01-01T00:00:00Z".to_string()),
16//! };
17//!
18//! let engine = DeterminismEngine::new(config).unwrap();
19//! let timestamp = engine.get_timestamp();
20//! let random_value = engine.next_u64();
21//! ```
22
23pub 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
33/// Determinism engine for reproducible test execution
34///
35/// This engine provides:
36/// - Seeded random number generation
37/// - Frozen clock timestamps
38/// - Digest generation for trace verification
39pub struct DeterminismEngine {
40    /// Configuration for determinism features
41    config: DeterminismConfig,
42    /// Seeded random number generator (thread-safe)
43    rng: Option<Arc<Mutex<Box<dyn RngCore + Send>>>>,
44    /// Frozen timestamp
45    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    /// Create new determinism engine from configuration
60    ///
61    /// # Arguments
62    /// * `config` - Determinism configuration with optional seed and freeze_clock
63    ///
64    /// # Returns
65    /// * `Result<Self>` - Initialized engine or error
66    ///
67    /// # Errors
68    /// * Returns error if freeze_clock is not valid RFC3339 format
69    pub fn new(config: DeterminismConfig) -> Result<Self> {
70        // Validate and parse freeze_clock if present
71        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        // Initialize RNG if seed is present
78        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    /// Parse RFC3339 timestamp string
88    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    /// Get current timestamp (frozen or actual)
100    ///
101    /// If freeze_clock is configured, returns the frozen timestamp.
102    /// Otherwise, returns the current system time.
103    pub fn get_timestamp(&self) -> DateTime<Utc> {
104        self.frozen_time.unwrap_or_else(Utc::now)
105    }
106
107    /// Get timestamp as RFC3339 string
108    pub fn get_timestamp_rfc3339(&self) -> String {
109        self.get_timestamp().to_rfc3339()
110    }
111
112    /// Generate next random u64 value
113    ///
114    /// If seed is configured, uses seeded RNG for deterministic values.
115    /// Otherwise, uses system randomness.
116    ///
117    /// # Returns
118    /// * Random u64 value
119    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    /// Generate next random u32 value
130    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    /// Fill buffer with random bytes
141    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    /// Check if determinism is enabled
152    pub fn is_deterministic(&self) -> bool {
153        self.config.seed.is_some() || self.config.freeze_clock.is_some()
154    }
155
156    /// Check if seed is configured
157    pub fn has_seed(&self) -> bool {
158        self.config.seed.is_some()
159    }
160
161    /// Check if clock is frozen
162    pub fn has_frozen_clock(&self) -> bool {
163        self.config.freeze_clock.is_some()
164    }
165
166    /// Get the seed value if configured
167    pub fn get_seed(&self) -> Option<u64> {
168        self.config.seed
169    }
170
171    /// Get the frozen clock timestamp string if configured
172    pub fn get_frozen_clock(&self) -> Option<&str> {
173        self.config.freeze_clock.as_deref()
174    }
175
176    /// Get reference to configuration
177    pub fn config(&self) -> &DeterminismConfig {
178        &self.config
179    }
180}
181
182// Implement Clone for DeterminismEngine
183// Note: RNG state is not cloned; instead, each clone gets a fresh RNG with the same seed
184impl 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        // Arrange
198        let config = DeterminismConfig {
199            seed: None,
200            freeze_clock: None,
201        };
202
203        // Act
204        let engine = DeterminismEngine::new(config)?;
205
206        // Assert
207        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        // Arrange
219        let config = DeterminismConfig {
220            seed: Some(42),
221            freeze_clock: None,
222        };
223
224        // Act
225        let engine = DeterminismEngine::new(config)?;
226
227        // Assert
228        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        // Arrange
239        let config = DeterminismConfig {
240            seed: None,
241            freeze_clock: Some("2025-01-01T00:00:00Z".to_string()),
242        };
243
244        // Act
245        let engine = DeterminismEngine::new(config)?;
246
247        // Assert
248        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        // Arrange
259        let config = DeterminismConfig {
260            seed: Some(42),
261            freeze_clock: Some("2025-01-01T00:00:00Z".to_string()),
262        };
263
264        // Act
265        let engine = DeterminismEngine::new(config)?;
266
267        // Assert
268        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        // Arrange
280        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        // Act
287        let ts1 = engine.get_timestamp();
288        std::thread::sleep(std::time::Duration::from_millis(10));
289        let ts2 = engine.get_timestamp();
290
291        // Assert
292        assert_eq!(ts1, ts2, "Frozen timestamps should be identical");
293
294        Ok(())
295    }
296
297    #[test]
298    fn test_frozen_timestamp_rfc3339() -> Result<()> {
299        // Arrange
300        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        // Act
307        let ts_str = engine.get_timestamp_rfc3339();
308
309        // Assert
310        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        // Arrange
318        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        // Act
326        let val1 = engine1.next_u64();
327        let val2 = engine2.next_u64();
328
329        // Assert
330        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        // Arrange
338        let config = DeterminismConfig {
339            seed: Some(12345),
340            freeze_clock: None,
341        };
342        let engine = DeterminismEngine::new(config)?;
343
344        // Act
345        let values: Vec<u64> = (0..10).map(|_| engine.next_u64()).collect();
346
347        // Assert - values should be different from each other
348        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        // Arrange
357        let config = DeterminismConfig {
358            seed: None,
359            freeze_clock: Some("not-a-valid-timestamp".to_string()),
360        };
361
362        // Act
363        let result = DeterminismEngine::new(config);
364
365        // Assert
366        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        // Arrange & Act & Assert
375        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        // Arrange
397        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        // Act
404        let engine2 = engine1.clone();
405
406        // Assert
407        assert_eq!(engine1.get_seed(), engine2.get_seed());
408        assert_eq!(engine1.get_frozen_clock(), engine2.get_frozen_clock());
409
410        // Both should produce same first value (fresh RNG with same seed)
411        assert_eq!(engine1.next_u64(), engine2.next_u64());
412
413        Ok(())
414    }
415
416    #[test]
417    fn test_fill_bytes_deterministic() -> Result<()> {
418        // Arrange
419        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        // Act
427        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
433        assert_eq!(buf1, buf2, "Same seed should produce identical byte sequences");
434
435        Ok(())
436    }
437}