1use std::time::Duration;
2
3use crate::adapters::{TestCase, TestRunResult, TestStatus, TestSuite};
4
5#[derive(Debug, Clone, Default)]
7pub enum BackoffStrategy {
8 #[default]
10 None,
11 Fixed(Duration),
13 Linear(Duration),
15 Exponential { base: Duration, max: Duration },
17}
18
19impl BackoffStrategy {
20 pub fn delay_for(&self, attempt: u32) -> Duration {
22 match self {
23 BackoffStrategy::None => Duration::ZERO,
24 BackoffStrategy::Fixed(d) => *d,
25 BackoffStrategy::Linear(base) => *base * attempt,
26 BackoffStrategy::Exponential { base, max } => {
27 let multiplier = 2u64.saturating_pow(attempt);
28 let delay = base.saturating_mul(multiplier as u32);
29 if delay > *max { *max } else { delay }
30 }
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct RetryConfig {
38 pub max_retries: u32,
40 pub backoff: BackoffStrategy,
42 pub stop_on_pass: bool,
44 pub retry_failed_only: bool,
46}
47
48impl RetryConfig {
49 pub fn new(max_retries: u32) -> Self {
51 Self {
52 max_retries,
53 backoff: BackoffStrategy::None,
54 stop_on_pass: true,
55 retry_failed_only: true,
56 }
57 }
58
59 pub fn with_backoff(mut self, backoff: BackoffStrategy) -> Self {
61 self.backoff = backoff;
62 self
63 }
64
65 pub fn with_stop_on_pass(mut self, stop: bool) -> Self {
67 self.stop_on_pass = stop;
68 self
69 }
70
71 pub fn with_retry_failed_only(mut self, failed_only: bool) -> Self {
73 self.retry_failed_only = failed_only;
74 self
75 }
76
77 pub fn is_enabled(&self) -> bool {
79 self.max_retries > 0
80 }
81}
82
83impl Default for RetryConfig {
84 fn default() -> Self {
85 Self::new(0)
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct RetryAttempt {
92 pub attempt: u32,
94 pub result: TestRunResult,
96 pub duration: Duration,
98}
99
100#[derive(Debug, Clone)]
102pub struct RetryResult {
103 pub original: TestRunResult,
105 pub attempts: Vec<RetryAttempt>,
107 pub final_result: TestRunResult,
109 pub total_attempts: u32,
111}
112
113impl RetryResult {
114 pub fn tests_fixed(&self) -> usize {
116 let original_failed = self.original.total_failed();
117 let final_failed = self.final_result.total_failed();
118 original_failed.saturating_sub(final_failed)
119 }
120
121 pub fn all_passed(&self) -> bool {
123 self.final_result.total_failed() == 0
124 }
125
126 pub fn had_effect(&self) -> bool {
128 self.original.total_failed() != self.final_result.total_failed()
129 }
130}
131
132pub fn extract_failed_tests(result: &TestRunResult) -> Vec<FailedTestInfo> {
134 let mut failed = Vec::new();
135 for suite in &result.suites {
136 for test in &suite.tests {
137 if test.status == TestStatus::Failed {
138 failed.push(FailedTestInfo {
139 suite_name: suite.name.clone(),
140 test_name: test.name.clone(),
141 error_message: test.error.as_ref().map(|e| e.message.clone()),
142 });
143 }
144 }
145 }
146 failed
147}
148
149#[derive(Debug, Clone)]
151pub struct FailedTestInfo {
152 pub suite_name: String,
154 pub test_name: String,
156 pub error_message: Option<String>,
158}
159
160impl FailedTestInfo {
161 pub fn full_name(&self) -> String {
163 format!("{}::{}", self.suite_name, self.test_name)
164 }
165}
166
167pub fn merge_retry_result(original: &TestRunResult, retry: &TestRunResult) -> TestRunResult {
170 let mut suites = Vec::new();
171
172 for orig_suite in &original.suites {
173 let retry_suite = retry.suites.iter().find(|s| s.name == orig_suite.name);
175
176 let tests: Vec<TestCase> = orig_suite
177 .tests
178 .iter()
179 .map(|orig_test| {
180 if orig_test.status != TestStatus::Failed {
181 return orig_test.clone();
182 }
183
184 if let Some(rs) = retry_suite
186 && let Some(retry_test) = rs.tests.iter().find(|t| t.name == orig_test.name)
187 && retry_test.status == TestStatus::Passed
188 {
189 return retry_test.clone();
191 }
192
193 orig_test.clone()
194 })
195 .collect();
196
197 suites.push(TestSuite {
198 name: orig_suite.name.clone(),
199 tests,
200 });
201 }
202
203 let exit_code = if suites.iter().all(|s| s.failed() == 0) {
204 0
205 } else {
206 original.raw_exit_code
207 };
208
209 TestRunResult {
210 suites,
211 duration: original.duration + retry.duration,
212 raw_exit_code: exit_code,
213 }
214}
215
216pub fn merge_all_retries(original: &TestRunResult, attempts: &[RetryAttempt]) -> TestRunResult {
218 let mut merged = original.clone();
219 for attempt in attempts {
220 merged = merge_retry_result(&merged, &attempt.result);
221 }
222 merged
223}
224
225pub fn build_retry_result(original: TestRunResult, attempts: Vec<RetryAttempt>) -> RetryResult {
227 let total_attempts = 1 + attempts.len() as u32;
228 let final_result = merge_all_retries(&original, &attempts);
229
230 RetryResult {
231 original,
232 attempts,
233 final_result,
234 total_attempts,
235 }
236}
237
238pub fn tests_still_failing(
240 current: &TestRunResult,
241 failed_names: &[FailedTestInfo],
242) -> Vec<FailedTestInfo> {
243 let mut still_failing = Vec::new();
244
245 for info in failed_names {
246 let still_failed = current.suites.iter().any(|suite| {
247 suite.name == info.suite_name
248 && suite
249 .tests
250 .iter()
251 .any(|t| t.name == info.test_name && t.status == TestStatus::Failed)
252 });
253
254 if still_failed {
255 still_failing.push(info.clone());
256 }
257 }
258
259 still_failing
260}
261
262pub fn failed_tests_as_filter(failed: &[FailedTestInfo]) -> String {
264 failed
265 .iter()
266 .map(|f| f.test_name.as_str())
267 .collect::<Vec<_>>()
268 .join(",")
269}
270
271#[derive(Debug, Clone)]
273pub struct RetryStats {
274 pub total_retries: u32,
276 pub tests_retried: usize,
278 pub tests_fixed: usize,
280 pub tests_still_failing: usize,
282 pub total_retry_time: Duration,
284}
285
286pub fn compute_retry_stats(result: &RetryResult) -> RetryStats {
288 let original_failed = result.original.total_failed();
289 let final_failed = result.final_result.total_failed();
290 let total_retry_time: Duration = result.attempts.iter().map(|a| a.duration).sum();
291
292 RetryStats {
293 total_retries: result.attempts.len() as u32,
294 tests_retried: original_failed,
295 tests_fixed: original_failed.saturating_sub(final_failed),
296 tests_still_failing: final_failed,
297 total_retry_time,
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 fn make_test(name: &str, status: TestStatus) -> TestCase {
306 TestCase {
307 name: name.into(),
308 status,
309 duration: Duration::from_millis(10),
310 error: None,
311 }
312 }
313
314 fn make_suite(name: &str, tests: Vec<TestCase>) -> TestSuite {
315 TestSuite {
316 name: name.into(),
317 tests,
318 }
319 }
320
321 fn make_result(suites: Vec<TestSuite>) -> TestRunResult {
322 TestRunResult {
323 suites,
324 duration: Duration::from_millis(100),
325 raw_exit_code: 1,
326 }
327 }
328
329 #[test]
332 fn backoff_none() {
333 let b = BackoffStrategy::None;
334 assert_eq!(b.delay_for(0), Duration::ZERO);
335 assert_eq!(b.delay_for(5), Duration::ZERO);
336 }
337
338 #[test]
339 fn backoff_fixed() {
340 let b = BackoffStrategy::Fixed(Duration::from_millis(500));
341 assert_eq!(b.delay_for(0), Duration::from_millis(500));
342 assert_eq!(b.delay_for(3), Duration::from_millis(500));
343 }
344
345 #[test]
346 fn backoff_linear() {
347 let b = BackoffStrategy::Linear(Duration::from_millis(100));
348 assert_eq!(b.delay_for(0), Duration::ZERO);
349 assert_eq!(b.delay_for(1), Duration::from_millis(100));
350 assert_eq!(b.delay_for(3), Duration::from_millis(300));
351 }
352
353 #[test]
354 fn backoff_exponential() {
355 let b = BackoffStrategy::Exponential {
356 base: Duration::from_millis(100),
357 max: Duration::from_secs(5),
358 };
359 assert_eq!(b.delay_for(0), Duration::from_millis(100)); assert_eq!(b.delay_for(1), Duration::from_millis(200)); assert_eq!(b.delay_for(2), Duration::from_millis(400)); assert_eq!(b.delay_for(3), Duration::from_millis(800)); }
364
365 #[test]
366 fn backoff_exponential_cap() {
367 let b = BackoffStrategy::Exponential {
368 base: Duration::from_secs(1),
369 max: Duration::from_secs(10),
370 };
371 assert_eq!(b.delay_for(10), Duration::from_secs(10)); }
373
374 #[test]
377 fn retry_config_default() {
378 let config = RetryConfig::default();
379 assert_eq!(config.max_retries, 0);
380 assert!(!config.is_enabled());
381 }
382
383 #[test]
384 fn retry_config_enabled() {
385 let config = RetryConfig::new(3);
386 assert!(config.is_enabled());
387 assert_eq!(config.max_retries, 3);
388 assert!(config.stop_on_pass);
389 assert!(config.retry_failed_only);
390 }
391
392 #[test]
393 fn retry_config_builder() {
394 let config = RetryConfig::new(2)
395 .with_backoff(BackoffStrategy::Fixed(Duration::from_secs(1)))
396 .with_stop_on_pass(false)
397 .with_retry_failed_only(false);
398
399 assert_eq!(config.max_retries, 2);
400 assert!(!config.stop_on_pass);
401 assert!(!config.retry_failed_only);
402 }
403
404 #[test]
407 fn extract_failed_test_info() {
408 let result = make_result(vec![make_suite(
409 "unit",
410 vec![
411 make_test("test_add", TestStatus::Passed),
412 make_test("test_div", TestStatus::Failed),
413 make_test("test_mul", TestStatus::Passed),
414 ],
415 )]);
416
417 let failed = extract_failed_tests(&result);
418 assert_eq!(failed.len(), 1);
419 assert_eq!(failed[0].test_name, "test_div");
420 assert_eq!(failed[0].suite_name, "unit");
421 assert_eq!(failed[0].full_name(), "unit::test_div");
422 }
423
424 #[test]
425 fn extract_failed_multiple_suites() {
426 let result = make_result(vec![
427 make_suite(
428 "math",
429 vec![
430 make_test("test_add", TestStatus::Failed),
431 make_test("test_sub", TestStatus::Passed),
432 ],
433 ),
434 make_suite(
435 "strings",
436 vec![make_test("test_concat", TestStatus::Failed)],
437 ),
438 ]);
439
440 let failed = extract_failed_tests(&result);
441 assert_eq!(failed.len(), 2);
442 assert_eq!(failed[0].test_name, "test_add");
443 assert_eq!(failed[1].test_name, "test_concat");
444 }
445
446 #[test]
447 fn extract_failed_none() {
448 let result = make_result(vec![make_suite(
449 "unit",
450 vec![make_test("test", TestStatus::Passed)],
451 )]);
452
453 let failed = extract_failed_tests(&result);
454 assert!(failed.is_empty());
455 }
456
457 #[test]
460 fn merge_retry_fixes_test() {
461 let original = make_result(vec![make_suite(
462 "unit",
463 vec![
464 make_test("test_a", TestStatus::Passed),
465 make_test("test_b", TestStatus::Failed),
466 ],
467 )]);
468
469 let retry = make_result(vec![make_suite(
470 "unit",
471 vec![make_test("test_b", TestStatus::Passed)],
472 )]);
473
474 let merged = merge_retry_result(&original, &retry);
475 assert_eq!(merged.total_passed(), 2);
476 assert_eq!(merged.total_failed(), 0);
477 assert_eq!(merged.raw_exit_code, 0); }
479
480 #[test]
481 fn merge_retry_still_fails() {
482 let original = make_result(vec![make_suite(
483 "unit",
484 vec![make_test("test_b", TestStatus::Failed)],
485 )]);
486
487 let retry = make_result(vec![make_suite(
488 "unit",
489 vec![make_test("test_b", TestStatus::Failed)],
490 )]);
491
492 let merged = merge_retry_result(&original, &retry);
493 assert_eq!(merged.total_failed(), 1);
494 assert_eq!(merged.raw_exit_code, 1);
495 }
496
497 #[test]
498 fn merge_retry_partial_fix() {
499 let original = make_result(vec![make_suite(
500 "unit",
501 vec![
502 make_test("test_a", TestStatus::Failed),
503 make_test("test_b", TestStatus::Failed),
504 ],
505 )]);
506
507 let retry = make_result(vec![make_suite(
508 "unit",
509 vec![
510 make_test("test_a", TestStatus::Passed),
511 make_test("test_b", TestStatus::Failed),
512 ],
513 )]);
514
515 let merged = merge_retry_result(&original, &retry);
516 assert_eq!(merged.total_passed(), 1);
517 assert_eq!(merged.total_failed(), 1);
518 }
519
520 #[test]
521 fn merge_no_matching_suite() {
522 let original = make_result(vec![make_suite(
523 "unit",
524 vec![make_test("test_a", TestStatus::Failed)],
525 )]);
526
527 let retry = make_result(vec![make_suite(
528 "other",
529 vec![make_test("test_a", TestStatus::Passed)],
530 )]);
531
532 let merged = merge_retry_result(&original, &retry);
533 assert_eq!(merged.total_failed(), 1);
535 }
536
537 #[test]
540 fn merge_all_progressive() {
541 let original = make_result(vec![make_suite(
542 "unit",
543 vec![
544 make_test("test_a", TestStatus::Failed),
545 make_test("test_b", TestStatus::Failed),
546 make_test("test_c", TestStatus::Failed),
547 ],
548 )]);
549
550 let attempt1 = RetryAttempt {
551 attempt: 1,
552 result: make_result(vec![make_suite(
553 "unit",
554 vec![make_test("test_a", TestStatus::Passed)],
555 )]),
556 duration: Duration::from_millis(50),
557 };
558
559 let attempt2 = RetryAttempt {
560 attempt: 2,
561 result: make_result(vec![make_suite(
562 "unit",
563 vec![make_test("test_b", TestStatus::Passed)],
564 )]),
565 duration: Duration::from_millis(50),
566 };
567
568 let merged = merge_all_retries(&original, &[attempt1, attempt2]);
569 assert_eq!(merged.total_passed(), 2);
570 assert_eq!(merged.total_failed(), 1); }
572
573 #[test]
576 fn retry_result_stats() {
577 let original = make_result(vec![make_suite(
578 "unit",
579 vec![
580 make_test("test_a", TestStatus::Failed),
581 make_test("test_b", TestStatus::Failed),
582 ],
583 )]);
584
585 let attempt = RetryAttempt {
586 attempt: 1,
587 result: make_result(vec![make_suite(
588 "unit",
589 vec![make_test("test_a", TestStatus::Passed)],
590 )]),
591 duration: Duration::from_millis(50),
592 };
593
594 let retry_result = build_retry_result(original, vec![attempt]);
595
596 assert_eq!(retry_result.total_attempts, 2);
597 assert_eq!(retry_result.tests_fixed(), 1);
598 assert!(!retry_result.all_passed());
599 assert!(retry_result.had_effect());
600 }
601
602 #[test]
603 fn retry_result_all_fixed() {
604 let original = make_result(vec![make_suite(
605 "unit",
606 vec![make_test("test_a", TestStatus::Failed)],
607 )]);
608
609 let attempt = RetryAttempt {
610 attempt: 1,
611 result: make_result(vec![make_suite(
612 "unit",
613 vec![make_test("test_a", TestStatus::Passed)],
614 )]),
615 duration: Duration::from_millis(50),
616 };
617
618 let retry_result = build_retry_result(original, vec![attempt]);
619 assert!(retry_result.all_passed());
620 assert!(retry_result.had_effect());
621 }
622
623 #[test]
624 fn retry_result_no_effect() {
625 let original = make_result(vec![make_suite(
626 "unit",
627 vec![make_test("test_a", TestStatus::Failed)],
628 )]);
629
630 let attempt = RetryAttempt {
631 attempt: 1,
632 result: make_result(vec![make_suite(
633 "unit",
634 vec![make_test("test_a", TestStatus::Failed)],
635 )]),
636 duration: Duration::from_millis(50),
637 };
638
639 let retry_result = build_retry_result(original, vec![attempt]);
640 assert!(!retry_result.had_effect());
641 }
642
643 #[test]
646 fn tests_still_failing_some() {
647 let current = make_result(vec![make_suite(
648 "unit",
649 vec![
650 make_test("test_a", TestStatus::Passed),
651 make_test("test_b", TestStatus::Failed),
652 ],
653 )]);
654
655 let failed = vec![
656 FailedTestInfo {
657 suite_name: "unit".into(),
658 test_name: "test_a".into(),
659 error_message: None,
660 },
661 FailedTestInfo {
662 suite_name: "unit".into(),
663 test_name: "test_b".into(),
664 error_message: None,
665 },
666 ];
667
668 let still = tests_still_failing(¤t, &failed);
669 assert_eq!(still.len(), 1);
670 assert_eq!(still[0].test_name, "test_b");
671 }
672
673 #[test]
674 fn tests_still_failing_none() {
675 let current = make_result(vec![make_suite(
676 "unit",
677 vec![make_test("test_a", TestStatus::Passed)],
678 )]);
679
680 let failed = vec![FailedTestInfo {
681 suite_name: "unit".into(),
682 test_name: "test_a".into(),
683 error_message: None,
684 }];
685
686 let still = tests_still_failing(¤t, &failed);
687 assert!(still.is_empty());
688 }
689
690 #[test]
693 fn failed_as_filter_string() {
694 let failed = vec![
695 FailedTestInfo {
696 suite_name: "unit".into(),
697 test_name: "test_a".into(),
698 error_message: None,
699 },
700 FailedTestInfo {
701 suite_name: "unit".into(),
702 test_name: "test_b".into(),
703 error_message: None,
704 },
705 ];
706
707 let filter = failed_tests_as_filter(&failed);
708 assert_eq!(filter, "test_a,test_b");
709 }
710
711 #[test]
712 fn failed_as_filter_empty() {
713 let filter = failed_tests_as_filter(&[]);
714 assert_eq!(filter, "");
715 }
716
717 #[test]
720 fn compute_stats_basic() {
721 let original = make_result(vec![make_suite(
722 "unit",
723 vec![
724 make_test("test_a", TestStatus::Failed),
725 make_test("test_b", TestStatus::Failed),
726 ],
727 )]);
728
729 let attempt = RetryAttempt {
730 attempt: 1,
731 result: make_result(vec![make_suite(
732 "unit",
733 vec![make_test("test_a", TestStatus::Passed)],
734 )]),
735 duration: Duration::from_millis(200),
736 };
737
738 let retry_result = build_retry_result(original, vec![attempt]);
739 let stats = compute_retry_stats(&retry_result);
740
741 assert_eq!(stats.total_retries, 1);
742 assert_eq!(stats.tests_retried, 2);
743 assert_eq!(stats.tests_fixed, 1);
744 assert_eq!(stats.tests_still_failing, 1);
745 assert_eq!(stats.total_retry_time, Duration::from_millis(200));
746 }
747
748 #[test]
749 fn compute_stats_multiple_attempts() {
750 let original = make_result(vec![make_suite(
751 "unit",
752 vec![
753 make_test("a", TestStatus::Failed),
754 make_test("b", TestStatus::Failed),
755 make_test("c", TestStatus::Failed),
756 ],
757 )]);
758
759 let a1 = RetryAttempt {
760 attempt: 1,
761 result: make_result(vec![make_suite(
762 "unit",
763 vec![make_test("a", TestStatus::Passed)],
764 )]),
765 duration: Duration::from_millis(100),
766 };
767
768 let a2 = RetryAttempt {
769 attempt: 2,
770 result: make_result(vec![make_suite(
771 "unit",
772 vec![make_test("b", TestStatus::Passed)],
773 )]),
774 duration: Duration::from_millis(100),
775 };
776
777 let retry_result = build_retry_result(original, vec![a1, a2]);
778 let stats = compute_retry_stats(&retry_result);
779
780 assert_eq!(stats.total_retries, 2);
781 assert_eq!(stats.tests_retried, 3);
782 assert_eq!(stats.tests_fixed, 2);
783 assert_eq!(stats.tests_still_failing, 1);
784 assert_eq!(stats.total_retry_time, Duration::from_millis(200));
785 }
786}