Skip to main content

testx/
stress.rs

1use std::time::{Duration, Instant};
2
3use crate::adapters::{TestRunResult, TestStatus};
4
5/// Configuration for stress testing mode.
6#[derive(Debug, Clone)]
7pub struct StressConfig {
8    /// Number of times to run the test suite.
9    pub iterations: usize,
10    /// Stop on first failure.
11    pub fail_fast: bool,
12    /// Maximum total duration for all iterations.
13    pub max_duration: Option<Duration>,
14    /// Minimum pass rate threshold (0.0 - 1.0). Fails CI if any test is below this.
15    pub threshold: Option<f64>,
16    /// Number of parallel stress workers (0 = sequential).
17    pub parallel_workers: usize,
18}
19
20impl StressConfig {
21    pub fn new(iterations: usize) -> Self {
22        Self {
23            iterations,
24            fail_fast: false,
25            max_duration: None,
26            threshold: None,
27            parallel_workers: 0,
28        }
29    }
30
31    pub fn with_fail_fast(mut self, fail_fast: bool) -> Self {
32        self.fail_fast = fail_fast;
33        self
34    }
35
36    pub fn with_max_duration(mut self, duration: Duration) -> Self {
37        self.max_duration = Some(duration);
38        self
39    }
40
41    pub fn with_threshold(mut self, threshold: f64) -> Self {
42        self.threshold = Some(threshold.clamp(0.0, 1.0));
43        self
44    }
45
46    pub fn with_parallel_workers(mut self, workers: usize) -> Self {
47        self.parallel_workers = workers;
48        self
49    }
50}
51
52impl Default for StressConfig {
53    fn default() -> Self {
54        Self::new(10)
55    }
56}
57
58/// Result of a single stress iteration.
59#[derive(Debug, Clone)]
60pub struct IterationResult {
61    pub iteration: usize,
62    pub result: TestRunResult,
63    pub duration: Duration,
64}
65
66/// Aggregated stress test report.
67#[derive(Debug, Clone)]
68pub struct StressReport {
69    pub iterations_completed: usize,
70    pub iterations_requested: usize,
71    pub total_duration: Duration,
72    pub failures: Vec<IterationFailure>,
73    pub flaky_tests: Vec<FlakyTestReport>,
74    pub all_passed: bool,
75    pub stopped_early: bool,
76    /// Whether the threshold check passed (None if no threshold set).
77    pub threshold_passed: Option<bool>,
78    /// The configured threshold, if any.
79    pub threshold: Option<f64>,
80    /// Per-iteration timing data for trend analysis.
81    pub iteration_durations: Vec<Duration>,
82    /// Statistical summary of iteration durations.
83    pub timing_stats: Option<TimingStats>,
84}
85
86/// Statistical summary of timing data.
87#[derive(Debug, Clone)]
88pub struct TimingStats {
89    pub mean_ms: f64,
90    pub median_ms: f64,
91    pub std_dev_ms: f64,
92    /// Coefficient of variation (std_dev / mean). High CV = inconsistent timing.
93    pub cv: f64,
94    pub p95_ms: f64,
95    pub p99_ms: f64,
96}
97
98/// Severity classification for flaky tests.
99#[derive(Debug, Clone, PartialEq)]
100pub enum FlakySeverity {
101    /// Pass rate < 50% — almost always fails, likely a real bug
102    Critical,
103    /// Pass rate 50-80% — frequently flaky
104    High,
105    /// Pass rate 80-95% — occasionally flaky
106    Medium,
107    /// Pass rate > 95% — rarely flaky, possibly environment-dependent
108    Low,
109}
110
111impl FlakySeverity {
112    pub fn from_pass_rate(pass_rate: f64) -> Self {
113        match pass_rate {
114            r if r < 50.0 => FlakySeverity::Critical,
115            r if r < 80.0 => FlakySeverity::High,
116            r if r < 95.0 => FlakySeverity::Medium,
117            _ => FlakySeverity::Low,
118        }
119    }
120
121    pub fn label(&self) -> &str {
122        match self {
123            FlakySeverity::Critical => "CRITICAL",
124            FlakySeverity::High => "HIGH",
125            FlakySeverity::Medium => "MEDIUM",
126            FlakySeverity::Low => "LOW",
127        }
128    }
129
130    pub fn icon(&self) -> &str {
131        match self {
132            FlakySeverity::Critical => "🔴",
133            FlakySeverity::High => "🟠",
134            FlakySeverity::Medium => "🟡",
135            FlakySeverity::Low => "🟢",
136        }
137    }
138}
139
140/// A specific failure in a stress test iteration.
141#[derive(Debug, Clone)]
142pub struct IterationFailure {
143    pub iteration: usize,
144    pub failed_tests: Vec<String>,
145}
146
147/// A test that was flaky across stress iterations.
148#[derive(Debug, Clone)]
149pub struct FlakyTestReport {
150    pub name: String,
151    pub suite: String,
152    pub pass_count: usize,
153    pub fail_count: usize,
154    pub total_runs: usize,
155    pub pass_rate: f64,
156    pub durations: Vec<Duration>,
157    pub avg_duration: Duration,
158    pub max_duration: Duration,
159    pub min_duration: Duration,
160    /// Severity classification.
161    pub severity: FlakySeverity,
162    /// Wilson score confidence interval lower bound (95% confidence).
163    /// A more statistically rigorous measure of pass rate.
164    pub wilson_lower: f64,
165    /// Timing coefficient of variation for this specific test.
166    pub timing_cv: f64,
167}
168
169/// Accumulator that collects iteration results and produces a report.
170pub struct StressAccumulator {
171    config: StressConfig,
172    iterations: Vec<IterationResult>,
173    start_time: Instant,
174}
175
176impl StressAccumulator {
177    pub fn new(config: StressConfig) -> Self {
178        Self {
179            config,
180            iterations: Vec::new(),
181            start_time: Instant::now(),
182        }
183    }
184
185    /// Record one iteration's results. Returns true if we should continue.
186    pub fn record(&mut self, result: TestRunResult, duration: Duration) -> bool {
187        let iteration = self.iterations.len() + 1;
188        let has_failures = result.total_failed() > 0;
189
190        self.iterations.push(IterationResult {
191            iteration,
192            result,
193            duration,
194        });
195
196        if self.config.fail_fast && has_failures {
197            return false;
198        }
199
200        if let Some(max_dur) = self.config.max_duration
201            && self.start_time.elapsed() >= max_dur
202        {
203            return false;
204        }
205
206        iteration < self.config.iterations
207    }
208
209    /// How many iterations have been completed.
210    pub fn completed(&self) -> usize {
211        self.iterations.len()
212    }
213
214    /// Total iterations requested.
215    pub fn requested(&self) -> usize {
216        self.config.iterations
217    }
218
219    /// Check if the max duration has been exceeded.
220    pub fn is_time_exceeded(&self) -> bool {
221        self.config
222            .max_duration
223            .is_some_and(|d| self.start_time.elapsed() >= d)
224    }
225
226    /// Build the final stress report.
227    pub fn report(self) -> StressReport {
228        let iterations_completed = self.iterations.len();
229        let total_duration = self.start_time.elapsed();
230        let stopped_early = iterations_completed < self.config.iterations;
231
232        // Collect iteration durations
233        let iteration_durations: Vec<Duration> =
234            self.iterations.iter().map(|it| it.duration).collect();
235
236        // Compute timing stats
237        let timing_stats = compute_timing_stats(&iteration_durations);
238
239        // Collect failures per iteration
240        let failures: Vec<IterationFailure> = self
241            .iterations
242            .iter()
243            .filter(|it| it.result.total_failed() > 0)
244            .map(|it| {
245                let failed_tests: Vec<String> = it
246                    .result
247                    .suites
248                    .iter()
249                    .flat_map(|s| {
250                        s.tests
251                            .iter()
252                            .filter(|t| t.status == TestStatus::Failed)
253                            .map(move |t| format!("{}::{}", s.name, t.name))
254                    })
255                    .collect();
256
257                IterationFailure {
258                    iteration: it.iteration,
259                    failed_tests,
260                }
261            })
262            .collect();
263
264        // Analyze flaky tests: tests that both passed and failed across iterations
265        let flaky_tests = analyze_flaky_tests(&self.iterations);
266
267        let all_passed = failures.is_empty();
268
269        // Check threshold
270        let threshold_passed = self
271            .config
272            .threshold
273            .map(|threshold| flaky_tests.iter().all(|f| f.pass_rate / 100.0 >= threshold));
274
275        StressReport {
276            iterations_completed,
277            iterations_requested: self.config.iterations,
278            total_duration,
279            failures,
280            flaky_tests,
281            all_passed,
282            stopped_early,
283            threshold_passed,
284            threshold: self.config.threshold,
285            iteration_durations,
286            timing_stats,
287        }
288    }
289}
290
291/// Analyze test results across iterations to find flaky tests.
292fn analyze_flaky_tests(iterations: &[IterationResult]) -> Vec<FlakyTestReport> {
293    use std::collections::HashMap;
294
295    // Track per-test status across iterations: (suite, test) -> vec of (status, duration)
296    let mut test_history: HashMap<(String, String), Vec<(TestStatus, Duration)>> = HashMap::new();
297
298    for iteration in iterations {
299        for suite in &iteration.result.suites {
300            for test in &suite.tests {
301                test_history
302                    .entry((suite.name.clone(), test.name.clone()))
303                    .or_default()
304                    .push((test.status.clone(), test.duration));
305            }
306        }
307    }
308
309    let mut flaky_tests: Vec<FlakyTestReport> = test_history
310        .into_iter()
311        .filter_map(|((suite, name), history)| {
312            let pass_count = history
313                .iter()
314                .filter(|(s, _)| *s == TestStatus::Passed)
315                .count();
316            let fail_count = history
317                .iter()
318                .filter(|(s, _)| *s == TestStatus::Failed)
319                .count();
320            let total_runs = history.len();
321
322            // A test is flaky if it both passed and failed
323            if pass_count > 0 && fail_count > 0 {
324                let durations: Vec<Duration> = history.iter().map(|(_, d)| *d).collect();
325                let total_dur: Duration = durations.iter().sum();
326                let avg_duration = total_dur / total_runs as u32;
327                let max_duration = durations.iter().copied().max().unwrap_or_default();
328                let min_duration = durations.iter().copied().min().unwrap_or_default();
329                let pass_rate = pass_count as f64 / total_runs as f64 * 100.0;
330
331                // Wilson score lower bound (95% confidence)
332                let wilson_lower = wilson_score_lower(pass_count, total_runs, 1.96);
333
334                // Timing coefficient of variation
335                let timing_cv = compute_cv(&durations);
336
337                let severity = FlakySeverity::from_pass_rate(pass_rate);
338
339                Some(FlakyTestReport {
340                    name,
341                    suite,
342                    pass_count,
343                    fail_count,
344                    total_runs,
345                    pass_rate,
346                    durations,
347                    avg_duration,
348                    max_duration,
349                    min_duration,
350                    severity,
351                    wilson_lower,
352                    timing_cv,
353                })
354            } else {
355                None
356            }
357        })
358        .collect();
359
360    // Sort by pass rate (lowest = most flaky)
361    flaky_tests.sort_by(|a, b| {
362        a.pass_rate
363            .partial_cmp(&b.pass_rate)
364            .unwrap_or(std::cmp::Ordering::Equal)
365    });
366
367    flaky_tests
368}
369
370/// Wilson score confidence interval lower bound.
371/// Gives a statistically meaningful lower bound on the true pass rate,
372/// accounting for sample size. Better than raw pass_rate for small N.
373fn wilson_score_lower(successes: usize, total: usize, z: f64) -> f64 {
374    if total == 0 {
375        return 0.0;
376    }
377    let n = total as f64;
378    let p = successes as f64 / n;
379    let z2 = z * z;
380    let denominator = 1.0 + z2 / n;
381    let center = p + z2 / (2.0 * n);
382    let spread = z * (p * (1.0 - p) / n + z2 / (4.0 * n * n)).sqrt();
383    ((center - spread) / denominator).max(0.0)
384}
385
386/// Compute the coefficient of variation for a set of durations.
387fn compute_cv(durations: &[Duration]) -> f64 {
388    if durations.len() < 2 {
389        return 0.0;
390    }
391    let values: Vec<f64> = durations.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
392    let n = values.len() as f64;
393    let mean = values.iter().sum::<f64>() / n;
394    if mean == 0.0 {
395        return 0.0;
396    }
397    let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
398    let std_dev = variance.sqrt();
399    std_dev / mean
400}
401
402/// Compute timing statistics for a set of durations.
403fn compute_timing_stats(durations: &[Duration]) -> Option<TimingStats> {
404    if durations.is_empty() {
405        return None;
406    }
407
408    let mut ms_values: Vec<f64> = durations.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
409    let n = ms_values.len() as f64;
410
411    let mean = ms_values.iter().sum::<f64>() / n;
412
413    ms_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
414
415    let median = if ms_values.len().is_multiple_of(2) {
416        let mid = ms_values.len() / 2;
417        (ms_values[mid - 1] + ms_values[mid]) / 2.0
418    } else {
419        ms_values[ms_values.len() / 2]
420    };
421
422    let variance = if ms_values.len() > 1 {
423        ms_values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0)
424    } else {
425        0.0
426    };
427    let std_dev = variance.sqrt();
428    let cv = if mean > 0.0 { std_dev / mean } else { 0.0 };
429
430    let p95_idx = ((ms_values.len() as f64 * 0.95).ceil() as usize)
431        .min(ms_values.len())
432        .saturating_sub(1);
433    let p99_idx = ((ms_values.len() as f64 * 0.99).ceil() as usize)
434        .min(ms_values.len())
435        .saturating_sub(1);
436
437    Some(TimingStats {
438        mean_ms: mean,
439        median_ms: median,
440        std_dev_ms: std_dev,
441        cv,
442        p95_ms: ms_values[p95_idx],
443        p99_ms: ms_values[p99_idx],
444    })
445}
446
447/// Format a stress report for display.
448pub fn format_stress_report(report: &StressReport) -> String {
449    let mut lines = Vec::new();
450
451    lines.push(format!(
452        "Stress Test Report: {}/{} iterations in {:.2}s",
453        report.iterations_completed,
454        report.iterations_requested,
455        report.total_duration.as_secs_f64(),
456    ));
457
458    if report.stopped_early {
459        lines.push("  (stopped early)".to_string());
460    }
461
462    lines.push(String::new());
463
464    if report.all_passed {
465        lines.push(format!(
466            "  All {} iterations passed — no flaky tests detected!",
467            report.iterations_completed
468        ));
469    } else {
470        lines.push(format!(
471            "  {} iteration(s) had failures",
472            report.failures.len()
473        ));
474
475        for failure in &report.failures {
476            lines.push(format!("  Iteration {}:", failure.iteration));
477            for test in &failure.failed_tests {
478                lines.push(format!("    - {}", test));
479            }
480        }
481    }
482
483    // Timing statistics
484    if let Some(stats) = &report.timing_stats {
485        lines.push(String::new());
486        lines.push("  Timing Statistics:".to_string());
487        lines.push(format!(
488            "    Mean: {:.1}ms | Median: {:.1}ms | Std Dev: {:.1}ms",
489            stats.mean_ms, stats.median_ms, stats.std_dev_ms
490        ));
491        lines.push(format!(
492            "    P95: {:.1}ms | P99: {:.1}ms | CV: {:.2}",
493            stats.p95_ms, stats.p99_ms, stats.cv
494        ));
495        if stats.cv > 0.3 {
496            lines.push(
497                "    ⚠ High timing variance detected — results may be environment-sensitive"
498                    .to_string(),
499            );
500        }
501    }
502
503    if !report.flaky_tests.is_empty() {
504        lines.push(String::new());
505        lines.push(format!(
506            "  Flaky tests detected ({}):",
507            report.flaky_tests.len()
508        ));
509        for flaky in &report.flaky_tests {
510            lines.push(format!(
511                "    {} [{}] {} ({}/{} passed, {:.1}% pass rate, wilson≥{:.1}%, avg {:.1}ms, cv={:.2})",
512                flaky.severity.icon(),
513                flaky.severity.label(),
514                flaky.name,
515                flaky.pass_count,
516                flaky.total_runs,
517                flaky.pass_rate,
518                flaky.wilson_lower * 100.0,
519                flaky.avg_duration.as_secs_f64() * 1000.0,
520                flaky.timing_cv,
521            ));
522        }
523    }
524
525    // Threshold result
526    if let (Some(threshold), Some(passed)) = (report.threshold, report.threshold_passed) {
527        lines.push(String::new());
528        if passed {
529            lines.push(format!(
530                "  ✅ Threshold check passed (minimum {:.0}% pass rate)",
531                threshold * 100.0
532            ));
533        } else {
534            lines.push(format!(
535                "  ❌ Threshold check FAILED (minimum {:.0}% pass rate required)",
536                threshold * 100.0
537            ));
538        }
539    }
540
541    lines.join("\n")
542}
543
544/// Produce a JSON representation of the stress report.
545pub fn stress_report_json(report: &StressReport) -> serde_json::Value {
546    let flaky: Vec<serde_json::Value> = report
547        .flaky_tests
548        .iter()
549        .map(|f| {
550            serde_json::json!({
551                "name": f.name,
552                "suite": f.suite,
553                "pass_count": f.pass_count,
554                "fail_count": f.fail_count,
555                "total_runs": f.total_runs,
556                "pass_rate": f.pass_rate,
557                "severity": f.severity.label(),
558                "wilson_lower": f.wilson_lower,
559                "timing_cv": f.timing_cv,
560                "avg_duration_ms": f.avg_duration.as_secs_f64() * 1000.0,
561                "min_duration_ms": f.min_duration.as_secs_f64() * 1000.0,
562                "max_duration_ms": f.max_duration.as_secs_f64() * 1000.0,
563            })
564        })
565        .collect();
566
567    let failures: Vec<serde_json::Value> = report
568        .failures
569        .iter()
570        .map(|f| {
571            serde_json::json!({
572                "iteration": f.iteration,
573                "failed_tests": f.failed_tests,
574            })
575        })
576        .collect();
577
578    let mut json = serde_json::json!({
579        "iterations_completed": report.iterations_completed,
580        "iterations_requested": report.iterations_requested,
581        "total_duration_ms": report.total_duration.as_secs_f64() * 1000.0,
582        "all_passed": report.all_passed,
583        "stopped_early": report.stopped_early,
584        "failures": failures,
585        "flaky_tests": flaky,
586    });
587
588    if let Some(stats) = &report.timing_stats {
589        json["timing_stats"] = serde_json::json!({
590            "mean_ms": stats.mean_ms,
591            "median_ms": stats.median_ms,
592            "std_dev_ms": stats.std_dev_ms,
593            "cv": stats.cv,
594            "p95_ms": stats.p95_ms,
595            "p99_ms": stats.p99_ms,
596        });
597    }
598
599    if let Some(threshold) = report.threshold {
600        json["threshold"] = serde_json::json!(threshold);
601        json["threshold_passed"] = serde_json::json!(report.threshold_passed);
602    }
603
604    json
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610    use crate::adapters::{TestCase, TestError, TestSuite};
611
612    fn make_passing_result(num_tests: usize) -> TestRunResult {
613        TestRunResult {
614            suites: vec![TestSuite {
615                name: "suite".to_string(),
616                tests: (0..num_tests)
617                    .map(|i| TestCase {
618                        name: format!("test_{}", i),
619                        status: TestStatus::Passed,
620                        duration: Duration::from_millis(10),
621                        error: None,
622                    })
623                    .collect(),
624            }],
625            duration: Duration::from_millis(100),
626            raw_exit_code: 0,
627        }
628    }
629
630    fn make_mixed_result(pass: usize, fail: usize) -> TestRunResult {
631        let mut tests: Vec<TestCase> = (0..pass)
632            .map(|i| TestCase {
633                name: format!("pass_{}", i),
634                status: TestStatus::Passed,
635                duration: Duration::from_millis(10),
636                error: None,
637            })
638            .collect();
639
640        for i in 0..fail {
641            tests.push(TestCase {
642                name: format!("fail_{}", i),
643                status: TestStatus::Failed,
644                duration: Duration::from_millis(10),
645                error: Some(TestError {
646                    message: "assertion failed".to_string(),
647                    location: None,
648                }),
649            });
650        }
651
652        TestRunResult {
653            suites: vec![TestSuite {
654                name: "suite".to_string(),
655                tests,
656            }],
657            duration: Duration::from_millis(100),
658            raw_exit_code: 1,
659        }
660    }
661
662    #[test]
663    fn stress_config_defaults() {
664        let cfg = StressConfig::default();
665        assert_eq!(cfg.iterations, 10);
666        assert!(!cfg.fail_fast);
667        assert!(cfg.max_duration.is_none());
668    }
669
670    #[test]
671    fn stress_config_builder() {
672        let cfg = StressConfig::new(100)
673            .with_fail_fast(true)
674            .with_max_duration(Duration::from_secs(60));
675
676        assert_eq!(cfg.iterations, 100);
677        assert!(cfg.fail_fast);
678        assert_eq!(cfg.max_duration, Some(Duration::from_secs(60)));
679    }
680
681    #[test]
682    fn accumulator_all_passing() {
683        let cfg = StressConfig::new(3);
684        let mut acc = StressAccumulator::new(cfg);
685
686        assert!(acc.record(make_passing_result(5), Duration::from_millis(100)));
687        assert!(acc.record(make_passing_result(5), Duration::from_millis(100)));
688        assert!(!acc.record(make_passing_result(5), Duration::from_millis(100)));
689
690        let report = acc.report();
691        assert!(report.all_passed);
692        assert_eq!(report.iterations_completed, 3);
693        assert_eq!(report.iterations_requested, 3);
694        assert!(report.failures.is_empty());
695        assert!(report.flaky_tests.is_empty());
696        assert!(!report.stopped_early);
697    }
698
699    #[test]
700    fn accumulator_fail_fast() {
701        let cfg = StressConfig::new(10).with_fail_fast(true);
702        let mut acc = StressAccumulator::new(cfg);
703
704        assert!(acc.record(make_passing_result(5), Duration::from_millis(100)));
705        // Second iteration fails — should stop
706        assert!(!acc.record(make_mixed_result(3, 2), Duration::from_millis(100)));
707
708        let report = acc.report();
709        assert!(!report.all_passed);
710        assert_eq!(report.iterations_completed, 2);
711        assert!(report.stopped_early);
712        assert_eq!(report.failures.len(), 1);
713        assert_eq!(report.failures[0].iteration, 2);
714    }
715
716    #[test]
717    fn accumulator_without_fail_fast() {
718        let cfg = StressConfig::new(3);
719        let mut acc = StressAccumulator::new(cfg);
720
721        assert!(acc.record(make_passing_result(5), Duration::from_millis(100)));
722        assert!(acc.record(make_mixed_result(3, 2), Duration::from_millis(100)));
723        assert!(!acc.record(make_passing_result(5), Duration::from_millis(100)));
724
725        let report = acc.report();
726        assert!(!report.all_passed);
727        assert_eq!(report.iterations_completed, 3);
728        assert!(!report.stopped_early);
729        assert_eq!(report.failures.len(), 1);
730    }
731
732    #[test]
733    fn flaky_test_detection() {
734        let cfg = StressConfig::new(3);
735        let mut acc = StressAccumulator::new(cfg);
736
737        // Iteration 1: test_0 passes
738        acc.record(make_passing_result(3), Duration::from_millis(100));
739
740        // Iteration 2: test_0 fails (make it flaky)
741        let mut r2 = make_passing_result(3);
742        r2.suites[0].tests[0].status = TestStatus::Failed;
743        r2.suites[0].tests[0].error = Some(TestError {
744            message: "flaky!".to_string(),
745            location: None,
746        });
747        r2.raw_exit_code = 1;
748        acc.record(r2, Duration::from_millis(100));
749
750        // Iteration 3: test_0 passes again
751        acc.record(make_passing_result(3), Duration::from_millis(100));
752
753        let report = acc.report();
754        assert_eq!(report.flaky_tests.len(), 1);
755        assert_eq!(report.flaky_tests[0].name, "test_0");
756        assert_eq!(report.flaky_tests[0].pass_count, 2);
757        assert_eq!(report.flaky_tests[0].fail_count, 1);
758        assert_eq!(report.flaky_tests[0].total_runs, 3);
759    }
760
761    #[test]
762    fn consistently_failing_not_flaky() {
763        let cfg = StressConfig::new(3);
764        let mut acc = StressAccumulator::new(cfg);
765
766        // All iterations have the same failure
767        acc.record(make_mixed_result(3, 1), Duration::from_millis(100));
768        acc.record(make_mixed_result(3, 1), Duration::from_millis(100));
769        acc.record(make_mixed_result(3, 1), Duration::from_millis(100));
770
771        let report = acc.report();
772        // fail_0 always fails — not flaky, just broken
773        assert!(report.flaky_tests.is_empty());
774    }
775
776    #[test]
777    fn consistently_passing_not_flaky() {
778        let cfg = StressConfig::new(5);
779        let mut acc = StressAccumulator::new(cfg);
780
781        for _ in 0..5 {
782            acc.record(make_passing_result(3), Duration::from_millis(100));
783        }
784
785        let report = acc.report();
786        assert!(report.flaky_tests.is_empty());
787    }
788
789    #[test]
790    fn format_report_all_passing() {
791        let report = StressReport {
792            iterations_completed: 10,
793            iterations_requested: 10,
794            total_duration: Duration::from_secs(5),
795            failures: vec![],
796            flaky_tests: vec![],
797            all_passed: true,
798            stopped_early: false,
799            threshold_passed: None,
800            threshold: None,
801            iteration_durations: vec![Duration::from_millis(500); 10],
802            timing_stats: None,
803        };
804
805        let output = format_stress_report(&report);
806        assert!(output.contains("10/10 iterations"));
807        assert!(output.contains("no flaky tests"));
808    }
809
810    #[test]
811    fn format_report_with_failures() {
812        let report = StressReport {
813            iterations_completed: 5,
814            iterations_requested: 10,
815            total_duration: Duration::from_secs(3),
816            failures: vec![IterationFailure {
817                iteration: 3,
818                failed_tests: vec!["suite::test_1".to_string()],
819            }],
820            flaky_tests: vec![FlakyTestReport {
821                name: "test_1".to_string(),
822                suite: "suite".to_string(),
823                pass_count: 4,
824                fail_count: 1,
825                total_runs: 5,
826                pass_rate: 80.0,
827                durations: vec![Duration::from_millis(10); 5],
828                avg_duration: Duration::from_millis(10),
829                max_duration: Duration::from_millis(15),
830                min_duration: Duration::from_millis(8),
831                severity: FlakySeverity::Medium,
832                wilson_lower: 0.449,
833                timing_cv: 0.0,
834            }],
835            all_passed: false,
836            stopped_early: true,
837            threshold_passed: None,
838            threshold: None,
839            iteration_durations: vec![Duration::from_millis(600); 5],
840            timing_stats: None,
841        };
842
843        let output = format_stress_report(&report);
844        assert!(output.contains("stopped early"));
845        assert!(output.contains("Iteration 3"));
846        assert!(output.contains("Flaky tests detected"));
847        assert!(output.contains("80.0% pass rate"));
848        assert!(output.contains("MEDIUM"));
849    }
850
851    #[test]
852    fn accumulator_completed_count() {
853        let cfg = StressConfig::new(5);
854        let mut acc = StressAccumulator::new(cfg);
855
856        assert_eq!(acc.completed(), 0);
857        assert_eq!(acc.requested(), 5);
858
859        acc.record(make_passing_result(3), Duration::from_millis(100));
860        assert_eq!(acc.completed(), 1);
861
862        acc.record(make_passing_result(3), Duration::from_millis(100));
863        assert_eq!(acc.completed(), 2);
864    }
865
866    #[test]
867    fn flaky_test_duration_stats() {
868        let cfg = StressConfig::new(3);
869        let mut acc = StressAccumulator::new(cfg);
870
871        // Three iterations with varying duration
872        let mut r1 = make_passing_result(1);
873        r1.suites[0].tests[0].duration = Duration::from_millis(10);
874        acc.record(r1, Duration::from_millis(100));
875
876        let mut r2 = make_passing_result(1);
877        r2.suites[0].tests[0].status = TestStatus::Failed;
878        r2.suites[0].tests[0].error = Some(TestError {
879            message: "fail".to_string(),
880            location: None,
881        });
882        r2.suites[0].tests[0].duration = Duration::from_millis(20);
883        r2.raw_exit_code = 1;
884        acc.record(r2, Duration::from_millis(100));
885
886        let mut r3 = make_passing_result(1);
887        r3.suites[0].tests[0].duration = Duration::from_millis(30);
888        acc.record(r3, Duration::from_millis(100));
889
890        let report = acc.report();
891        assert_eq!(report.flaky_tests.len(), 1);
892        let flaky = &report.flaky_tests[0];
893        assert_eq!(flaky.min_duration, Duration::from_millis(10));
894        assert_eq!(flaky.max_duration, Duration::from_millis(30));
895        assert_eq!(flaky.avg_duration, Duration::from_millis(20));
896    }
897
898    #[test]
899    fn multiple_flaky_tests_sorted_by_pass_rate() {
900        let cfg = StressConfig::new(4);
901        let mut acc = StressAccumulator::new(cfg);
902
903        // Create results where test_a fails 3/4 times (25% pass rate)
904        // and test_b fails 1/4 times (75% pass rate)
905        for i in 0..4 {
906            let result = TestRunResult {
907                suites: vec![TestSuite {
908                    name: "suite".to_string(),
909                    tests: vec![
910                        TestCase {
911                            name: "test_a".to_string(),
912                            status: if i == 0 {
913                                TestStatus::Passed
914                            } else {
915                                TestStatus::Failed
916                            },
917                            duration: Duration::from_millis(10),
918                            error: if i == 0 {
919                                None
920                            } else {
921                                Some(TestError {
922                                    message: "fail".into(),
923                                    location: None,
924                                })
925                            },
926                        },
927                        TestCase {
928                            name: "test_b".to_string(),
929                            status: if i == 2 {
930                                TestStatus::Failed
931                            } else {
932                                TestStatus::Passed
933                            },
934                            duration: Duration::from_millis(10),
935                            error: if i == 2 {
936                                Some(TestError {
937                                    message: "fail".into(),
938                                    location: None,
939                                })
940                            } else {
941                                None
942                            },
943                        },
944                    ],
945                }],
946                duration: Duration::from_millis(100),
947                raw_exit_code: if i == 0 { 0 } else { 1 },
948            };
949            acc.record(result, Duration::from_millis(100));
950        }
951
952        let report = acc.report();
953        assert_eq!(report.flaky_tests.len(), 2);
954
955        // Should be sorted by pass rate (lowest first)
956        assert_eq!(report.flaky_tests[0].name, "test_a");
957        assert_eq!(report.flaky_tests[1].name, "test_b");
958        assert!(report.flaky_tests[0].pass_rate < report.flaky_tests[1].pass_rate);
959    }
960
961    // ─── Wilson score tests ───
962
963    #[test]
964    fn wilson_score_zero_total() {
965        assert_eq!(wilson_score_lower(0, 0, 1.96), 0.0);
966    }
967
968    #[test]
969    fn wilson_score_all_pass() {
970        let score = wilson_score_lower(10, 10, 1.96);
971        assert!(
972            score > 0.7,
973            "all-pass wilson lower should be > 0.7, got {score}"
974        );
975        assert!(score < 1.0);
976    }
977
978    #[test]
979    fn wilson_score_all_fail() {
980        let score = wilson_score_lower(0, 10, 1.96);
981        assert_eq!(score, 0.0);
982    }
983
984    #[test]
985    fn wilson_score_half() {
986        let score = wilson_score_lower(5, 10, 1.96);
987        assert!(
988            score > 0.2,
989            "50% pass rate wilson lower should be > 0.2, got {score}"
990        );
991        assert!(
992            score < 0.5,
993            "50% pass rate wilson lower should be < 0.5, got {score}"
994        );
995    }
996
997    #[test]
998    fn wilson_score_small_sample() {
999        // With only 2 samples, uncertainty is high — lower bound should be much less than raw rate
1000        let score = wilson_score_lower(1, 2, 1.96);
1001        assert!(
1002            score < 0.5,
1003            "small sample should pull wilson lower bound down, got {score}"
1004        );
1005        assert!(score > 0.0);
1006    }
1007
1008    // ─── Coefficient of variation tests ───
1009
1010    #[test]
1011    fn cv_single_duration() {
1012        assert_eq!(compute_cv(&[Duration::from_millis(100)]), 0.0);
1013    }
1014
1015    #[test]
1016    fn cv_identical_durations() {
1017        let d = vec![Duration::from_millis(100); 5];
1018        let cv = compute_cv(&d);
1019        assert!(
1020            cv.abs() < 1e-10,
1021            "identical durations should have cv ≈ 0, got {cv}"
1022        );
1023    }
1024
1025    #[test]
1026    fn cv_varied_durations() {
1027        let d = vec![
1028            Duration::from_millis(10),
1029            Duration::from_millis(20),
1030            Duration::from_millis(30),
1031            Duration::from_millis(40),
1032            Duration::from_millis(50),
1033        ];
1034        let cv = compute_cv(&d);
1035        assert!(cv > 0.4, "varied durations should have cv > 0.4, got {cv}");
1036        assert!(cv < 0.7, "varied durations should have cv < 0.7, got {cv}");
1037    }
1038
1039    #[test]
1040    fn cv_empty() {
1041        assert_eq!(compute_cv(&[]), 0.0);
1042    }
1043
1044    // ─── Timing stats tests ───
1045
1046    #[test]
1047    fn timing_stats_empty() {
1048        assert!(compute_timing_stats(&[]).is_none());
1049    }
1050
1051    #[test]
1052    fn timing_stats_single() {
1053        let stats = compute_timing_stats(&[Duration::from_millis(100)]).unwrap();
1054        assert!((stats.mean_ms - 100.0).abs() < 0.1);
1055        assert!((stats.median_ms - 100.0).abs() < 0.1);
1056        assert!(stats.std_dev_ms.abs() < 0.1);
1057        assert!(stats.cv.abs() < 0.01);
1058    }
1059
1060    #[test]
1061    fn timing_stats_even_count() {
1062        let d = vec![
1063            Duration::from_millis(10),
1064            Duration::from_millis(20),
1065            Duration::from_millis(30),
1066            Duration::from_millis(40),
1067        ];
1068        let stats = compute_timing_stats(&d).unwrap();
1069        assert!((stats.mean_ms - 25.0).abs() < 0.1);
1070        assert!((stats.median_ms - 25.0).abs() < 0.1); // (20+30)/2
1071        assert!(stats.p95_ms >= 30.0);
1072        assert!(stats.p99_ms >= 30.0);
1073    }
1074
1075    #[test]
1076    fn timing_stats_odd_count() {
1077        let d = vec![
1078            Duration::from_millis(10),
1079            Duration::from_millis(20),
1080            Duration::from_millis(30),
1081        ];
1082        let stats = compute_timing_stats(&d).unwrap();
1083        assert!((stats.median_ms - 20.0).abs() < 0.1);
1084    }
1085
1086    #[test]
1087    fn timing_stats_percentiles() {
1088        // 100 values from 1 to 100
1089        let d: Vec<Duration> = (1..=100).map(Duration::from_millis).collect();
1090        let stats = compute_timing_stats(&d).unwrap();
1091        assert!(
1092            stats.p95_ms >= 95.0,
1093            "p95 should be ≥ 95, got {}",
1094            stats.p95_ms
1095        );
1096        assert!(
1097            stats.p99_ms >= 99.0,
1098            "p99 should be ≥ 99, got {}",
1099            stats.p99_ms
1100        );
1101    }
1102
1103    // ─── Severity classification tests ───
1104
1105    #[test]
1106    fn severity_critical() {
1107        assert_eq!(FlakySeverity::from_pass_rate(0.0), FlakySeverity::Critical);
1108        assert_eq!(FlakySeverity::from_pass_rate(25.0), FlakySeverity::Critical);
1109        assert_eq!(FlakySeverity::from_pass_rate(49.9), FlakySeverity::Critical);
1110    }
1111
1112    #[test]
1113    fn severity_high() {
1114        assert_eq!(FlakySeverity::from_pass_rate(50.0), FlakySeverity::High);
1115        assert_eq!(FlakySeverity::from_pass_rate(70.0), FlakySeverity::High);
1116        assert_eq!(FlakySeverity::from_pass_rate(79.9), FlakySeverity::High);
1117    }
1118
1119    #[test]
1120    fn severity_medium() {
1121        assert_eq!(FlakySeverity::from_pass_rate(80.0), FlakySeverity::Medium);
1122        assert_eq!(FlakySeverity::from_pass_rate(90.0), FlakySeverity::Medium);
1123        assert_eq!(FlakySeverity::from_pass_rate(94.9), FlakySeverity::Medium);
1124    }
1125
1126    #[test]
1127    fn severity_low() {
1128        assert_eq!(FlakySeverity::from_pass_rate(95.0), FlakySeverity::Low);
1129        assert_eq!(FlakySeverity::from_pass_rate(100.0), FlakySeverity::Low);
1130    }
1131
1132    #[test]
1133    fn severity_labels_and_icons() {
1134        assert_eq!(FlakySeverity::Critical.label(), "CRITICAL");
1135        assert_eq!(FlakySeverity::High.label(), "HIGH");
1136        assert_eq!(FlakySeverity::Medium.label(), "MEDIUM");
1137        assert_eq!(FlakySeverity::Low.label(), "LOW");
1138        // Icons are emoji strings
1139        assert!(!FlakySeverity::Critical.icon().is_empty());
1140        assert!(!FlakySeverity::Low.icon().is_empty());
1141    }
1142
1143    // ─── Threshold tests ───
1144
1145    #[test]
1146    fn threshold_config_builder() {
1147        let cfg = StressConfig::new(10).with_threshold(0.8);
1148        assert_eq!(cfg.threshold, Some(0.8));
1149    }
1150
1151    #[test]
1152    fn threshold_clamps_to_range() {
1153        let cfg = StressConfig::new(10).with_threshold(1.5);
1154        assert_eq!(cfg.threshold, Some(1.0));
1155        let cfg = StressConfig::new(10).with_threshold(-0.5);
1156        assert_eq!(cfg.threshold, Some(0.0));
1157    }
1158
1159    #[test]
1160    fn threshold_passed_when_all_above() {
1161        let cfg = StressConfig::new(3).with_threshold(0.5);
1162        let mut acc = StressAccumulator::new(cfg);
1163
1164        // Iteration 1: all pass
1165        acc.record(make_passing_result(3), Duration::from_millis(100));
1166        // Iteration 2: one test fails (makes it flaky at 50%)
1167        let mut r2 = make_passing_result(3);
1168        r2.suites[0].tests[0].status = TestStatus::Failed;
1169        r2.suites[0].tests[0].error = Some(TestError {
1170            message: "flaky".to_string(),
1171            location: None,
1172        });
1173        r2.raw_exit_code = 1;
1174        acc.record(r2, Duration::from_millis(100));
1175        // Iteration 3: all pass again (test_0 is 66% pass rate, above 50% threshold)
1176        acc.record(make_passing_result(3), Duration::from_millis(100));
1177
1178        let report = acc.report();
1179        // test_0 has pass_rate = 66.7%, threshold = 50% → should pass
1180        assert_eq!(report.threshold_passed, Some(true));
1181    }
1182
1183    #[test]
1184    fn threshold_fails_when_below() {
1185        let cfg = StressConfig::new(4).with_threshold(0.9);
1186        let mut acc = StressAccumulator::new(cfg);
1187
1188        // Make test_0 flaky: pass 2/4 times = 50% pass rate, below 90% threshold
1189        acc.record(make_passing_result(3), Duration::from_millis(100));
1190
1191        let mut r2 = make_passing_result(3);
1192        r2.suites[0].tests[0].status = TestStatus::Failed;
1193        r2.suites[0].tests[0].error = Some(TestError {
1194            message: "f".to_string(),
1195            location: None,
1196        });
1197        r2.raw_exit_code = 1;
1198        acc.record(r2, Duration::from_millis(100));
1199
1200        let mut r3 = make_passing_result(3);
1201        r3.suites[0].tests[0].status = TestStatus::Failed;
1202        r3.suites[0].tests[0].error = Some(TestError {
1203            message: "f".to_string(),
1204            location: None,
1205        });
1206        r3.raw_exit_code = 1;
1207        acc.record(r3, Duration::from_millis(100));
1208
1209        acc.record(make_passing_result(3), Duration::from_millis(100));
1210
1211        let report = acc.report();
1212        // test_0 pass rate = 50%, threshold = 90% → fails
1213        assert_eq!(report.threshold_passed, Some(false));
1214    }
1215
1216    #[test]
1217    fn no_threshold_returns_none() {
1218        let cfg = StressConfig::new(2);
1219        let mut acc = StressAccumulator::new(cfg);
1220        acc.record(make_passing_result(3), Duration::from_millis(100));
1221        acc.record(make_passing_result(3), Duration::from_millis(100));
1222        let report = acc.report();
1223        assert!(report.threshold_passed.is_none());
1224        assert!(report.threshold.is_none());
1225    }
1226
1227    // ─── Timing stats in report ───
1228
1229    #[test]
1230    fn report_contains_timing_stats() {
1231        let cfg = StressConfig::new(3);
1232        let mut acc = StressAccumulator::new(cfg);
1233
1234        acc.record(make_passing_result(3), Duration::from_millis(100));
1235        acc.record(make_passing_result(3), Duration::from_millis(200));
1236        acc.record(make_passing_result(3), Duration::from_millis(300));
1237
1238        let report = acc.report();
1239        assert!(report.timing_stats.is_some());
1240        let stats = report.timing_stats.unwrap();
1241        assert!((stats.mean_ms - 200.0).abs() < 1.0);
1242        assert_eq!(report.iteration_durations.len(), 3);
1243    }
1244
1245    // ─── Stress report JSON ───
1246
1247    #[test]
1248    fn stress_json_basic() {
1249        let report = StressReport {
1250            iterations_completed: 5,
1251            iterations_requested: 5,
1252            total_duration: Duration::from_secs(2),
1253            failures: vec![],
1254            flaky_tests: vec![],
1255            all_passed: true,
1256            stopped_early: false,
1257            threshold_passed: None,
1258            threshold: None,
1259            iteration_durations: vec![Duration::from_millis(400); 5],
1260            timing_stats: None,
1261        };
1262
1263        let json = stress_report_json(&report);
1264        assert_eq!(json["iterations_completed"], 5);
1265        assert_eq!(json["all_passed"], true);
1266        assert!(json.get("threshold").is_none());
1267    }
1268
1269    #[test]
1270    fn stress_json_with_threshold() {
1271        let report = StressReport {
1272            iterations_completed: 3,
1273            iterations_requested: 3,
1274            total_duration: Duration::from_secs(1),
1275            failures: vec![],
1276            flaky_tests: vec![],
1277            all_passed: true,
1278            stopped_early: false,
1279            threshold_passed: Some(true),
1280            threshold: Some(0.8),
1281            iteration_durations: vec![],
1282            timing_stats: None,
1283        };
1284
1285        let json = stress_report_json(&report);
1286        assert_eq!(json["threshold"], 0.8);
1287        assert_eq!(json["threshold_passed"], true);
1288    }
1289
1290    #[test]
1291    fn stress_json_with_flaky_tests() {
1292        let report = StressReport {
1293            iterations_completed: 3,
1294            iterations_requested: 3,
1295            total_duration: Duration::from_secs(1),
1296            failures: vec![IterationFailure {
1297                iteration: 2,
1298                failed_tests: vec!["test_a".to_string()],
1299            }],
1300            flaky_tests: vec![FlakyTestReport {
1301                name: "test_a".to_string(),
1302                suite: "suite".to_string(),
1303                pass_count: 2,
1304                fail_count: 1,
1305                total_runs: 3,
1306                pass_rate: 66.7,
1307                durations: vec![Duration::from_millis(10); 3],
1308                avg_duration: Duration::from_millis(10),
1309                max_duration: Duration::from_millis(12),
1310                min_duration: Duration::from_millis(8),
1311                severity: FlakySeverity::High,
1312                wilson_lower: 0.3,
1313                timing_cv: 0.1,
1314            }],
1315            all_passed: false,
1316            stopped_early: false,
1317            threshold_passed: None,
1318            threshold: None,
1319            iteration_durations: vec![],
1320            timing_stats: None,
1321        };
1322
1323        let json = stress_report_json(&report);
1324        assert_eq!(json["flaky_tests"][0]["name"], "test_a");
1325        assert_eq!(json["flaky_tests"][0]["severity"], "HIGH");
1326        assert_eq!(json["failures"][0]["iteration"], 2);
1327    }
1328
1329    #[test]
1330    fn stress_json_with_timing_stats() {
1331        let report = StressReport {
1332            iterations_completed: 3,
1333            iterations_requested: 3,
1334            total_duration: Duration::from_secs(1),
1335            failures: vec![],
1336            flaky_tests: vec![],
1337            all_passed: true,
1338            stopped_early: false,
1339            threshold_passed: None,
1340            threshold: None,
1341            iteration_durations: vec![],
1342            timing_stats: Some(TimingStats {
1343                mean_ms: 100.0,
1344                median_ms: 95.0,
1345                std_dev_ms: 10.0,
1346                cv: 0.1,
1347                p95_ms: 120.0,
1348                p99_ms: 130.0,
1349            }),
1350        };
1351
1352        let json = stress_report_json(&report);
1353        assert_eq!(json["timing_stats"]["mean_ms"], 100.0);
1354        assert_eq!(json["timing_stats"]["cv"], 0.1);
1355        assert_eq!(json["timing_stats"]["p95_ms"], 120.0);
1356    }
1357
1358    // ─── Flaky severity in report ───
1359
1360    #[test]
1361    fn flaky_tests_have_severity_and_wilson() {
1362        let cfg = StressConfig::new(4);
1363        let mut acc = StressAccumulator::new(cfg);
1364
1365        // test_0: pass 1/4 = 25% → Critical
1366        for i in 0..4 {
1367            let mut r = make_passing_result(1);
1368            if i > 0 {
1369                r.suites[0].tests[0].status = TestStatus::Failed;
1370                r.suites[0].tests[0].error = Some(TestError {
1371                    message: "f".to_string(),
1372                    location: None,
1373                });
1374                r.raw_exit_code = 1;
1375            }
1376            acc.record(r, Duration::from_millis(100));
1377        }
1378
1379        let report = acc.report();
1380        assert_eq!(report.flaky_tests.len(), 1);
1381        let flaky = &report.flaky_tests[0];
1382        assert_eq!(flaky.severity, FlakySeverity::Critical);
1383        assert!(flaky.wilson_lower >= 0.0);
1384        assert!(flaky.wilson_lower < 0.5);
1385    }
1386
1387    // ─── Format report with timing stats ───
1388
1389    #[test]
1390    fn format_report_shows_timing_stats() {
1391        let report = StressReport {
1392            iterations_completed: 10,
1393            iterations_requested: 10,
1394            total_duration: Duration::from_secs(5),
1395            failures: vec![],
1396            flaky_tests: vec![],
1397            all_passed: true,
1398            stopped_early: false,
1399            threshold_passed: None,
1400            threshold: None,
1401            iteration_durations: vec![],
1402            timing_stats: Some(TimingStats {
1403                mean_ms: 500.0,
1404                median_ms: 480.0,
1405                std_dev_ms: 50.0,
1406                cv: 0.1,
1407                p95_ms: 600.0,
1408                p99_ms: 650.0,
1409            }),
1410        };
1411
1412        let output = format_stress_report(&report);
1413        assert!(output.contains("Timing Statistics"));
1414        assert!(output.contains("Mean: 500.0ms"));
1415        assert!(output.contains("P95: 600.0ms"));
1416    }
1417
1418    #[test]
1419    fn format_report_shows_threshold_pass() {
1420        let report = StressReport {
1421            iterations_completed: 5,
1422            iterations_requested: 5,
1423            total_duration: Duration::from_secs(2),
1424            failures: vec![],
1425            flaky_tests: vec![],
1426            all_passed: true,
1427            stopped_early: false,
1428            threshold_passed: Some(true),
1429            threshold: Some(0.9),
1430            iteration_durations: vec![],
1431            timing_stats: None,
1432        };
1433
1434        let output = format_stress_report(&report);
1435        assert!(output.contains("Threshold check passed"));
1436    }
1437
1438    #[test]
1439    fn format_report_shows_threshold_fail() {
1440        let report = StressReport {
1441            iterations_completed: 5,
1442            iterations_requested: 5,
1443            total_duration: Duration::from_secs(2),
1444            failures: vec![],
1445            flaky_tests: vec![],
1446            all_passed: true,
1447            stopped_early: false,
1448            threshold_passed: Some(false),
1449            threshold: Some(0.95),
1450            iteration_durations: vec![],
1451            timing_stats: None,
1452        };
1453
1454        let output = format_stress_report(&report);
1455        assert!(output.contains("Threshold check FAILED"));
1456    }
1457
1458    #[test]
1459    fn format_report_high_cv_warning() {
1460        let report = StressReport {
1461            iterations_completed: 10,
1462            iterations_requested: 10,
1463            total_duration: Duration::from_secs(5),
1464            failures: vec![],
1465            flaky_tests: vec![],
1466            all_passed: true,
1467            stopped_early: false,
1468            threshold_passed: None,
1469            threshold: None,
1470            iteration_durations: vec![],
1471            timing_stats: Some(TimingStats {
1472                mean_ms: 500.0,
1473                median_ms: 480.0,
1474                std_dev_ms: 200.0,
1475                cv: 0.4, // > 0.3 threshold
1476                p95_ms: 900.0,
1477                p99_ms: 950.0,
1478            }),
1479        };
1480
1481        let output = format_stress_report(&report);
1482        assert!(output.contains("High timing variance"));
1483    }
1484
1485    // ─── Parallel workers config ───
1486
1487    #[test]
1488    fn parallel_workers_config() {
1489        let cfg = StressConfig::new(10).with_parallel_workers(4);
1490        assert_eq!(cfg.parallel_workers, 4);
1491    }
1492
1493    // ─── Memory growth safety ───
1494
1495    #[test]
1496    fn accumulator_large_iteration_count_no_crash() {
1497        // Simulate 500 iterations with small test suites to verify
1498        // the accumulator doesn't crash or cause excessive memory issues
1499        let cfg = StressConfig::new(500);
1500        let mut acc = StressAccumulator::new(cfg);
1501
1502        for _ in 0..500 {
1503            let result = make_passing_result(5);
1504            acc.record(result, Duration::from_millis(10));
1505        }
1506
1507        assert_eq!(acc.completed(), 500);
1508        let report = acc.report();
1509        assert_eq!(report.iterations_completed, 500);
1510        assert!(report.all_passed);
1511        assert_eq!(report.iteration_durations.len(), 500);
1512    }
1513
1514    #[test]
1515    fn accumulator_many_tests_per_iteration_no_crash() {
1516        // Simulate iterations with 200 tests each to check memory with large test suites
1517        let cfg = StressConfig::new(10);
1518        let mut acc = StressAccumulator::new(cfg);
1519
1520        for _ in 0..10 {
1521            let result = make_passing_result(200);
1522            acc.record(result, Duration::from_millis(50));
1523        }
1524
1525        let report = acc.report();
1526        assert_eq!(report.iterations_completed, 10);
1527        assert!(report.all_passed);
1528    }
1529
1530    #[test]
1531    fn accumulator_large_flaky_report_no_crash() {
1532        // Many flaky tests across many iterations
1533        let cfg = StressConfig::new(50);
1534        let mut acc = StressAccumulator::new(cfg);
1535
1536        for i in 0..50 {
1537            let mut result = make_passing_result(20);
1538            // Make every other test fail on even iterations → 10 flaky tests
1539            if i % 2 == 0 {
1540                for j in (0..20).step_by(2) {
1541                    result.suites[0].tests[j].status = TestStatus::Failed;
1542                    result.suites[0].tests[j].error = Some(TestError {
1543                        message: "flaky".to_string(),
1544                        location: None,
1545                    });
1546                }
1547                result.raw_exit_code = 1;
1548            }
1549            acc.record(result, Duration::from_millis(10));
1550        }
1551
1552        let report = acc.report();
1553        assert_eq!(report.iterations_completed, 50);
1554        // Should have detected multiple flaky tests
1555        assert!(
1556            !report.flaky_tests.is_empty(),
1557            "should detect flaky tests across 50 iterations"
1558        );
1559        // Each flaky test should have exactly 50 duration entries
1560        for flaky in &report.flaky_tests {
1561            assert_eq!(
1562                flaky.total_runs, 50,
1563                "each test should have been seen in all 50 iterations"
1564            );
1565            assert_eq!(
1566                flaky.durations.len(),
1567                50,
1568                "each flaky test should have 50 duration samples"
1569            );
1570        }
1571    }
1572
1573    #[test]
1574    fn accumulator_report_consumes_self() {
1575        // Verify report() takes ownership (self, not &self), ensuring the
1576        // large Vec<IterationResult> is freed after report generation
1577        let cfg = StressConfig::new(3);
1578        let mut acc = StressAccumulator::new(cfg);
1579        acc.record(make_passing_result(5), Duration::from_millis(10));
1580        acc.record(make_passing_result(5), Duration::from_millis(10));
1581        acc.record(make_passing_result(5), Duration::from_millis(10));
1582
1583        let _report = acc.report();
1584        // acc is moved — cannot be used again (compile-time guarantee)
1585        // The iterations Vec is dropped when report() extracts what it needs
1586    }
1587
1588    #[test]
1589    fn max_duration_stops_accumulation() {
1590        let cfg = StressConfig::new(1000).with_max_duration(Duration::from_millis(1));
1591        let mut acc = StressAccumulator::new(cfg);
1592
1593        // First iteration always succeeds
1594        acc.record(make_passing_result(5), Duration::from_millis(10));
1595        // Sleep to exceed max_duration
1596        std::thread::sleep(Duration::from_millis(5));
1597        // This should return false (time exceeded)
1598        let should_continue = acc.record(make_passing_result(5), Duration::from_millis(10));
1599        assert!(
1600            !should_continue || acc.is_time_exceeded(),
1601            "should stop when max_duration exceeded"
1602        );
1603    }
1604}