1use std::time::{Duration, Instant};
2
3use crate::adapters::{TestRunResult, TestStatus};
4
5#[derive(Debug, Clone)]
7pub struct StressConfig {
8 pub iterations: usize,
10 pub fail_fast: bool,
12 pub max_duration: Option<Duration>,
14 pub threshold: Option<f64>,
16 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#[derive(Debug, Clone)]
60pub struct IterationResult {
61 pub iteration: usize,
62 pub result: TestRunResult,
63 pub duration: Duration,
64}
65
66#[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 pub threshold_passed: Option<bool>,
78 pub threshold: Option<f64>,
80 pub iteration_durations: Vec<Duration>,
82 pub timing_stats: Option<TimingStats>,
84}
85
86#[derive(Debug, Clone)]
88pub struct TimingStats {
89 pub mean_ms: f64,
90 pub median_ms: f64,
91 pub std_dev_ms: f64,
92 pub cv: f64,
94 pub p95_ms: f64,
95 pub p99_ms: f64,
96}
97
98#[derive(Debug, Clone, PartialEq)]
100pub enum FlakySeverity {
101 Critical,
103 High,
105 Medium,
107 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#[derive(Debug, Clone)]
142pub struct IterationFailure {
143 pub iteration: usize,
144 pub failed_tests: Vec<String>,
145}
146
147#[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 pub severity: FlakySeverity,
162 pub wilson_lower: f64,
165 pub timing_cv: f64,
167}
168
169pub 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 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 pub fn completed(&self) -> usize {
211 self.iterations.len()
212 }
213
214 pub fn requested(&self) -> usize {
216 self.config.iterations
217 }
218
219 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 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 let iteration_durations: Vec<Duration> =
234 self.iterations.iter().map(|it| it.duration).collect();
235
236 let timing_stats = compute_timing_stats(&iteration_durations);
238
239 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 let flaky_tests = analyze_flaky_tests(&self.iterations);
266
267 let all_passed = failures.is_empty();
268
269 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
291fn analyze_flaky_tests(iterations: &[IterationResult]) -> Vec<FlakyTestReport> {
293 use std::collections::HashMap;
294
295 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 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 let wilson_lower = wilson_score_lower(pass_count, total_runs, 1.96);
333
334 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 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
370fn 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
386fn 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
402fn 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
447pub 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 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 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
544pub 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 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 acc.record(make_passing_result(3), Duration::from_millis(100));
739
740 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 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 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 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 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 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 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 #[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 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 #[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 #[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); 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 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 #[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 assert!(!FlakySeverity::Critical.icon().is_empty());
1140 assert!(!FlakySeverity::Low.icon().is_empty());
1141 }
1142
1143 #[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 acc.record(make_passing_result(3), Duration::from_millis(100));
1166 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 acc.record(make_passing_result(3), Duration::from_millis(100));
1177
1178 let report = acc.report();
1179 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 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 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 #[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 #[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 #[test]
1361 fn flaky_tests_have_severity_and_wilson() {
1362 let cfg = StressConfig::new(4);
1363 let mut acc = StressAccumulator::new(cfg);
1364
1365 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 #[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, 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 #[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 #[test]
1496 fn accumulator_large_iteration_count_no_crash() {
1497 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 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 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 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 assert!(
1556 !report.flaky_tests.is_empty(),
1557 "should detect flaky tests across 50 iterations"
1558 );
1559 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 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 }
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 acc.record(make_passing_result(5), Duration::from_millis(10));
1595 std::thread::sleep(Duration::from_millis(5));
1597 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}