1use std::collections::VecDeque;
31
32#[derive(Debug, Clone, Copy, PartialEq)]
46pub enum ChaosScenario {
47 EntityStorm {
49 max_entities: usize,
51 },
52 InputFlood {
54 events_per_frame: usize,
56 },
57 TimeWarp {
59 min_dt: f32,
61 max_dt: f32,
63 },
64 ResizeBlitz {
66 frequency: u32,
68 },
69 ConfigSweep,
71 RngTorture {
73 iterations: usize,
75 },
76}
77
78#[derive(Debug, Clone)]
89pub struct ChaosConfig {
90 pub scenario: ChaosScenario,
92 pub duration_frames: u64,
94 pub seed: u64,
96 pub intensity: f32,
98}
99
100impl ChaosConfig {
101 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
195 pub const fn with_intensity(mut self, intensity: f32) -> Self {
196 self.intensity = intensity;
197 self
198 }
199}
200
201#[derive(Debug, Clone, Default)]
203pub struct ChaosResults {
204 pub frames_executed: u64,
206 pub slow_frames: u64,
208 pub max_frame_time_ms: f64,
210 pub min_frame_time_ms: f64,
212 pub avg_frame_time_ms: f64,
214 pub panics: Vec<String>,
216 pub nan_detected: bool,
218 pub inf_detected: bool,
220 pub peak_memory_bytes: Option<usize>,
222 pub inputs_dropped: u64,
224 pub scenario: Option<ChaosScenario>,
226}
227
228impl ChaosResults {
229 #[must_use]
231 pub fn new() -> Self {
232 Self {
233 min_frame_time_ms: f64::INFINITY,
234 ..Default::default()
235 }
236 }
237
238 #[must_use]
240 pub fn passed(&self) -> bool {
241 self.panics.is_empty() && !self.nan_detected && !self.inf_detected
242 }
243
244 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 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 pub fn record_nan(&mut self) {
263 self.nan_detected = true;
264 }
265
266 pub fn record_inf(&mut self) {
268 self.inf_detected = true;
269 }
270
271 pub fn record_panic(&mut self, message: String) {
273 self.panics.push(message);
274 }
275
276 pub fn record_dropped_inputs(&mut self, count: u64) {
278 self.inputs_dropped += count;
279 }
280}
281
282#[derive(Debug, Clone)]
304pub struct FrameTimeStats {
305 samples: Vec<f64>,
307}
308
309impl Default for FrameTimeStats {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315impl FrameTimeStats {
316 #[must_use]
318 pub fn new() -> Self {
319 Self {
320 samples: Vec::new(),
321 }
322 }
323
324 #[must_use]
326 pub fn with_capacity(capacity: usize) -> Self {
327 Self {
328 samples: Vec::with_capacity(capacity),
329 }
330 }
331
332 pub fn record(&mut self, frame_time_ms: f64) {
334 self.samples.push(frame_time_ms);
335 }
336
337 #[must_use]
339 pub fn len(&self) -> usize {
340 self.samples.len()
341 }
342
343 #[must_use]
345 pub fn is_empty(&self) -> bool {
346 self.samples.is_empty()
347 }
348
349 #[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 #[must_use]
365 pub fn min(&self) -> f64 {
366 self.samples.iter().copied().fold(f64::INFINITY, f64::min)
367 }
368
369 #[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 #[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 #[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 #[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 pub fn clear(&mut self) {
417 self.samples.clear();
418 }
419}
420
421#[derive(Debug, Clone)]
423pub struct FrameTimeReport {
424 pub count: usize,
426 pub min: f64,
428 pub max: f64,
430 pub mean: f64,
432 pub std_dev: f64,
434 pub p50: f64,
436 pub p90: f64,
438 pub p95: f64,
440 pub p99: f64,
442}
443
444impl FrameTimeReport {
445 #[must_use]
447 pub fn meets_60fps(&self) -> bool {
448 self.p99 < 16.67
449 }
450
451 #[must_use]
453 pub fn meets_120fps(&self) -> bool {
454 self.p99 < 8.33
455 }
456
457 #[must_use]
459 pub fn meets_30fps(&self) -> bool {
460 self.p99 < 33.33
461 }
462
463 #[must_use]
465 pub fn jitter(&self) -> f64 {
466 self.max - self.min
467 }
468}
469
470#[derive(Debug, Clone)]
476pub enum AnomalyResult {
477 Normal,
479 Anomaly {
481 value: f64,
483 z_score: f64,
485 expected_range: (f64, f64),
487 },
488}
489
490impl AnomalyResult {
491 #[must_use]
493 pub fn is_anomaly(&self) -> bool {
494 matches!(self, Self::Anomaly { .. })
495 }
496
497 #[must_use]
499 pub fn is_normal(&self) -> bool {
500 matches!(self, Self::Normal)
501 }
502}
503
504#[derive(Debug, Clone)]
506pub struct DriftReport {
507 pub baseline_mean: f64,
509 pub current_mean: f64,
511 pub drift_percent: f64,
513}
514
515impl DriftReport {
516 #[must_use]
518 pub fn is_regression(&self) -> bool {
519 self.drift_percent > 0.0
520 }
521
522 #[must_use]
524 pub fn is_improvement(&self) -> bool {
525 self.drift_percent < 0.0
526 }
527}
528
529#[derive(Debug)]
553pub struct DriftDetector {
554 window: VecDeque<f64>,
556 window_size: usize,
558 z_threshold: f64,
560 baseline_mean: f64,
562 baseline_std: f64,
564 drift_threshold_percent: f64,
566}
567
568impl DriftDetector {
569 #[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 #[must_use]
589 pub fn with_drift_threshold(mut self, percent: f64) -> Self {
590 self.drift_threshold_percent = percent;
591 self
592 }
593
594 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 self.baseline_std = variance.sqrt().max(0.001);
612 }
613
614 #[must_use]
616 #[allow(clippy::float_cmp)] pub fn is_calibrated(&self) -> bool {
618 self.baseline_mean != 0.0 || self.baseline_std != 1.0
619 }
620
621 #[allow(clippy::suboptimal_flops)] pub fn observe(&mut self, frame_time_ms: f64) -> AnomalyResult {
624 self.window.push_back(frame_time_ms);
626 if self.window.len() > self.window_size {
627 let _ = self.window.pop_front();
628 }
629
630 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 #[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 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 #[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 pub fn reset(&mut self) {
690 self.window.clear();
691 }
692
693 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#[derive(Debug, Clone)]
707pub struct LoadTestConfig {
708 pub name: String,
710 pub chaos: Option<ChaosConfig>,
712 pub property_tests: bool,
714 pub benchmark: bool,
716 pub duration_frames: u64,
718 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 #[must_use]
738 pub fn new(name: impl Into<String>) -> Self {
739 Self {
740 name: name.into(),
741 ..Default::default()
742 }
743 }
744
745 #[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 #[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 #[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 #[must_use]
786 pub fn with_chaos(mut self, chaos: ChaosConfig) -> Self {
787 self.chaos = Some(chaos);
788 self
789 }
790
791 #[must_use]
793 pub fn with_property_tests(mut self) -> Self {
794 self.property_tests = true;
795 self
796 }
797
798 #[must_use]
800 pub fn with_benchmarks(mut self) -> Self {
801 self.benchmark = true;
802 self
803 }
804
805 #[must_use]
807 pub fn with_duration(mut self, frames: u64) -> Self {
808 self.duration_frames = frames;
809 self
810 }
811}
812
813#[derive(Debug, Clone)]
815pub struct LoadTestResult {
816 pub name: String,
818 pub passed: bool,
820 pub chaos_results: Option<ChaosResults>,
822 pub frame_stats: FrameTimeReport,
824 pub anomaly_count: usize,
826 pub property_failures: Vec<String>,
828 pub error: Option<String>,
830}
831
832impl LoadTestResult {
833 #[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 #[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#[derive(Debug, Clone)]
874pub struct LoadTestSummary {
875 pub total: usize,
877 pub passed: usize,
879 pub failed: usize,
881 pub results: Vec<LoadTestResult>,
883}
884
885impl LoadTestSummary {
886 #[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 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 #[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#[cfg(test)]
926#[allow(clippy::cast_lossless, clippy::unwrap_used, clippy::float_cmp)]
927mod tests {
928 use super::*;
929
930 #[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 #[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); results.record_frame_time(20.0); 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 #[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 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); }
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); }
1095 let report = stats.report();
1096 assert!(!report.meets_120fps());
1097 assert!(!report.meets_60fps());
1098 assert!(!report.meets_30fps());
1099 }
1100
1101 #[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); 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 for _ in 0..5 {
1141 let _ = detector.observe(1.5); }
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 for _ in 0..5 {
1156 let _ = detector.observe(1.05); }
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()); }
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()); }
1184
1185 #[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 #[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 #[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 #[test]
1283 fn test_drift_detector_calibrate_empty_samples() {
1284 let mut detector = DriftDetector::new(10, 2.0);
1285 detector.calibrate(&[]); 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 for i in 0..5 {
1298 let _ = detector.observe(i as f64 + 1.0);
1299 }
1300
1301 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]); for _ in 0..5 {
1312 let _ = detector.observe(1.0);
1313 }
1314
1315 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); detector.calibrate(&[10.0, 10.0, 10.0, 10.0, 10.0]);
1323
1324 for _ in 0..5 {
1326 let _ = detector.observe(10.5); }
1328
1329 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 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 for i in 0..5 {
1347 let _ = detector.observe(i as f64);
1348 }
1349
1350 detector.reset();
1352 assert!(detector.is_calibrated()); assert!(detector.window_stats().is_none()); detector.full_reset();
1357 assert!(!detector.is_calibrated()); }
1359
1360 #[test]
1361 fn test_frame_time_stats_percentile_empty() {
1362 let stats = FrameTimeStats::new();
1363 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 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 #[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}