Skip to main content

jugar_web/
loadtest.rs

1//! Load Testing and Performance Validation
2//!
3//! Implements Section 11 of the Renacer-Based Game Event Tracing specification.
4//!
5//! ## Key Components
6//!
7//! - [`ChaosConfig`] / [`ChaosScenario`]: Chaos engineering configuration
8//! - [`ChaosRunner`]: Executes chaos scenarios and collects results
9//! - [`FrameTimeStats`]: Percentile-based frame time analysis
10//! - [`DriftDetector`]: Z-score based anomaly detection
11//!
12//! ## Research Foundation
13//!
14//! - Basiri et al. (2016): Chaos Engineering principles from Netflix
15//! - Claessen & Hughes (2000): Property-based testing (QuickCheck)
16//! - Dean & Barroso (2013): Tail latency and p99 analysis
17//!
18//! ## Usage
19//!
20//! ```ignore
21//! use jugar_web::loadtest::{ChaosConfig, ChaosRunner};
22//!
23//! // Run input flood chaos scenario
24//! let config = ChaosConfig::input_flood();
25//! let mut runner = ChaosRunner::new(platform, config);
26//! let results = runner.run();
27//! assert!(!results.nan_detected);
28//! ```
29
30use std::collections::VecDeque;
31
32// =============================================================================
33// Chaos Engineering Types
34// =============================================================================
35
36/// Chaos scenario types for stress testing game systems.
37///
38/// Each scenario targets specific failure modes:
39/// - `EntityStorm`: Memory limits, physics stability
40/// - `InputFlood`: Input buffer overflow, dropped inputs
41/// - `TimeWarp`: Physics explosion, NaN propagation
42/// - `ResizeBlitz`: Layout thrashing, coordinate drift
43/// - `ConfigSweep`: Edge case configurations
44/// - `RngTorture`: Determinism validation
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub enum ChaosScenario {
47    /// Spawn entities at maximum rate to test memory and physics stability.
48    EntityStorm {
49        /// Maximum number of entities to spawn.
50        max_entities: usize,
51    },
52    /// Flood input buffer with events to test throughput limits.
53    InputFlood {
54        /// Number of input events to generate per frame.
55        events_per_frame: usize,
56    },
57    /// Vary delta time extremely to test physics stability.
58    TimeWarp {
59        /// Minimum delta time in seconds.
60        min_dt: f32,
61        /// Maximum delta time in seconds.
62        max_dt: f32,
63    },
64    /// Rapid resize events to test layout system.
65    ResizeBlitz {
66        /// Frequency of resize events (per N frames).
67        frequency: u32,
68    },
69    /// Test all configuration permutations.
70    ConfigSweep,
71    /// Adversarial RNG seeds for determinism validation.
72    RngTorture {
73        /// Number of seed iterations to test.
74        iterations: usize,
75    },
76}
77
78/// Configuration for chaos testing scenarios.
79///
80/// # Example
81///
82/// ```
83/// use jugar_web::loadtest::ChaosConfig;
84///
85/// let config = ChaosConfig::input_flood();
86/// assert_eq!(config.duration_frames, 300);
87/// ```
88#[derive(Debug, Clone)]
89pub struct ChaosConfig {
90    /// Scenario type to execute.
91    pub scenario: ChaosScenario,
92    /// Duration in frames.
93    pub duration_frames: u64,
94    /// Random seed for reproducibility.
95    pub seed: u64,
96    /// Intensity level (0.0 - 1.0).
97    pub intensity: f32,
98}
99
100impl ChaosConfig {
101    /// Create a new chaos configuration.
102    #[must_use]
103    pub const fn new(scenario: ChaosScenario, duration_frames: u64, seed: u64) -> Self {
104        Self {
105            scenario,
106            duration_frames,
107            seed,
108            intensity: 1.0,
109        }
110    }
111
112    /// Standard entity storm for stress testing.
113    ///
114    /// Spawns up to 1000 entities over 600 frames (10 seconds at 60 FPS).
115    #[must_use]
116    pub const fn entity_storm() -> Self {
117        Self {
118            scenario: ChaosScenario::EntityStorm { max_entities: 1000 },
119            duration_frames: 600,
120            seed: 0xDEAD_BEEF,
121            intensity: 1.0,
122        }
123    }
124
125    /// Input flood to test buffer limits.
126    ///
127    /// Generates 100 input events per frame for 300 frames.
128    #[must_use]
129    pub const fn input_flood() -> Self {
130        Self {
131            scenario: ChaosScenario::InputFlood {
132                events_per_frame: 100,
133            },
134            duration_frames: 300,
135            seed: 0xCAFE_BABE,
136            intensity: 1.0,
137        }
138    }
139
140    /// Time warp scenario for physics stability testing.
141    ///
142    /// Varies delta time from 0.1ms to 1000ms.
143    #[must_use]
144    pub const fn time_warp() -> Self {
145        Self {
146            scenario: ChaosScenario::TimeWarp {
147                min_dt: 0.000_1,
148                max_dt: 1.0,
149            },
150            duration_frames: 600,
151            seed: 0xBAD_F00D,
152            intensity: 1.0,
153        }
154    }
155
156    /// Resize blitz for layout testing.
157    ///
158    /// Triggers resize every 5 frames for 300 frames.
159    #[must_use]
160    pub const fn resize_blitz() -> Self {
161        Self {
162            scenario: ChaosScenario::ResizeBlitz { frequency: 5 },
163            duration_frames: 300,
164            seed: 0xFEED_FACE,
165            intensity: 1.0,
166        }
167    }
168
169    /// RNG torture test for determinism validation.
170    ///
171    /// Tests 1000 different RNG seeds.
172    #[must_use]
173    pub const fn rng_torture() -> Self {
174        Self {
175            scenario: ChaosScenario::RngTorture { iterations: 1000 },
176            duration_frames: 100,
177            seed: 0x1234_5678,
178            intensity: 1.0,
179        }
180    }
181
182    /// Configuration sweep for edge case testing.
183    #[must_use]
184    pub const fn config_sweep() -> Self {
185        Self {
186            scenario: ChaosScenario::ConfigSweep,
187            duration_frames: 60,
188            seed: 0xABCD_EF01,
189            intensity: 1.0,
190        }
191    }
192
193    /// Set the intensity level (0.0 - 1.0).
194    #[must_use]
195    pub const fn with_intensity(mut self, intensity: f32) -> Self {
196        self.intensity = intensity;
197        self
198    }
199}
200
201/// Results from chaos scenario execution.
202#[derive(Debug, Clone, Default)]
203pub struct ChaosResults {
204    /// Total frames executed.
205    pub frames_executed: u64,
206    /// Frames that exceeded target time (16.67ms).
207    pub slow_frames: u64,
208    /// Maximum frame time observed (ms).
209    pub max_frame_time_ms: f64,
210    /// Minimum frame time observed (ms).
211    pub min_frame_time_ms: f64,
212    /// Average frame time (ms).
213    pub avg_frame_time_ms: f64,
214    /// Any panics caught (in catch_unwind context).
215    pub panics: Vec<String>,
216    /// NaN/Inf values detected in game state.
217    pub nan_detected: bool,
218    /// Infinity values detected in game state.
219    pub inf_detected: bool,
220    /// Memory high-water mark (bytes), if tracked.
221    pub peak_memory_bytes: Option<usize>,
222    /// Inputs dropped (if any).
223    pub inputs_dropped: u64,
224    /// Scenario that was executed.
225    pub scenario: Option<ChaosScenario>,
226}
227
228impl ChaosResults {
229    /// Create new empty results.
230    #[must_use]
231    pub fn new() -> Self {
232        Self {
233            min_frame_time_ms: f64::INFINITY,
234            ..Default::default()
235        }
236    }
237
238    /// Check if the chaos test passed (no critical issues).
239    #[must_use]
240    pub fn passed(&self) -> bool {
241        self.panics.is_empty() && !self.nan_detected && !self.inf_detected
242    }
243
244    /// Record a frame time sample.
245    pub fn record_frame_time(&mut self, frame_time_ms: f64) {
246        self.frames_executed += 1;
247        if frame_time_ms > 16.67 {
248            self.slow_frames += 1;
249        }
250        if frame_time_ms > self.max_frame_time_ms {
251            self.max_frame_time_ms = frame_time_ms;
252        }
253        if frame_time_ms < self.min_frame_time_ms {
254            self.min_frame_time_ms = frame_time_ms;
255        }
256        // Running average
257        let n = self.frames_executed as f64;
258        self.avg_frame_time_ms = self.avg_frame_time_ms * (n - 1.0) / n + frame_time_ms / n;
259    }
260
261    /// Record a NaN detection.
262    pub fn record_nan(&mut self) {
263        self.nan_detected = true;
264    }
265
266    /// Record an Inf detection.
267    pub fn record_inf(&mut self) {
268        self.inf_detected = true;
269    }
270
271    /// Record a panic message.
272    pub fn record_panic(&mut self, message: String) {
273        self.panics.push(message);
274    }
275
276    /// Record dropped inputs.
277    pub fn record_dropped_inputs(&mut self, count: u64) {
278        self.inputs_dropped += count;
279    }
280}
281
282// =============================================================================
283// Frame Time Statistics
284// =============================================================================
285
286/// Frame time statistics with percentile analysis.
287///
288/// Based on Dean & Barroso (2013): "The Tail at Scale" - p99 latency
289/// is critical for interactive systems like games.
290///
291/// # Example
292///
293/// ```
294/// use jugar_web::loadtest::FrameTimeStats;
295///
296/// let mut stats = FrameTimeStats::new();
297/// for i in 0..100 {
298///     stats.record(i as f64 * 0.1 + 5.0);
299/// }
300/// let report = stats.report();
301/// assert!(report.p50 < report.p99);
302/// ```
303#[derive(Debug, Clone)]
304pub struct FrameTimeStats {
305    /// All frame times (ms).
306    samples: Vec<f64>,
307}
308
309impl Default for FrameTimeStats {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315impl FrameTimeStats {
316    /// Create new empty statistics collector.
317    #[must_use]
318    pub fn new() -> Self {
319        Self {
320            samples: Vec::new(),
321        }
322    }
323
324    /// Create with pre-allocated capacity.
325    #[must_use]
326    pub fn with_capacity(capacity: usize) -> Self {
327        Self {
328            samples: Vec::with_capacity(capacity),
329        }
330    }
331
332    /// Record a frame time sample (in milliseconds).
333    pub fn record(&mut self, frame_time_ms: f64) {
334        self.samples.push(frame_time_ms);
335    }
336
337    /// Get the number of samples.
338    #[must_use]
339    pub fn len(&self) -> usize {
340        self.samples.len()
341    }
342
343    /// Check if empty.
344    #[must_use]
345    pub fn is_empty(&self) -> bool {
346        self.samples.is_empty()
347    }
348
349    /// Get percentile value (0-100).
350    ///
351    /// Common percentiles: p50 (median), p90, p95, p99.
352    #[must_use]
353    pub fn percentile(&self, p: f64) -> f64 {
354        if self.samples.is_empty() {
355            return 0.0;
356        }
357        let mut sorted = self.samples.clone();
358        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
359        let idx = ((p / 100.0) * (sorted.len() - 1) as f64) as usize;
360        sorted[idx]
361    }
362
363    /// Get minimum value.
364    #[must_use]
365    pub fn min(&self) -> f64 {
366        self.samples.iter().copied().fold(f64::INFINITY, f64::min)
367    }
368
369    /// Get maximum value.
370    #[must_use]
371    pub fn max(&self) -> f64 {
372        self.samples
373            .iter()
374            .copied()
375            .fold(f64::NEG_INFINITY, f64::max)
376    }
377
378    /// Get mean value.
379    #[must_use]
380    pub fn mean(&self) -> f64 {
381        if self.samples.is_empty() {
382            return 0.0;
383        }
384        self.samples.iter().sum::<f64>() / self.samples.len() as f64
385    }
386
387    /// Get standard deviation.
388    #[must_use]
389    pub fn std_dev(&self) -> f64 {
390        if self.samples.len() < 2 {
391            return 0.0;
392        }
393        let mean = self.mean();
394        let variance: f64 = self.samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
395            / (self.samples.len() - 1) as f64;
396        variance.sqrt()
397    }
398
399    /// Generate a summary report.
400    #[must_use]
401    pub fn report(&self) -> FrameTimeReport {
402        FrameTimeReport {
403            count: self.samples.len(),
404            min: self.min(),
405            max: self.max(),
406            mean: self.mean(),
407            std_dev: self.std_dev(),
408            p50: self.percentile(50.0),
409            p90: self.percentile(90.0),
410            p95: self.percentile(95.0),
411            p99: self.percentile(99.0),
412        }
413    }
414
415    /// Clear all samples.
416    pub fn clear(&mut self) {
417        self.samples.clear();
418    }
419}
420
421/// Summary report of frame time statistics.
422#[derive(Debug, Clone)]
423pub struct FrameTimeReport {
424    /// Number of samples.
425    pub count: usize,
426    /// Minimum frame time (ms).
427    pub min: f64,
428    /// Maximum frame time (ms).
429    pub max: f64,
430    /// Mean frame time (ms).
431    pub mean: f64,
432    /// Standard deviation (ms).
433    pub std_dev: f64,
434    /// 50th percentile / median (ms).
435    pub p50: f64,
436    /// 90th percentile (ms).
437    pub p90: f64,
438    /// 95th percentile (ms).
439    pub p95: f64,
440    /// 99th percentile (ms).
441    pub p99: f64,
442}
443
444impl FrameTimeReport {
445    /// Check if performance meets 60 FPS target (16.67ms budget).
446    #[must_use]
447    pub fn meets_60fps(&self) -> bool {
448        self.p99 < 16.67
449    }
450
451    /// Check if performance meets 120 FPS target (8.33ms budget).
452    #[must_use]
453    pub fn meets_120fps(&self) -> bool {
454        self.p99 < 8.33
455    }
456
457    /// Check if performance meets 30 FPS target (33.33ms budget).
458    #[must_use]
459    pub fn meets_30fps(&self) -> bool {
460        self.p99 < 33.33
461    }
462
463    /// Get the jitter (max - min).
464    #[must_use]
465    pub fn jitter(&self) -> f64 {
466        self.max - self.min
467    }
468}
469
470// =============================================================================
471// Drift Detection (Anomaly Detection)
472// =============================================================================
473
474/// Result of anomaly detection.
475#[derive(Debug, Clone)]
476pub enum AnomalyResult {
477    /// Frame time is within normal range.
478    Normal,
479    /// Frame time is anomalous.
480    Anomaly {
481        /// The anomalous value (ms).
482        value: f64,
483        /// Z-score of the anomaly.
484        z_score: f64,
485        /// Expected range (min, max) in ms.
486        expected_range: (f64, f64),
487    },
488}
489
490impl AnomalyResult {
491    /// Check if this is an anomaly.
492    #[must_use]
493    pub fn is_anomaly(&self) -> bool {
494        matches!(self, Self::Anomaly { .. })
495    }
496
497    /// Check if this is normal.
498    #[must_use]
499    pub fn is_normal(&self) -> bool {
500        matches!(self, Self::Normal)
501    }
502}
503
504/// Report of sustained performance drift.
505#[derive(Debug, Clone)]
506pub struct DriftReport {
507    /// Baseline mean frame time (ms).
508    pub baseline_mean: f64,
509    /// Current window mean frame time (ms).
510    pub current_mean: f64,
511    /// Drift as a percentage of baseline.
512    pub drift_percent: f64,
513}
514
515impl DriftReport {
516    /// Check if drift is a regression (slower).
517    #[must_use]
518    pub fn is_regression(&self) -> bool {
519        self.drift_percent > 0.0
520    }
521
522    /// Check if drift is an improvement (faster).
523    #[must_use]
524    pub fn is_improvement(&self) -> bool {
525        self.drift_percent < 0.0
526    }
527}
528
529/// Drift detector using Z-score analysis.
530///
531/// Detects performance regressions using sliding window baseline comparison.
532/// Based on statistical process control principles.
533///
534/// # Example
535///
536/// ```
537/// use jugar_web::loadtest::DriftDetector;
538///
539/// let mut detector = DriftDetector::new(10, 2.0);
540///
541/// // Calibrate with baseline samples
542/// detector.calibrate(&[1.0, 1.1, 0.9, 1.0, 1.05, 0.95, 1.0, 1.0, 0.98, 1.02]);
543///
544/// // Normal observation
545/// let result = detector.observe(1.0);
546/// assert!(result.is_normal());
547///
548/// // Anomalous observation (way outside normal range)
549/// let result = detector.observe(10.0);
550/// assert!(result.is_anomaly());
551/// ```
552#[derive(Debug)]
553pub struct DriftDetector {
554    /// Sliding window of recent frame times.
555    window: VecDeque<f64>,
556    /// Window size.
557    window_size: usize,
558    /// Z-score threshold for anomaly detection.
559    z_threshold: f64,
560    /// Baseline mean (from calibration).
561    baseline_mean: f64,
562    /// Baseline standard deviation (from calibration).
563    baseline_std: f64,
564    /// Drift threshold percentage (default 10%).
565    drift_threshold_percent: f64,
566}
567
568impl DriftDetector {
569    /// Create a new drift detector.
570    ///
571    /// # Arguments
572    ///
573    /// * `window_size` - Number of samples in the sliding window
574    /// * `z_threshold` - Z-score threshold for anomaly detection (typical: 2.0-3.0)
575    #[must_use]
576    pub fn new(window_size: usize, z_threshold: f64) -> Self {
577        Self {
578            window: VecDeque::with_capacity(window_size),
579            window_size,
580            z_threshold,
581            baseline_mean: 0.0,
582            baseline_std: 1.0,
583            drift_threshold_percent: 10.0,
584        }
585    }
586
587    /// Set the drift threshold percentage.
588    #[must_use]
589    pub fn with_drift_threshold(mut self, percent: f64) -> Self {
590        self.drift_threshold_percent = percent;
591        self
592    }
593
594    /// Calibrate the detector with baseline samples.
595    ///
596    /// This establishes the "normal" baseline for comparison.
597    pub fn calibrate(&mut self, samples: &[f64]) {
598        if samples.is_empty() {
599            return;
600        }
601
602        self.baseline_mean = samples.iter().sum::<f64>() / samples.len() as f64;
603
604        let variance: f64 = samples
605            .iter()
606            .map(|x| (x - self.baseline_mean).powi(2))
607            .sum::<f64>()
608            / samples.len() as f64;
609
610        // Avoid division by zero
611        self.baseline_std = variance.sqrt().max(0.001);
612    }
613
614    /// Check if the detector has been calibrated.
615    #[must_use]
616    #[allow(clippy::float_cmp)] // Comparing to initial default values is intentional
617    pub fn is_calibrated(&self) -> bool {
618        self.baseline_mean != 0.0 || self.baseline_std != 1.0
619    }
620
621    /// Add a sample and check for anomaly.
622    #[allow(clippy::suboptimal_flops)] // Explicit mul-add is clearer here
623    pub fn observe(&mut self, frame_time_ms: f64) -> AnomalyResult {
624        // Add to sliding window
625        self.window.push_back(frame_time_ms);
626        if self.window.len() > self.window_size {
627            let _ = self.window.pop_front();
628        }
629
630        // Calculate Z-score
631        let z_score = (frame_time_ms - self.baseline_mean) / self.baseline_std;
632
633        if z_score.abs() > self.z_threshold {
634            AnomalyResult::Anomaly {
635                value: frame_time_ms,
636                z_score,
637                expected_range: (
638                    self.baseline_mean - self.z_threshold * self.baseline_std,
639                    self.baseline_mean + self.z_threshold * self.baseline_std,
640                ),
641            }
642        } else {
643            AnomalyResult::Normal
644        }
645    }
646
647    /// Detect sustained drift (window mean significantly different from baseline).
648    #[must_use]
649    pub fn detect_drift(&self) -> Option<DriftReport> {
650        if self.window.len() < self.window_size {
651            return None;
652        }
653
654        let window_mean: f64 = self.window.iter().sum::<f64>() / self.window.len() as f64;
655
656        // Avoid division by zero
657        if self.baseline_mean.abs() < 0.001 {
658            return None;
659        }
660
661        let drift = (window_mean - self.baseline_mean) / self.baseline_mean * 100.0;
662
663        if drift.abs() > self.drift_threshold_percent {
664            Some(DriftReport {
665                baseline_mean: self.baseline_mean,
666                current_mean: window_mean,
667                drift_percent: drift,
668            })
669        } else {
670            None
671        }
672    }
673
674    /// Get current window statistics.
675    #[must_use]
676    pub fn window_stats(&self) -> Option<(f64, f64)> {
677        if self.window.is_empty() {
678            return None;
679        }
680
681        let mean = self.window.iter().sum::<f64>() / self.window.len() as f64;
682        let variance: f64 =
683            self.window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / self.window.len() as f64;
684
685        Some((mean, variance.sqrt()))
686    }
687
688    /// Reset the detector (clear window, keep calibration).
689    pub fn reset(&mut self) {
690        self.window.clear();
691    }
692
693    /// Full reset (clear window and calibration).
694    pub fn full_reset(&mut self) {
695        self.window.clear();
696        self.baseline_mean = 0.0;
697        self.baseline_std = 1.0;
698    }
699}
700
701// =============================================================================
702// Load Test Configuration
703// =============================================================================
704
705/// Configuration for a load test run.
706#[derive(Debug, Clone)]
707pub struct LoadTestConfig {
708    /// Name of the test.
709    pub name: String,
710    /// Chaos configuration (optional).
711    pub chaos: Option<ChaosConfig>,
712    /// Run property-based tests.
713    pub property_tests: bool,
714    /// Run benchmarks.
715    pub benchmark: bool,
716    /// Duration in frames.
717    pub duration_frames: u64,
718    /// Tier level (1, 2, or 3).
719    pub tier: u8,
720}
721
722impl Default for LoadTestConfig {
723    fn default() -> Self {
724        Self {
725            name: "default".to_string(),
726            chaos: None,
727            property_tests: false,
728            benchmark: false,
729            duration_frames: 600,
730            tier: 1,
731        }
732    }
733}
734
735impl LoadTestConfig {
736    /// Create a new load test configuration.
737    #[must_use]
738    pub fn new(name: impl Into<String>) -> Self {
739        Self {
740            name: name.into(),
741            ..Default::default()
742        }
743    }
744
745    /// Create Tier 1 config (on-save, < 5 seconds).
746    #[must_use]
747    pub fn tier1(name: impl Into<String>) -> Self {
748        Self {
749            name: name.into(),
750            chaos: None,
751            property_tests: false,
752            benchmark: false,
753            duration_frames: 60,
754            tier: 1,
755        }
756    }
757
758    /// Create Tier 2 config (on-commit, < 30 seconds).
759    #[must_use]
760    pub fn tier2(name: impl Into<String>) -> Self {
761        Self {
762            name: name.into(),
763            chaos: Some(ChaosConfig::input_flood()),
764            property_tests: true,
765            benchmark: false,
766            duration_frames: 300,
767            tier: 2,
768        }
769    }
770
771    /// Create Tier 3 config (on-merge, < 5 minutes).
772    #[must_use]
773    pub fn tier3(name: impl Into<String>) -> Self {
774        Self {
775            name: name.into(),
776            chaos: Some(ChaosConfig::entity_storm()),
777            property_tests: true,
778            benchmark: true,
779            duration_frames: 3600,
780            tier: 3,
781        }
782    }
783
784    /// Add chaos configuration.
785    #[must_use]
786    pub fn with_chaos(mut self, chaos: ChaosConfig) -> Self {
787        self.chaos = Some(chaos);
788        self
789    }
790
791    /// Enable property tests.
792    #[must_use]
793    pub fn with_property_tests(mut self) -> Self {
794        self.property_tests = true;
795        self
796    }
797
798    /// Enable benchmarks.
799    #[must_use]
800    pub fn with_benchmarks(mut self) -> Self {
801        self.benchmark = true;
802        self
803    }
804
805    /// Set duration in frames.
806    #[must_use]
807    pub fn with_duration(mut self, frames: u64) -> Self {
808        self.duration_frames = frames;
809        self
810    }
811}
812
813/// Result from a load test run.
814#[derive(Debug, Clone)]
815pub struct LoadTestResult {
816    /// Name of the test.
817    pub name: String,
818    /// Whether the test passed.
819    pub passed: bool,
820    /// Chaos results (if chaos was run).
821    pub chaos_results: Option<ChaosResults>,
822    /// Frame time statistics.
823    pub frame_stats: FrameTimeReport,
824    /// Anomalies detected.
825    pub anomaly_count: usize,
826    /// Property test failures (if any).
827    pub property_failures: Vec<String>,
828    /// Error message (if failed).
829    pub error: Option<String>,
830}
831
832impl LoadTestResult {
833    /// Create a passing result.
834    #[must_use]
835    pub fn pass(name: impl Into<String>, frame_stats: FrameTimeReport) -> Self {
836        Self {
837            name: name.into(),
838            passed: true,
839            chaos_results: None,
840            frame_stats,
841            anomaly_count: 0,
842            property_failures: Vec::new(),
843            error: None,
844        }
845    }
846
847    /// Create a failing result.
848    #[must_use]
849    pub fn fail(name: impl Into<String>, error: impl Into<String>) -> Self {
850        Self {
851            name: name.into(),
852            passed: false,
853            chaos_results: None,
854            frame_stats: FrameTimeReport {
855                count: 0,
856                min: 0.0,
857                max: 0.0,
858                mean: 0.0,
859                std_dev: 0.0,
860                p50: 0.0,
861                p90: 0.0,
862                p95: 0.0,
863                p99: 0.0,
864            },
865            anomaly_count: 0,
866            property_failures: Vec::new(),
867            error: Some(error.into()),
868        }
869    }
870}
871
872/// Summary of all load test results.
873#[derive(Debug, Clone)]
874pub struct LoadTestSummary {
875    /// Total number of tests.
876    pub total: usize,
877    /// Number of passed tests.
878    pub passed: usize,
879    /// Number of failed tests.
880    pub failed: usize,
881    /// Individual results.
882    pub results: Vec<LoadTestResult>,
883}
884
885impl LoadTestSummary {
886    /// Create a new empty summary.
887    #[must_use]
888    pub fn new() -> Self {
889        Self {
890            total: 0,
891            passed: 0,
892            failed: 0,
893            results: Vec::new(),
894        }
895    }
896
897    /// Add a result to the summary.
898    pub fn add(&mut self, result: LoadTestResult) {
899        self.total += 1;
900        if result.passed {
901            self.passed += 1;
902        } else {
903            self.failed += 1;
904        }
905        self.results.push(result);
906    }
907
908    /// Check if all tests passed.
909    #[must_use]
910    pub fn all_passed(&self) -> bool {
911        self.failed == 0
912    }
913}
914
915impl Default for LoadTestSummary {
916    fn default() -> Self {
917        Self::new()
918    }
919}
920
921// =============================================================================
922// Tests
923// =============================================================================
924
925#[cfg(test)]
926#[allow(clippy::cast_lossless, clippy::unwrap_used, clippy::float_cmp)]
927mod tests {
928    use super::*;
929
930    // -------------------------------------------------------------------------
931    // ChaosConfig tests
932    // -------------------------------------------------------------------------
933
934    #[test]
935    fn test_chaos_config_entity_storm() {
936        let config = ChaosConfig::entity_storm();
937        assert_eq!(config.duration_frames, 600);
938        assert!(matches!(
939            config.scenario,
940            ChaosScenario::EntityStorm { max_entities: 1000 }
941        ));
942    }
943
944    #[test]
945    fn test_chaos_config_input_flood() {
946        let config = ChaosConfig::input_flood();
947        assert_eq!(config.duration_frames, 300);
948        assert!(matches!(
949            config.scenario,
950            ChaosScenario::InputFlood {
951                events_per_frame: 100
952            }
953        ));
954    }
955
956    #[test]
957    fn test_chaos_config_time_warp() {
958        let config = ChaosConfig::time_warp();
959        assert!(matches!(
960            config.scenario,
961            ChaosScenario::TimeWarp { min_dt, max_dt } if min_dt < max_dt
962        ));
963    }
964
965    #[test]
966    fn test_chaos_config_with_intensity() {
967        let config = ChaosConfig::entity_storm().with_intensity(0.5);
968        assert!((config.intensity - 0.5).abs() < f32::EPSILON);
969    }
970
971    // -------------------------------------------------------------------------
972    // ChaosResults tests
973    // -------------------------------------------------------------------------
974
975    #[test]
976    fn test_chaos_results_new() {
977        let results = ChaosResults::new();
978        assert_eq!(results.frames_executed, 0);
979        assert!(results.passed());
980    }
981
982    #[test]
983    fn test_chaos_results_record_frame_time() {
984        let mut results = ChaosResults::new();
985        results.record_frame_time(10.0);
986        results.record_frame_time(20.0);
987        assert_eq!(results.frames_executed, 2);
988        assert!((results.max_frame_time_ms - 20.0).abs() < f64::EPSILON);
989        assert!((results.min_frame_time_ms - 10.0).abs() < f64::EPSILON);
990    }
991
992    #[test]
993    fn test_chaos_results_slow_frames() {
994        let mut results = ChaosResults::new();
995        results.record_frame_time(10.0); // Fast
996        results.record_frame_time(20.0); // Slow (> 16.67ms)
997        assert_eq!(results.slow_frames, 1);
998    }
999
1000    #[test]
1001    fn test_chaos_results_nan_detection() {
1002        let mut results = ChaosResults::new();
1003        assert!(results.passed());
1004        results.record_nan();
1005        assert!(!results.passed());
1006    }
1007
1008    #[test]
1009    fn test_chaos_results_inf_detection() {
1010        let mut results = ChaosResults::new();
1011        assert!(results.passed());
1012        results.record_inf();
1013        assert!(!results.passed());
1014    }
1015
1016    #[test]
1017    fn test_chaos_results_panic_recording() {
1018        let mut results = ChaosResults::new();
1019        assert!(results.passed());
1020        results.record_panic("test panic".to_string());
1021        assert!(!results.passed());
1022    }
1023
1024    // -------------------------------------------------------------------------
1025    // FrameTimeStats tests
1026    // -------------------------------------------------------------------------
1027
1028    #[test]
1029    fn test_frame_time_stats_empty() {
1030        let stats = FrameTimeStats::new();
1031        assert!(stats.is_empty());
1032        assert_eq!(stats.len(), 0);
1033        assert!((stats.percentile(50.0) - 0.0).abs() < f64::EPSILON);
1034    }
1035
1036    #[test]
1037    fn test_frame_time_stats_record() {
1038        let mut stats = FrameTimeStats::new();
1039        stats.record(10.0);
1040        stats.record(20.0);
1041        stats.record(30.0);
1042        assert_eq!(stats.len(), 3);
1043        assert!((stats.mean() - 20.0).abs() < f64::EPSILON);
1044    }
1045
1046    #[test]
1047    fn test_frame_time_stats_percentiles() {
1048        let mut stats = FrameTimeStats::new();
1049        for i in 1..=100 {
1050            stats.record(i as f64);
1051        }
1052        assert!((stats.percentile(50.0) - 50.0).abs() < 1.0);
1053        assert!((stats.percentile(90.0) - 90.0).abs() < 1.0);
1054        assert!((stats.percentile(99.0) - 99.0).abs() < 1.0);
1055    }
1056
1057    #[test]
1058    fn test_frame_time_stats_min_max() {
1059        let mut stats = FrameTimeStats::new();
1060        stats.record(5.0);
1061        stats.record(10.0);
1062        stats.record(3.0);
1063        assert!((stats.min() - 3.0).abs() < f64::EPSILON);
1064        assert!((stats.max() - 10.0).abs() < f64::EPSILON);
1065    }
1066
1067    #[test]
1068    fn test_frame_time_stats_std_dev() {
1069        let mut stats = FrameTimeStats::new();
1070        // Standard deviation of [2, 4, 4, 4, 5, 5, 7, 9] is 2.0
1071        for v in [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
1072            stats.record(v);
1073        }
1074        assert!((stats.std_dev() - 2.138).abs() < 0.01);
1075    }
1076
1077    #[test]
1078    fn test_frame_time_report_fps_targets() {
1079        let mut stats = FrameTimeStats::new();
1080        for _ in 0..100 {
1081            stats.record(5.0); // 5ms = 200 FPS
1082        }
1083        let report = stats.report();
1084        assert!(report.meets_120fps());
1085        assert!(report.meets_60fps());
1086        assert!(report.meets_30fps());
1087    }
1088
1089    #[test]
1090    fn test_frame_time_report_fps_fail() {
1091        let mut stats = FrameTimeStats::new();
1092        for _ in 0..100 {
1093            stats.record(50.0); // 50ms = 20 FPS
1094        }
1095        let report = stats.report();
1096        assert!(!report.meets_120fps());
1097        assert!(!report.meets_60fps());
1098        assert!(!report.meets_30fps());
1099    }
1100
1101    // -------------------------------------------------------------------------
1102    // DriftDetector tests
1103    // -------------------------------------------------------------------------
1104
1105    #[test]
1106    fn test_drift_detector_new() {
1107        let detector = DriftDetector::new(10, 2.0);
1108        assert!(!detector.is_calibrated());
1109    }
1110
1111    #[test]
1112    fn test_drift_detector_calibrate() {
1113        let mut detector = DriftDetector::new(10, 2.0);
1114        detector.calibrate(&[1.0, 1.0, 1.0, 1.0, 1.0]);
1115        assert!(detector.is_calibrated());
1116    }
1117
1118    #[test]
1119    fn test_drift_detector_observe_normal() {
1120        let mut detector = DriftDetector::new(10, 2.0);
1121        detector.calibrate(&[1.0, 1.1, 0.9, 1.0, 1.05]);
1122        let result = detector.observe(1.0);
1123        assert!(result.is_normal());
1124    }
1125
1126    #[test]
1127    fn test_drift_detector_observe_anomaly() {
1128        let mut detector = DriftDetector::new(10, 2.0);
1129        detector.calibrate(&[1.0, 1.0, 1.0, 1.0, 1.0]);
1130        let result = detector.observe(10.0); // Way outside normal
1131        assert!(result.is_anomaly());
1132    }
1133
1134    #[test]
1135    fn test_drift_detector_detect_drift() {
1136        let mut detector = DriftDetector::new(5, 2.0).with_drift_threshold(10.0);
1137        detector.calibrate(&[1.0, 1.0, 1.0, 1.0, 1.0]);
1138
1139        // Fill window with higher values
1140        for _ in 0..5 {
1141            let _ = detector.observe(1.5); // 50% higher
1142        }
1143
1144        let drift = detector.detect_drift();
1145        assert!(drift.is_some());
1146        assert!(drift.unwrap().is_regression());
1147    }
1148
1149    #[test]
1150    fn test_drift_detector_no_drift() {
1151        let mut detector = DriftDetector::new(5, 2.0).with_drift_threshold(10.0);
1152        detector.calibrate(&[1.0, 1.0, 1.0, 1.0, 1.0]);
1153
1154        // Fill window with similar values
1155        for _ in 0..5 {
1156            let _ = detector.observe(1.05); // 5% higher (below threshold)
1157        }
1158
1159        let drift = detector.detect_drift();
1160        assert!(drift.is_none());
1161    }
1162
1163    #[test]
1164    fn test_drift_detector_reset() {
1165        let mut detector = DriftDetector::new(5, 2.0);
1166        detector.calibrate(&[1.0, 1.0, 1.0]);
1167        let _ = detector.observe(1.0);
1168
1169        detector.reset();
1170        assert!(detector.window_stats().is_none());
1171        assert!(detector.is_calibrated()); // Calibration preserved
1172    }
1173
1174    #[test]
1175    fn test_drift_detector_full_reset() {
1176        let mut detector = DriftDetector::new(5, 2.0);
1177        detector.calibrate(&[1.0, 1.0, 1.0]);
1178        let _ = detector.observe(1.0);
1179
1180        detector.full_reset();
1181        assert!(detector.window_stats().is_none());
1182        assert!(!detector.is_calibrated()); // Calibration cleared
1183    }
1184
1185    // -------------------------------------------------------------------------
1186    // LoadTestConfig tests
1187    // -------------------------------------------------------------------------
1188
1189    #[test]
1190    fn test_load_test_config_tier1() {
1191        let config = LoadTestConfig::tier1("fast_test");
1192        assert_eq!(config.tier, 1);
1193        assert_eq!(config.duration_frames, 60);
1194        assert!(!config.property_tests);
1195        assert!(!config.benchmark);
1196    }
1197
1198    #[test]
1199    fn test_load_test_config_tier2() {
1200        let config = LoadTestConfig::tier2("medium_test");
1201        assert_eq!(config.tier, 2);
1202        assert!(config.property_tests);
1203        assert!(config.chaos.is_some());
1204    }
1205
1206    #[test]
1207    fn test_load_test_config_tier3() {
1208        let config = LoadTestConfig::tier3("full_test");
1209        assert_eq!(config.tier, 3);
1210        assert!(config.property_tests);
1211        assert!(config.benchmark);
1212        assert!(config.chaos.is_some());
1213    }
1214
1215    #[test]
1216    fn test_load_test_config_builder() {
1217        let config = LoadTestConfig::new("custom")
1218            .with_chaos(ChaosConfig::time_warp())
1219            .with_property_tests()
1220            .with_benchmarks()
1221            .with_duration(1000);
1222
1223        assert_eq!(config.name, "custom");
1224        assert!(config.chaos.is_some());
1225        assert!(config.property_tests);
1226        assert!(config.benchmark);
1227        assert_eq!(config.duration_frames, 1000);
1228    }
1229
1230    // -------------------------------------------------------------------------
1231    // LoadTestResult tests
1232    // -------------------------------------------------------------------------
1233
1234    #[test]
1235    fn test_load_test_result_pass() {
1236        let stats = FrameTimeStats::new();
1237        let result = LoadTestResult::pass("test", stats.report());
1238        assert!(result.passed);
1239        assert!(result.error.is_none());
1240    }
1241
1242    #[test]
1243    fn test_load_test_result_fail() {
1244        let result = LoadTestResult::fail("test", "something went wrong");
1245        assert!(!result.passed);
1246        assert!(result.error.is_some());
1247    }
1248
1249    // -------------------------------------------------------------------------
1250    // LoadTestSummary tests
1251    // -------------------------------------------------------------------------
1252
1253    #[test]
1254    fn test_load_test_summary_add() {
1255        let mut summary = LoadTestSummary::new();
1256        assert_eq!(summary.total, 0);
1257
1258        let stats = FrameTimeStats::new();
1259        summary.add(LoadTestResult::pass("test1", stats.report()));
1260        summary.add(LoadTestResult::fail("test2", "error"));
1261
1262        assert_eq!(summary.total, 2);
1263        assert_eq!(summary.passed, 1);
1264        assert_eq!(summary.failed, 1);
1265        assert!(!summary.all_passed());
1266    }
1267
1268    #[test]
1269    fn test_load_test_summary_all_passed() {
1270        let mut summary = LoadTestSummary::new();
1271        let stats = FrameTimeStats::new();
1272        summary.add(LoadTestResult::pass("test1", stats.report()));
1273        summary.add(LoadTestResult::pass("test2", stats.report()));
1274
1275        assert!(summary.all_passed());
1276    }
1277
1278    // -------------------------------------------------------------------------
1279    // Coverage Gap Tests - DriftDetector edge cases
1280    // -------------------------------------------------------------------------
1281
1282    #[test]
1283    fn test_drift_detector_calibrate_empty_samples() {
1284        let mut detector = DriftDetector::new(10, 2.0);
1285        detector.calibrate(&[]); // Empty samples - should return early
1286
1287        // Should not be calibrated after empty samples
1288        assert!(!detector.is_calibrated());
1289    }
1290
1291    #[test]
1292    fn test_drift_detector_incomplete_window() {
1293        let mut detector = DriftDetector::new(10, 2.0);
1294        detector.calibrate(&[1.0, 2.0, 3.0, 4.0, 5.0]);
1295
1296        // Add only 5 samples (need 10 for full window)
1297        for i in 0..5 {
1298            let _ = detector.observe(i as f64 + 1.0);
1299        }
1300
1301        // Drift detection should return None for incomplete window
1302        assert!(detector.detect_drift().is_none());
1303    }
1304
1305    #[test]
1306    fn test_drift_detector_zero_baseline_mean() {
1307        let mut detector = DriftDetector::new(5, 2.0);
1308        detector.calibrate(&[0.0, 0.0, 0.0, 0.0, 0.0]); // Zero mean baseline
1309
1310        // Fill the window
1311        for _ in 0..5 {
1312            let _ = detector.observe(1.0);
1313        }
1314
1315        // Division guard - should return None when baseline mean is near zero
1316        assert!(detector.detect_drift().is_none());
1317    }
1318
1319    #[test]
1320    fn test_drift_detector_no_drift_detected() {
1321        let mut detector = DriftDetector::new(5, 50.0); // High threshold (50%)
1322        detector.calibrate(&[10.0, 10.0, 10.0, 10.0, 10.0]);
1323
1324        // Add similar samples (within threshold)
1325        for _ in 0..5 {
1326            let _ = detector.observe(10.5); // Only 5% drift
1327        }
1328
1329        // No drift should be detected (below threshold)
1330        assert!(detector.detect_drift().is_none());
1331    }
1332
1333    #[test]
1334    fn test_drift_detector_window_stats_empty() {
1335        let detector = DriftDetector::new(10, 2.0);
1336        // No samples added, window is empty
1337        assert!(detector.window_stats().is_none());
1338    }
1339
1340    #[test]
1341    fn test_drift_detector_reset_methods() {
1342        let mut detector = DriftDetector::new(5, 2.0);
1343        detector.calibrate(&[1.0, 2.0, 3.0, 4.0, 5.0]);
1344
1345        // Add some samples
1346        for i in 0..5 {
1347            let _ = detector.observe(i as f64);
1348        }
1349
1350        // Partial reset - keeps calibration
1351        detector.reset();
1352        assert!(detector.is_calibrated()); // Still calibrated
1353        assert!(detector.window_stats().is_none()); // Window cleared
1354
1355        // Full reset - clears everything
1356        detector.full_reset();
1357        assert!(!detector.is_calibrated()); // Not calibrated anymore
1358    }
1359
1360    #[test]
1361    fn test_frame_time_stats_percentile_empty() {
1362        let stats = FrameTimeStats::new();
1363        // Empty samples should return 0.0
1364        assert_eq!(stats.percentile(50.0), 0.0);
1365        assert_eq!(stats.percentile(95.0), 0.0);
1366    }
1367
1368    #[test]
1369    fn test_chaos_config_config_sweep() {
1370        let config = ChaosConfig::config_sweep();
1371        assert!(matches!(config.scenario, ChaosScenario::ConfigSweep));
1372    }
1373
1374    #[test]
1375    fn test_chaos_config_all_factory_variants() {
1376        // Test all factory methods create correct variants
1377        let entity = ChaosConfig::entity_storm();
1378        assert!(matches!(entity.scenario, ChaosScenario::EntityStorm { .. }));
1379
1380        let input = ChaosConfig::input_flood();
1381        assert!(matches!(input.scenario, ChaosScenario::InputFlood { .. }));
1382
1383        let time = ChaosConfig::time_warp();
1384        assert!(matches!(time.scenario, ChaosScenario::TimeWarp { .. }));
1385
1386        let resize = ChaosConfig::resize_blitz();
1387        assert!(matches!(resize.scenario, ChaosScenario::ResizeBlitz { .. }));
1388
1389        let rng = ChaosConfig::rng_torture();
1390        assert!(matches!(rng.scenario, ChaosScenario::RngTorture { .. }));
1391
1392        let config_sweep = ChaosConfig::config_sweep();
1393        assert!(matches!(config_sweep.scenario, ChaosScenario::ConfigSweep));
1394    }
1395
1396    // ==================== High-Priority Coverage Tests ====================
1397
1398    #[test]
1399    fn test_chaos_results_record_dropped_inputs() {
1400        let mut results = ChaosResults::new();
1401        assert_eq!(results.inputs_dropped, 0);
1402
1403        results.record_dropped_inputs(5);
1404        assert_eq!(results.inputs_dropped, 5);
1405
1406        results.record_dropped_inputs(10);
1407        assert_eq!(results.inputs_dropped, 15);
1408    }
1409
1410    #[test]
1411    fn test_frame_time_stats_with_capacity() {
1412        let stats = FrameTimeStats::with_capacity(1000);
1413        assert!(stats.is_empty());
1414        assert_eq!(stats.len(), 0);
1415    }
1416
1417    #[test]
1418    fn test_frame_time_stats_clear() {
1419        let mut stats = FrameTimeStats::new();
1420        stats.record(10.0);
1421        stats.record(20.0);
1422        assert_eq!(stats.len(), 2);
1423
1424        stats.clear();
1425        assert!(stats.is_empty());
1426        assert_eq!(stats.len(), 0);
1427    }
1428
1429    #[test]
1430    fn test_frame_time_report_jitter() {
1431        let mut stats = FrameTimeStats::new();
1432        stats.record(5.0);
1433        stats.record(10.0);
1434        stats.record(15.0);
1435        let report = stats.report();
1436        assert!((report.jitter() - 10.0).abs() < 0.001);
1437    }
1438
1439    #[test]
1440    fn test_drift_report_is_improvement() {
1441        let regression = DriftReport {
1442            baseline_mean: 10.0,
1443            current_mean: 12.0,
1444            drift_percent: 20.0,
1445        };
1446        assert!(regression.is_regression());
1447        assert!(!regression.is_improvement());
1448
1449        let improvement = DriftReport {
1450            baseline_mean: 10.0,
1451            current_mean: 8.0,
1452            drift_percent: -20.0,
1453        };
1454        assert!(!improvement.is_regression());
1455        assert!(improvement.is_improvement());
1456    }
1457
1458    #[test]
1459    fn test_load_test_config_tier1_factory() {
1460        let config = LoadTestConfig::tier1("tier1-test");
1461        assert_eq!(config.name, "tier1-test");
1462        assert_eq!(config.tier, 1);
1463        assert!(config.chaos.is_none());
1464        assert!(!config.property_tests);
1465        assert!(!config.benchmark);
1466        assert_eq!(config.duration_frames, 60);
1467    }
1468
1469    #[test]
1470    fn test_load_test_config_tier2_factory() {
1471        let config = LoadTestConfig::tier2("tier2-test");
1472        assert_eq!(config.name, "tier2-test");
1473        assert_eq!(config.tier, 2);
1474        assert!(config.chaos.is_some());
1475        assert!(config.property_tests);
1476        assert!(!config.benchmark);
1477        assert_eq!(config.duration_frames, 300);
1478    }
1479
1480    #[test]
1481    fn test_load_test_config_tier3_factory() {
1482        let config = LoadTestConfig::tier3("tier3-test");
1483        assert_eq!(config.name, "tier3-test");
1484        assert_eq!(config.tier, 3);
1485        assert!(config.chaos.is_some());
1486        assert!(config.property_tests);
1487        assert!(config.benchmark);
1488        assert_eq!(config.duration_frames, 3600);
1489    }
1490
1491    #[test]
1492    fn test_load_test_result_pass_factory() {
1493        let mut stats = FrameTimeStats::new();
1494        stats.record(10.0);
1495        let report = stats.report();
1496
1497        let result = LoadTestResult::pass("pass-test", report);
1498        assert!(result.passed);
1499        assert_eq!(result.name, "pass-test");
1500        assert!(result.error.is_none());
1501        assert!(result.chaos_results.is_none());
1502    }
1503
1504    #[test]
1505    fn test_load_test_result_fail_factory() {
1506        let result = LoadTestResult::fail("fail-test", "Something went wrong");
1507        assert!(!result.passed);
1508        assert_eq!(result.name, "fail-test");
1509        assert_eq!(result.error.as_deref(), Some("Something went wrong"));
1510    }
1511
1512    #[test]
1513    fn test_load_test_summary_new_empty() {
1514        let summary = LoadTestSummary::new();
1515        assert_eq!(summary.total, 0);
1516        assert_eq!(summary.passed, 0);
1517        assert_eq!(summary.failed, 0);
1518        assert!(summary.results.is_empty());
1519        assert!(summary.all_passed());
1520    }
1521
1522    #[test]
1523    fn test_chaos_config_entity_storm_params() {
1524        let config = ChaosConfig::entity_storm();
1525        assert!(matches!(
1526            config.scenario,
1527            ChaosScenario::EntityStorm { max_entities: 1000 }
1528        ));
1529        assert_eq!(config.duration_frames, 600);
1530    }
1531
1532    #[test]
1533    fn test_chaos_config_input_flood_params() {
1534        let config = ChaosConfig::input_flood();
1535        assert!(matches!(
1536            config.scenario,
1537            ChaosScenario::InputFlood {
1538                events_per_frame: 100
1539            }
1540        ));
1541        assert_eq!(config.duration_frames, 300);
1542    }
1543
1544    #[test]
1545    fn test_chaos_config_time_warp_params() {
1546        let config = ChaosConfig::time_warp();
1547        assert!(matches!(config.scenario, ChaosScenario::TimeWarp { .. }));
1548    }
1549
1550    #[test]
1551    fn test_chaos_config_resize_blitz_params() {
1552        let config = ChaosConfig::resize_blitz();
1553        assert!(matches!(
1554            config.scenario,
1555            ChaosScenario::ResizeBlitz { frequency: 5 }
1556        ));
1557    }
1558
1559    #[test]
1560    fn test_chaos_config_rng_torture_params() {
1561        let config = ChaosConfig::rng_torture();
1562        assert!(matches!(
1563            config.scenario,
1564            ChaosScenario::RngTorture { iterations: 1000 }
1565        ));
1566    }
1567}