Skip to main content

probador/
simulation.rs

1//! Simulation Playback Module (PROBAR-SPEC-006 Section K)
2//!
3//! Implements deterministic replay, parameterized replay, Monte Carlo
4//! simulation, and chaos engineering for load test analysis.
5//!
6//! Based on research:
7//! - [C11] Wasm-R3 record-reduce-replay methodology
8
9#![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// =============================================================================
32// K.2 Simulation Modes
33// =============================================================================
34
35/// Simulation mode
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum SimulationMode {
38    /// Exact reproduction of recorded session
39    DeterministicReplay,
40    /// Replay with modified parameters
41    Parameterized {
42        /// Parameter multipliers
43        multipliers: HashMap<String, f64>,
44    },
45    /// Monte Carlo randomized variations
46    MonteCarlo {
47        /// Number of iterations
48        iterations: u32,
49        /// Random seed (for reproducibility)
50        seed: Option<u64>,
51    },
52    /// Chaos engineering with failure injection
53    Chaos {
54        /// Failures to inject
55        injections: Vec<FailureInjection>,
56    },
57}
58
59/// Failure injection for chaos engineering
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct FailureInjection {
62    /// Injection type
63    pub injection_type: InjectionType,
64    /// Probability (0.0 - 1.0)
65    pub probability: f64,
66    /// Target component
67    pub target: String,
68    /// Duration in milliseconds (for delays)
69    pub duration_ms: Option<u64>,
70}
71
72impl FailureInjection {
73    /// Create a latency injection
74    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    /// Create a packet loss injection
84    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    /// Create an error injection
94    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/// Types of failure injection
105#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
106pub enum InjectionType {
107    /// Add latency
108    Latency,
109    /// Drop packets
110    PacketLoss,
111    /// Return errors
112    Error,
113    /// Timeout
114    Timeout,
115    /// CPU throttle
116    CpuThrottle,
117    /// Memory pressure
118    MemoryPressure,
119}
120
121impl InjectionType {
122    /// Get display name
123    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// =============================================================================
136// K.1 Simulation Configuration
137// =============================================================================
138
139/// Simulation configuration
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SimulationConfig {
142    /// Base session file path
143    pub base_session: PathBuf,
144    /// Simulation mode
145    pub mode: SimulationMode,
146    /// Parameter variations for Monte Carlo
147    pub parameter_variations: HashMap<String, ParameterVariation>,
148    /// Output configuration
149    pub output: SimulationOutput,
150}
151
152impl SimulationConfig {
153    /// Create deterministic replay config
154    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    /// Create Monte Carlo config
164    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    /// Add parameter variation
177    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/// Parameter variation definition
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ParameterVariation {
187    /// Distribution type
188    pub distribution: Distribution,
189    /// Minimum value
190    pub min: f64,
191    /// Maximum value
192    pub max: f64,
193    /// Base value (center for normal distribution)
194    pub base: f64,
195}
196
197impl ParameterVariation {
198    /// Create uniform variation
199    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    /// Create normal variation
209    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    /// Sample a value (simple implementation without external RNG)
219    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                // Box-Muller approximation (using single random value)
224                let z = (random_value - 0.5) * 6.0; // Rough approximation
225                mean + z * std_dev
226            }
227            Distribution::Exponential { lambda } => -random_value.ln() / lambda,
228            Distribution::Poisson { lambda } => {
229                // Approximation for Poisson
230                (random_value * lambda * 2.0).round()
231            }
232        }
233    }
234}
235
236/// Statistical distribution
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub enum Distribution {
239    /// Uniform distribution
240    Uniform,
241    /// Normal (Gaussian) distribution
242    Normal {
243        /// Mean
244        mean: f64,
245        /// Standard deviation
246        std_dev: f64,
247    },
248    /// Exponential distribution
249    Exponential {
250        /// Rate parameter
251        lambda: f64,
252    },
253    /// Poisson distribution
254    Poisson {
255        /// Rate parameter
256        lambda: f64,
257    },
258}
259
260/// Simulation output configuration
261#[derive(Debug, Clone, Serialize, Deserialize, Default)]
262pub struct SimulationOutput {
263    /// Output directory
264    pub directory: Option<PathBuf>,
265    /// Save individual iteration results
266    pub save_iterations: bool,
267    /// Generate summary report
268    pub generate_summary: bool,
269}
270
271// =============================================================================
272// K.3 Monte Carlo Results
273// =============================================================================
274
275/// Monte Carlo simulation result
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct MonteCarloResult {
278    /// Number of iterations completed
279    pub iterations: u32,
280    /// Latency distribution (p95 across iterations)
281    pub latency_distribution: LatencyDistribution,
282    /// SLA probability results
283    pub sla_probabilities: Vec<SlaProbability>,
284    /// Sensitivity analysis
285    pub sensitivity_analysis: Vec<SensitivityFactor>,
286    /// Recommendations
287    pub recommendations: Vec<String>,
288}
289
290impl MonteCarloResult {
291    /// Create new result
292    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    /// Add SLA probability
303    pub fn add_sla(&mut self, sla: SlaProbability) {
304        self.sla_probabilities.push(sla);
305    }
306
307    /// Add sensitivity factor
308    pub fn add_sensitivity(&mut self, factor: SensitivityFactor) {
309        self.sensitivity_analysis.push(factor);
310    }
311
312    /// Add recommendation
313    pub fn add_recommendation(&mut self, rec: &str) {
314        self.recommendations.push(rec.to_string());
315    }
316}
317
318/// Latency distribution from Monte Carlo
319#[derive(Debug, Clone, Serialize, Deserialize, Default)]
320pub struct LatencyDistribution {
321    /// Mean latency
322    pub mean_ms: f64,
323    /// Standard deviation
324    pub std_dev_ms: f64,
325    /// 95% confidence interval lower bound
326    pub ci_lower_ms: f64,
327    /// 95% confidence interval upper bound
328    pub ci_upper_ms: f64,
329    /// Histogram buckets (for visualization)
330    pub histogram: Vec<(f64, u32)>, // (latency_ms, count)
331}
332
333impl LatencyDistribution {
334    /// Create from samples
335    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        // 95% CI = mean ± 1.96 * std_dev / sqrt(n)
346        let margin = 1.96 * std_dev / n.sqrt();
347
348        // Build histogram (10 buckets)
349        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/// SLA probability result
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct SlaProbability {
379    /// SLA target description
380    pub target: String,
381    /// Probability of meeting target (0.0 - 1.0)
382    pub probability: f64,
383    /// Risk level
384    pub risk: RiskLevel,
385}
386
387impl SlaProbability {
388    /// Create new SLA probability
389    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/// Risk level
409#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
410pub enum RiskLevel {
411    /// < 5% chance of missing
412    Minimal,
413    /// 5-20% chance of missing
414    Low,
415    /// 20-50% chance of missing
416    Medium,
417    /// > 50% chance of missing
418    High,
419}
420
421impl RiskLevel {
422    /// Get display string
423    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    /// Get bar representation
433    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/// Sensitivity factor from analysis
444#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct SensitivityFactor {
446    /// Parameter name
447    pub parameter: String,
448    /// Correlation coefficient with target metric
449    pub correlation: f64,
450    /// Impact level
451    pub impact: ImpactLevel,
452}
453
454impl SensitivityFactor {
455    /// Create new factor
456    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/// Impact level
474#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
475pub enum ImpactLevel {
476    /// Low impact
477    Low,
478    /// Medium impact
479    Medium,
480    /// High impact
481    High,
482}
483
484impl ImpactLevel {
485    /// Get display string
486    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    /// Get bar representation
495    pub fn bar(&self) -> &'static str {
496        match self {
497            Self::Low => "███",
498            Self::Medium => "████████████",
499            Self::High => "████████████████████",
500        }
501    }
502}
503
504// =============================================================================
505// K.4 Chaos Engineering Results
506// =============================================================================
507
508/// Chaos experiment result
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct ChaosResult {
511    /// Experiment name
512    pub experiment_name: String,
513    /// Injections applied
514    pub injections: Vec<FailureInjection>,
515    /// System behavior observations
516    pub observations: Vec<ChaosObservation>,
517    /// Did system degrade gracefully?
518    pub graceful_degradation: bool,
519    /// Recovery time in milliseconds
520    pub recovery_time_ms: Option<u64>,
521}
522
523impl ChaosResult {
524    /// Create new result
525    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    /// Add observation
536    pub fn add_observation(&mut self, obs: ChaosObservation) {
537        // Check if any observation indicates non-graceful degradation
538        if matches!(obs.severity, ObservationSeverity::Critical) {
539            self.graceful_degradation = false;
540        }
541        self.observations.push(obs);
542    }
543}
544
545/// Observation during chaos experiment
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct ChaosObservation {
548    /// Timestamp in milliseconds
549    pub timestamp_ms: u64,
550    /// Component affected
551    pub component: String,
552    /// Observation description
553    pub description: String,
554    /// Severity
555    pub severity: ObservationSeverity,
556}
557
558impl ChaosObservation {
559    /// Create new observation
560    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/// Observation severity
576#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
577pub enum ObservationSeverity {
578    /// Informational
579    Info,
580    /// Warning - degraded but functional
581    Warning,
582    /// Error - partial failure
583    Error,
584    /// Critical - complete failure
585    Critical,
586}
587
588impl ObservationSeverity {
589    /// Get symbol
590    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
600// =============================================================================
601// Rendering
602// =============================================================================
603
604/// Render Monte Carlo results as TUI
605pub 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    // Latency distribution
614    out.push_str("LATENCY DISTRIBUTION (p95 across iterations)\n");
615    out.push_str("┌─────────────────────────────────────────────────────────────────────────┐\n");
616
617    // Histogram visualization
618    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    // SLA probabilities
649    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    // Sensitivity analysis
676    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    // Recommendations
703    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
713/// Render chaos result as TUI
714pub 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    // Injections
721    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    // Observations
733    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    // Verdict
752    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
766/// Render as JSON
767pub fn render_monte_carlo_json(result: &MonteCarloResult) -> String {
768    serde_json::to_string_pretty(result).unwrap_or_else(|_| "{}".to_string())
769}
770
771/// Truncate string
772fn 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// =============================================================================
781// Tests
782// =============================================================================
783
784#[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        // Should be close to mean when random = 0.5
833        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        // Cover all InjectionType::name() branches
947        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        // ln(0.5) / 1.0 ≈ 0.693
986        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        // Should be around 5.0 (random=0.5 * lambda=5.0 * 2.0 = 5.0)
1000        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        // Add distribution
1092        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        // Add multiple SLAs with different risk levels
1097        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        // Add multiple sensitivity factors
1102        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        // Add multiple recommendations
1107        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}