1#![allow(clippy::must_use_candidate)]
10#![allow(clippy::missing_panics_doc)]
11#![allow(clippy::missing_errors_doc)]
12#![allow(clippy::module_name_repetitions)]
13#![allow(clippy::missing_const_for_fn)]
14#![allow(clippy::cast_possible_truncation)]
15#![allow(clippy::cast_precision_loss)]
16#![allow(clippy::cast_lossless)]
17#![allow(clippy::cast_sign_loss)]
18#![allow(clippy::suboptimal_flops)]
19#![allow(clippy::format_push_string)]
20#![allow(clippy::uninlined_format_args)]
21#![allow(clippy::return_self_not_must_use)]
22#![allow(clippy::cloned_instead_of_copied)]
23#![allow(clippy::useless_format)]
24#![allow(clippy::single_char_add_str)]
25#![allow(clippy::useless_vec)]
26
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29use std::path::PathBuf;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum SimulationMode {
38 DeterministicReplay,
40 Parameterized {
42 multipliers: HashMap<String, f64>,
44 },
45 MonteCarlo {
47 iterations: u32,
49 seed: Option<u64>,
51 },
52 Chaos {
54 injections: Vec<FailureInjection>,
56 },
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct FailureInjection {
62 pub injection_type: InjectionType,
64 pub probability: f64,
66 pub target: String,
68 pub duration_ms: Option<u64>,
70}
71
72impl FailureInjection {
73 pub fn latency(target: &str, probability: f64, delay_ms: u64) -> Self {
75 Self {
76 injection_type: InjectionType::Latency,
77 probability,
78 target: target.to_string(),
79 duration_ms: Some(delay_ms),
80 }
81 }
82
83 pub fn packet_loss(target: &str, probability: f64) -> Self {
85 Self {
86 injection_type: InjectionType::PacketLoss,
87 probability,
88 target: target.to_string(),
89 duration_ms: None,
90 }
91 }
92
93 pub fn error(target: &str, probability: f64) -> Self {
95 Self {
96 injection_type: InjectionType::Error,
97 probability,
98 target: target.to_string(),
99 duration_ms: None,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
106pub enum InjectionType {
107 Latency,
109 PacketLoss,
111 Error,
113 Timeout,
115 CpuThrottle,
117 MemoryPressure,
119}
120
121impl InjectionType {
122 pub fn name(&self) -> &'static str {
124 match self {
125 Self::Latency => "latency",
126 Self::PacketLoss => "packet_loss",
127 Self::Error => "error",
128 Self::Timeout => "timeout",
129 Self::CpuThrottle => "cpu_throttle",
130 Self::MemoryPressure => "memory_pressure",
131 }
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SimulationConfig {
142 pub base_session: PathBuf,
144 pub mode: SimulationMode,
146 pub parameter_variations: HashMap<String, ParameterVariation>,
148 pub output: SimulationOutput,
150}
151
152impl SimulationConfig {
153 pub fn deterministic(session_path: PathBuf) -> Self {
155 Self {
156 base_session: session_path,
157 mode: SimulationMode::DeterministicReplay,
158 parameter_variations: HashMap::new(),
159 output: SimulationOutput::default(),
160 }
161 }
162
163 pub fn monte_carlo(session_path: PathBuf, iterations: u32) -> Self {
165 Self {
166 base_session: session_path,
167 mode: SimulationMode::MonteCarlo {
168 iterations,
169 seed: None,
170 },
171 parameter_variations: HashMap::new(),
172 output: SimulationOutput::default(),
173 }
174 }
175
176 pub fn with_variation(mut self, name: &str, variation: ParameterVariation) -> Self {
178 self.parameter_variations
179 .insert(name.to_string(), variation);
180 self
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ParameterVariation {
187 pub distribution: Distribution,
189 pub min: f64,
191 pub max: f64,
193 pub base: f64,
195}
196
197impl ParameterVariation {
198 pub fn uniform(min: f64, max: f64) -> Self {
200 Self {
201 distribution: Distribution::Uniform,
202 min,
203 max,
204 base: (min + max) / 2.0,
205 }
206 }
207
208 pub fn normal(mean: f64, std_dev: f64) -> Self {
210 Self {
211 distribution: Distribution::Normal { mean, std_dev },
212 min: mean - 3.0 * std_dev,
213 max: mean + 3.0 * std_dev,
214 base: mean,
215 }
216 }
217
218 pub fn sample(&self, random_value: f64) -> f64 {
220 match &self.distribution {
221 Distribution::Uniform => self.min + random_value * (self.max - self.min),
222 Distribution::Normal { mean, std_dev } => {
223 let z = (random_value - 0.5) * 6.0; mean + z * std_dev
226 }
227 Distribution::Exponential { lambda } => -random_value.ln() / lambda,
228 Distribution::Poisson { lambda } => {
229 (random_value * lambda * 2.0).round()
231 }
232 }
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub enum Distribution {
239 Uniform,
241 Normal {
243 mean: f64,
245 std_dev: f64,
247 },
248 Exponential {
250 lambda: f64,
252 },
253 Poisson {
255 lambda: f64,
257 },
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, Default)]
262pub struct SimulationOutput {
263 pub directory: Option<PathBuf>,
265 pub save_iterations: bool,
267 pub generate_summary: bool,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct MonteCarloResult {
278 pub iterations: u32,
280 pub latency_distribution: LatencyDistribution,
282 pub sla_probabilities: Vec<SlaProbability>,
284 pub sensitivity_analysis: Vec<SensitivityFactor>,
286 pub recommendations: Vec<String>,
288}
289
290impl MonteCarloResult {
291 pub fn new(iterations: u32) -> Self {
293 Self {
294 iterations,
295 latency_distribution: LatencyDistribution::default(),
296 sla_probabilities: Vec::new(),
297 sensitivity_analysis: Vec::new(),
298 recommendations: Vec::new(),
299 }
300 }
301
302 pub fn add_sla(&mut self, sla: SlaProbability) {
304 self.sla_probabilities.push(sla);
305 }
306
307 pub fn add_sensitivity(&mut self, factor: SensitivityFactor) {
309 self.sensitivity_analysis.push(factor);
310 }
311
312 pub fn add_recommendation(&mut self, rec: &str) {
314 self.recommendations.push(rec.to_string());
315 }
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, Default)]
320pub struct LatencyDistribution {
321 pub mean_ms: f64,
323 pub std_dev_ms: f64,
325 pub ci_lower_ms: f64,
327 pub ci_upper_ms: f64,
329 pub histogram: Vec<(f64, u32)>, }
332
333impl LatencyDistribution {
334 pub fn from_samples(samples: &[f64]) -> Self {
336 if samples.is_empty() {
337 return Self::default();
338 }
339
340 let n = samples.len() as f64;
341 let mean = samples.iter().sum::<f64>() / n;
342 let variance = samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
343 let std_dev = variance.sqrt();
344
345 let margin = 1.96 * std_dev / n.sqrt();
347
348 let min = samples.iter().cloned().fold(f64::INFINITY, f64::min);
350 let max = samples.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
351 let bucket_size = (max - min) / 10.0;
352
353 let mut buckets = vec![0u32; 10];
354 for &sample in samples {
355 let idx = ((sample - min) / bucket_size) as usize;
356 let idx = idx.min(9);
357 buckets[idx] += 1;
358 }
359
360 let histogram: Vec<(f64, u32)> = buckets
361 .iter()
362 .enumerate()
363 .map(|(i, &count)| (min + (i as f64 + 0.5) * bucket_size, count))
364 .collect();
365
366 Self {
367 mean_ms: mean,
368 std_dev_ms: std_dev,
369 ci_lower_ms: mean - margin,
370 ci_upper_ms: mean + margin,
371 histogram,
372 }
373 }
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct SlaProbability {
379 pub target: String,
381 pub probability: f64,
383 pub risk: RiskLevel,
385}
386
387impl SlaProbability {
388 pub fn new(target: &str, probability: f64) -> Self {
390 let risk = if probability >= 0.95 {
391 RiskLevel::Minimal
392 } else if probability >= 0.80 {
393 RiskLevel::Low
394 } else if probability >= 0.50 {
395 RiskLevel::Medium
396 } else {
397 RiskLevel::High
398 };
399
400 Self {
401 target: target.to_string(),
402 probability,
403 risk,
404 }
405 }
406}
407
408#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
410pub enum RiskLevel {
411 Minimal,
413 Low,
415 Medium,
417 High,
419}
420
421impl RiskLevel {
422 pub fn as_str(&self) -> &'static str {
424 match self {
425 Self::Minimal => "MINIMAL",
426 Self::Low => "LOW",
427 Self::Medium => "MEDIUM",
428 Self::High => "HIGH",
429 }
430 }
431
432 pub fn bar(&self) -> &'static str {
434 match self {
435 Self::Minimal => "█",
436 Self::Low => "████",
437 Self::Medium => "██████████",
438 Self::High => "███████████████",
439 }
440 }
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct SensitivityFactor {
446 pub parameter: String,
448 pub correlation: f64,
450 pub impact: ImpactLevel,
452}
453
454impl SensitivityFactor {
455 pub fn new(parameter: &str, correlation: f64) -> Self {
457 let impact = if correlation.abs() >= 0.7 {
458 ImpactLevel::High
459 } else if correlation.abs() >= 0.4 {
460 ImpactLevel::Medium
461 } else {
462 ImpactLevel::Low
463 };
464
465 Self {
466 parameter: parameter.to_string(),
467 correlation,
468 impact,
469 }
470 }
471}
472
473#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
475pub enum ImpactLevel {
476 Low,
478 Medium,
480 High,
482}
483
484impl ImpactLevel {
485 pub fn as_str(&self) -> &'static str {
487 match self {
488 Self::Low => "LOW",
489 Self::Medium => "MEDIUM",
490 Self::High => "HIGH",
491 }
492 }
493
494 pub fn bar(&self) -> &'static str {
496 match self {
497 Self::Low => "███",
498 Self::Medium => "████████████",
499 Self::High => "████████████████████",
500 }
501 }
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct ChaosResult {
511 pub experiment_name: String,
513 pub injections: Vec<FailureInjection>,
515 pub observations: Vec<ChaosObservation>,
517 pub graceful_degradation: bool,
519 pub recovery_time_ms: Option<u64>,
521}
522
523impl ChaosResult {
524 pub fn new(name: &str) -> Self {
526 Self {
527 experiment_name: name.to_string(),
528 injections: Vec::new(),
529 observations: Vec::new(),
530 graceful_degradation: true,
531 recovery_time_ms: None,
532 }
533 }
534
535 pub fn add_observation(&mut self, obs: ChaosObservation) {
537 if matches!(obs.severity, ObservationSeverity::Critical) {
539 self.graceful_degradation = false;
540 }
541 self.observations.push(obs);
542 }
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct ChaosObservation {
548 pub timestamp_ms: u64,
550 pub component: String,
552 pub description: String,
554 pub severity: ObservationSeverity,
556}
557
558impl ChaosObservation {
559 pub fn new(
561 timestamp_ms: u64,
562 component: &str,
563 description: &str,
564 severity: ObservationSeverity,
565 ) -> Self {
566 Self {
567 timestamp_ms,
568 component: component.to_string(),
569 description: description.to_string(),
570 severity,
571 }
572 }
573}
574
575#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
577pub enum ObservationSeverity {
578 Info,
580 Warning,
582 Error,
584 Critical,
586}
587
588impl ObservationSeverity {
589 pub fn symbol(&self) -> &'static str {
591 match self {
592 Self::Info => "ℹ",
593 Self::Warning => "⚠",
594 Self::Error => "✗",
595 Self::Critical => "💀",
596 }
597 }
598}
599
600pub fn render_monte_carlo_report(result: &MonteCarloResult) -> String {
606 let mut out = String::new();
607
608 out.push_str("MONTE CARLO SIMULATION\n");
609 out.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
610
611 out.push_str(&format!("Iterations: {}\n\n", result.iterations));
612
613 out.push_str("LATENCY DISTRIBUTION (p95 across iterations)\n");
615 out.push_str("┌─────────────────────────────────────────────────────────────────────────┐\n");
616
617 if !result.latency_distribution.histogram.is_empty() {
619 let max_count = result
620 .latency_distribution
621 .histogram
622 .iter()
623 .map(|(_, c)| *c)
624 .max()
625 .unwrap_or(1);
626 for (latency, count) in &result.latency_distribution.histogram {
627 let bar_len = (*count as f64 / max_count as f64 * 30.0) as usize;
628 let bar: String = "█".repeat(bar_len.max(1));
629 out.push_str(&format!(
630 "│ {:>6.0}ms │ {:30} │ {:>4}\n",
631 latency, bar, count
632 ));
633 }
634 }
635
636 out.push_str(&format!(
637 "│ │\n"
638 ));
639 out.push_str(&format!(
640 "│ Mean: {:.0}ms StdDev: {:.0}ms 95%% CI: [{:.0}ms, {:.0}ms] │\n",
641 result.latency_distribution.mean_ms,
642 result.latency_distribution.std_dev_ms,
643 result.latency_distribution.ci_lower_ms,
644 result.latency_distribution.ci_upper_ms
645 ));
646 out.push_str("└─────────────────────────────────────────────────────────────────────────┘\n\n");
647
648 if !result.sla_probabilities.is_empty() {
650 out.push_str("FAILURE PROBABILITY\n");
651 out.push_str(
652 "┌─────────────────────────┬────────────────────────┬─────────────────────────┐\n",
653 );
654 out.push_str(
655 "│ SLA Target │ Probability of Meeting │ Risk Level │\n",
656 );
657 out.push_str(
658 "├─────────────────────────┼────────────────────────┼─────────────────────────┤\n",
659 );
660
661 for sla in &result.sla_probabilities {
662 out.push_str(&format!(
663 "│ {:<23} │ {:>20.1}% │ {:15} {:>7} │\n",
664 truncate(&sla.target, 23),
665 sla.probability * 100.0,
666 sla.risk.bar(),
667 sla.risk.as_str()
668 ));
669 }
670 out.push_str(
671 "└─────────────────────────┴────────────────────────┴─────────────────────────┘\n\n",
672 );
673 }
674
675 if !result.sensitivity_analysis.is_empty() {
677 out.push_str("SENSITIVITY ANALYSIS\n");
678 out.push_str(
679 "┌─────────────────────────┬──────────────────────┬───────────────────────────┐\n",
680 );
681 out.push_str(
682 "│ Parameter │ Correlation with p95 │ Impact │\n",
683 );
684 out.push_str(
685 "├─────────────────────────┼──────────────────────┼───────────────────────────┤\n",
686 );
687
688 for factor in &result.sensitivity_analysis {
689 out.push_str(&format!(
690 "│ {:<23} │ r = {:>16.2} │ {:20} {:>5} │\n",
691 truncate(&factor.parameter, 23),
692 factor.correlation,
693 factor.impact.bar(),
694 factor.impact.as_str()
695 ));
696 }
697 out.push_str(
698 "└─────────────────────────┴──────────────────────┴───────────────────────────┘\n\n",
699 );
700 }
701
702 if !result.recommendations.is_empty() {
704 out.push_str("RECOMMENDATIONS\n");
705 for (i, rec) in result.recommendations.iter().enumerate() {
706 out.push_str(&format!("{}. {}\n", i + 1, rec));
707 }
708 }
709
710 out
711}
712
713pub fn render_chaos_report(result: &ChaosResult) -> String {
715 let mut out = String::new();
716
717 out.push_str(&format!("CHAOS EXPERIMENT: {}\n", result.experiment_name));
718 out.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
719
720 out.push_str("INJECTIONS APPLIED\n");
722 for injection in &result.injections {
723 out.push_str(&format!(
724 " • {} on '{}' (probability: {:.0}%)\n",
725 injection.injection_type.name(),
726 injection.target,
727 injection.probability * 100.0
728 ));
729 }
730 out.push_str("\n");
731
732 out.push_str("OBSERVATIONS\n");
734 out.push_str("┌──────────┬─────────────┬─────────────────────────────────────────────────┐\n");
735 out.push_str("│ Time │ Component │ Observation │\n");
736 out.push_str("├──────────┼─────────────┼─────────────────────────────────────────────────┤\n");
737
738 for obs in &result.observations {
739 out.push_str(&format!(
740 "│ {:>6}ms │ {:<11} │ {} {:<45} │\n",
741 obs.timestamp_ms,
742 truncate(&obs.component, 11),
743 obs.severity.symbol(),
744 truncate(&obs.description, 45)
745 ));
746 }
747 out.push_str(
748 "└──────────┴─────────────┴─────────────────────────────────────────────────┘\n\n",
749 );
750
751 let verdict = if result.graceful_degradation {
753 "✓ System degraded gracefully"
754 } else {
755 "✗ System did NOT degrade gracefully"
756 };
757 out.push_str(&format!("VERDICT: {}\n", verdict));
758
759 if let Some(recovery) = result.recovery_time_ms {
760 out.push_str(&format!("Recovery Time: {}ms\n", recovery));
761 }
762
763 out
764}
765
766pub fn render_monte_carlo_json(result: &MonteCarloResult) -> String {
768 serde_json::to_string_pretty(result).unwrap_or_else(|_| "{}".to_string())
769}
770
771fn truncate(s: &str, max: usize) -> String {
773 if s.len() <= max {
774 s.to_string()
775 } else {
776 format!("{}…", &s[..max - 1])
777 }
778}
779
780#[cfg(test)]
785#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
786mod tests {
787 use super::*;
788
789 #[test]
790 fn test_failure_injection() {
791 let latency = FailureInjection::latency("network", 0.1, 100);
792 assert_eq!(latency.injection_type, InjectionType::Latency);
793 assert_eq!(latency.probability, 0.1);
794 assert_eq!(latency.duration_ms, Some(100));
795
796 let packet_loss = FailureInjection::packet_loss("network", 0.05);
797 assert_eq!(packet_loss.injection_type, InjectionType::PacketLoss);
798 }
799
800 #[test]
801 fn test_injection_type() {
802 assert_eq!(InjectionType::Latency.name(), "latency");
803 assert_eq!(InjectionType::PacketLoss.name(), "packet_loss");
804 }
805
806 #[test]
807 fn test_simulation_config() {
808 let config = SimulationConfig::deterministic(PathBuf::from("session.simular"));
809 assert!(matches!(config.mode, SimulationMode::DeterministicReplay));
810
811 let mc_config = SimulationConfig::monte_carlo(PathBuf::from("session.simular"), 1000);
812 assert!(matches!(
813 mc_config.mode,
814 SimulationMode::MonteCarlo {
815 iterations: 1000,
816 ..
817 }
818 ));
819 }
820
821 #[test]
822 fn test_parameter_variation_uniform() {
823 let var = ParameterVariation::uniform(10.0, 20.0);
824 let sample = var.sample(0.5);
825 assert!((10.0..=20.0).contains(&sample));
826 }
827
828 #[test]
829 fn test_parameter_variation_normal() {
830 let var = ParameterVariation::normal(100.0, 10.0);
831 let sample = var.sample(0.5);
832 assert!((sample - 100.0).abs() < 50.0);
834 }
835
836 #[test]
837 fn test_latency_distribution() {
838 let samples = vec![100.0, 110.0, 120.0, 130.0, 140.0];
839 let dist = LatencyDistribution::from_samples(&samples);
840
841 assert!((dist.mean_ms - 120.0).abs() < 0.001);
842 assert!(dist.std_dev_ms > 0.0);
843 }
844
845 #[test]
846 fn test_sla_probability() {
847 let sla = SlaProbability::new("p95 < 200ms", 0.95);
848 assert_eq!(sla.risk, RiskLevel::Minimal);
849
850 let sla_high = SlaProbability::new("p95 < 100ms", 0.30);
851 assert_eq!(sla_high.risk, RiskLevel::High);
852 }
853
854 #[test]
855 fn test_risk_level() {
856 assert_eq!(RiskLevel::Minimal.as_str(), "MINIMAL");
857 assert!(RiskLevel::High.bar().len() > RiskLevel::Minimal.bar().len());
858 }
859
860 #[test]
861 fn test_sensitivity_factor() {
862 let high = SensitivityFactor::new("network_latency", 0.85);
863 assert_eq!(high.impact, ImpactLevel::High);
864
865 let low = SensitivityFactor::new("cache_size", 0.15);
866 assert_eq!(low.impact, ImpactLevel::Low);
867 }
868
869 #[test]
870 fn test_monte_carlo_result() {
871 let mut result = MonteCarloResult::new(1000);
872 result.add_sla(SlaProbability::new("p95 < 200ms", 0.89));
873 result.add_sensitivity(SensitivityFactor::new("network", 0.82));
874 result.add_recommendation("Use CDN for WASM assets");
875
876 assert_eq!(result.iterations, 1000);
877 assert_eq!(result.sla_probabilities.len(), 1);
878 assert_eq!(result.sensitivity_analysis.len(), 1);
879 assert_eq!(result.recommendations.len(), 1);
880 }
881
882 #[test]
883 fn test_chaos_result() {
884 let mut result = ChaosResult::new("Network Partition");
885 result
886 .injections
887 .push(FailureInjection::packet_loss("network", 0.5));
888 result.add_observation(ChaosObservation::new(
889 1000,
890 "frontend",
891 "Requests timing out",
892 ObservationSeverity::Warning,
893 ));
894
895 assert!(result.graceful_degradation);
896
897 result.add_observation(ChaosObservation::new(
898 2000,
899 "backend",
900 "Complete failure",
901 ObservationSeverity::Critical,
902 ));
903 assert!(!result.graceful_degradation);
904 }
905
906 #[test]
907 fn test_observation_severity() {
908 assert_eq!(ObservationSeverity::Info.symbol(), "ℹ");
909 assert_eq!(ObservationSeverity::Critical.symbol(), "💀");
910 }
911
912 #[test]
913 fn test_render_monte_carlo_report() {
914 let mut result = MonteCarloResult::new(100);
915 result.latency_distribution = LatencyDistribution::from_samples(&[100.0, 200.0, 150.0]);
916 result.add_sla(SlaProbability::new("p95 < 300ms", 0.95));
917
918 let report = render_monte_carlo_report(&result);
919 assert!(report.contains("MONTE CARLO"));
920 assert!(report.contains("100"));
921 }
922
923 #[test]
924 fn test_render_chaos_report() {
925 let mut result = ChaosResult::new("Test Chaos");
926 result
927 .injections
928 .push(FailureInjection::latency("api", 0.1, 50));
929
930 let report = render_chaos_report(&result);
931 assert!(report.contains("Test Chaos"));
932 assert!(report.contains("INJECTIONS"));
933 }
934
935 #[test]
936 fn test_failure_injection_error() {
937 let injection = FailureInjection::error("database", 0.3);
938 assert_eq!(injection.injection_type, InjectionType::Error);
939 assert_eq!(injection.probability, 0.3);
940 assert_eq!(injection.target, "database");
941 assert!(injection.duration_ms.is_none());
942 }
943
944 #[test]
945 fn test_injection_type_all_names() {
946 assert_eq!(InjectionType::Latency.name(), "latency");
948 assert_eq!(InjectionType::PacketLoss.name(), "packet_loss");
949 assert_eq!(InjectionType::Error.name(), "error");
950 assert_eq!(InjectionType::Timeout.name(), "timeout");
951 assert_eq!(InjectionType::CpuThrottle.name(), "cpu_throttle");
952 assert_eq!(InjectionType::MemoryPressure.name(), "memory_pressure");
953 }
954
955 #[test]
956 fn test_simulation_config_with_variation() {
957 let config = SimulationConfig::deterministic(PathBuf::from("session.json")).with_variation(
958 "latency",
959 ParameterVariation {
960 distribution: Distribution::Normal {
961 mean: 50.0,
962 std_dev: 10.0,
963 },
964 min: 10.0,
965 max: 100.0,
966 base: 50.0,
967 },
968 );
969
970 assert!(config.parameter_variations.contains_key("latency"));
971 let variation = config.parameter_variations.get("latency").unwrap();
972 assert_eq!(variation.min, 10.0);
973 assert_eq!(variation.max, 100.0);
974 assert_eq!(variation.base, 50.0);
975 }
976
977 #[test]
978 fn test_parameter_variation_exponential() {
979 let var = ParameterVariation {
980 distribution: Distribution::Exponential { lambda: 1.0 },
981 min: 0.0,
982 max: 10.0,
983 base: 1.0,
984 };
985 let sample = var.sample(0.5);
987 assert!(sample > 0.0);
988 }
989
990 #[test]
991 fn test_parameter_variation_poisson() {
992 let var = ParameterVariation {
993 distribution: Distribution::Poisson { lambda: 5.0 },
994 min: 0.0,
995 max: 20.0,
996 base: 5.0,
997 };
998 let sample = var.sample(0.5);
999 assert!((sample - 5.0).abs() < 0.01);
1001 }
1002
1003 #[test]
1004 fn test_sla_probability_low_risk() {
1005 let sla = SlaProbability::new("p95 < 150ms", 0.85);
1006 assert_eq!(sla.risk, RiskLevel::Low);
1007 }
1008
1009 #[test]
1010 fn test_sla_probability_medium_risk() {
1011 let sla = SlaProbability::new("p95 < 100ms", 0.60);
1012 assert_eq!(sla.risk, RiskLevel::Medium);
1013 }
1014
1015 #[test]
1016 fn test_risk_level_all_bars() {
1017 assert_eq!(RiskLevel::Minimal.bar(), "█");
1018 assert_eq!(RiskLevel::Low.bar(), "████");
1019 assert_eq!(RiskLevel::Medium.bar(), "██████████");
1020 assert_eq!(RiskLevel::High.bar(), "███████████████");
1021 }
1022
1023 #[test]
1024 fn test_risk_level_all_strings() {
1025 assert_eq!(RiskLevel::Minimal.as_str(), "MINIMAL");
1026 assert_eq!(RiskLevel::Low.as_str(), "LOW");
1027 assert_eq!(RiskLevel::Medium.as_str(), "MEDIUM");
1028 assert_eq!(RiskLevel::High.as_str(), "HIGH");
1029 }
1030
1031 #[test]
1032 fn test_impact_level_all_strings() {
1033 assert_eq!(ImpactLevel::Low.as_str(), "LOW");
1034 assert_eq!(ImpactLevel::Medium.as_str(), "MEDIUM");
1035 assert_eq!(ImpactLevel::High.as_str(), "HIGH");
1036 }
1037
1038 #[test]
1039 fn test_impact_level_all_bars() {
1040 assert_eq!(ImpactLevel::Low.bar(), "███");
1041 assert_eq!(ImpactLevel::Medium.bar(), "████████████");
1042 assert_eq!(ImpactLevel::High.bar(), "████████████████████");
1043 }
1044
1045 #[test]
1046 fn test_sensitivity_factor_medium_impact() {
1047 let factor = SensitivityFactor::new("cache_hit_rate", 0.55);
1048 assert_eq!(factor.impact, ImpactLevel::Medium);
1049 }
1050
1051 #[test]
1052 fn test_observation_severity_all_symbols() {
1053 assert_eq!(ObservationSeverity::Info.symbol(), "ℹ");
1054 assert_eq!(ObservationSeverity::Warning.symbol(), "⚠");
1055 assert_eq!(ObservationSeverity::Error.symbol(), "✗");
1056 assert_eq!(ObservationSeverity::Critical.symbol(), "💀");
1057 }
1058
1059 #[test]
1060 fn test_latency_distribution_empty() {
1061 let dist = LatencyDistribution::from_samples(&[]);
1062 assert_eq!(dist.mean_ms, 0.0);
1063 assert_eq!(dist.std_dev_ms, 0.0);
1064 }
1065
1066 #[test]
1067 fn test_chaos_result_with_recovery_time() {
1068 let mut result = ChaosResult::new("Recovery Test");
1069 result.recovery_time_ms = Some(5000);
1070 assert_eq!(result.recovery_time_ms, Some(5000));
1071 }
1072
1073 #[test]
1074 fn test_chaos_observation_fields() {
1075 let obs = ChaosObservation::new(
1076 1500,
1077 "api_gateway",
1078 "High latency detected",
1079 ObservationSeverity::Error,
1080 );
1081 assert_eq!(obs.timestamp_ms, 1500);
1082 assert_eq!(obs.component, "api_gateway");
1083 assert_eq!(obs.description, "High latency detected");
1084 assert_eq!(obs.severity, ObservationSeverity::Error);
1085 }
1086
1087 #[test]
1088 fn test_monte_carlo_result_full_workflow() {
1089 let mut result = MonteCarloResult::new(500);
1090
1091 result.latency_distribution = LatencyDistribution::from_samples(&[
1093 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0,
1094 ]);
1095
1096 result.add_sla(SlaProbability::new("p95 < 200ms", 0.98));
1098 result.add_sla(SlaProbability::new("p99 < 300ms", 0.75));
1099 result.add_sla(SlaProbability::new("p99.9 < 500ms", 0.45));
1100
1101 result.add_sensitivity(SensitivityFactor::new("network_latency", 0.88));
1103 result.add_sensitivity(SensitivityFactor::new("db_connection_pool", 0.52));
1104 result.add_sensitivity(SensitivityFactor::new("cache_ttl", 0.15));
1105
1106 result.add_recommendation("Consider using connection pooling");
1108 result.add_recommendation("Increase cache TTL for static assets");
1109
1110 assert_eq!(result.iterations, 500);
1111 assert_eq!(result.sla_probabilities.len(), 3);
1112 assert_eq!(result.sensitivity_analysis.len(), 3);
1113 assert_eq!(result.recommendations.len(), 2);
1114 }
1115
1116 #[test]
1117 fn test_simulation_mode_parameterized() {
1118 let mut multipliers = HashMap::new();
1119 multipliers.insert("latency".to_string(), 1.5);
1120 multipliers.insert("throughput".to_string(), 0.8);
1121
1122 let mode = SimulationMode::Parameterized { multipliers };
1123 if let SimulationMode::Parameterized { multipliers: m } = mode {
1124 assert_eq!(m.len(), 2);
1125 assert_eq!(m.get("latency"), Some(&1.5));
1126 } else {
1127 panic!("Expected Parameterized mode");
1128 }
1129 }
1130
1131 #[test]
1132 fn test_simulation_mode_chaos() {
1133 let injections = vec![
1134 FailureInjection::latency("network", 0.1, 100),
1135 FailureInjection::packet_loss("network", 0.05),
1136 ];
1137
1138 let mode = SimulationMode::Chaos { injections };
1139 if let SimulationMode::Chaos { injections: i } = mode {
1140 assert_eq!(i.len(), 2);
1141 } else {
1142 panic!("Expected Chaos mode");
1143 }
1144 }
1145
1146 #[test]
1147 fn test_simulation_output_default() {
1148 let output = SimulationOutput::default();
1149 assert!(output.directory.is_none());
1150 assert!(!output.save_iterations);
1151 assert!(!output.generate_summary);
1152 }
1153}