package playback import ( "context" "errors" "reflect" "testing" "time" ) func testRetryPolicy(maxAttempts int) RetryPolicy { return RetryPolicy{ MaxAttempts: maxAttempts, InitialDelay: 500 * time.Millisecond, MaxDelay: 10 * time.Second, } } func TestRunWithRetryFirstAttemptSucceeds(t *testing.T) { attempts := 0 err := runWithRetry( context.Background(), testRetryPolicy(3), func(context.Context) (bool, error) { attempts++ return false, nil }, func(error) bool { t.Fatal("shouldRetry called after successful attempt") return false }, func(context.Context, time.Duration) error { t.Fatal("wait called after successful attempt") return nil }, nil, ) if err != nil { t.Fatalf("runWithRetry() error = %v, want nil", err) } if attempts != 1 { t.Fatalf("attempt count = %d, want 1", attempts) } } func TestRunWithRetryFailuresThenSuccess(t *testing.T) { attemptErr := errors.New("attempt failed") attempts := 0 var delays []time.Duration err := runWithRetry( context.Background(), testRetryPolicy(3), func(context.Context) (bool, error) { attempts++ if attempts < 3 { return false, attemptErr } return false, nil }, func(error) bool { return true }, func(_ context.Context, delay time.Duration) error { delays = append(delays, delay) return nil }, nil, ) if err != nil { t.Fatalf("runWithRetry() error = %v, want nil", err) } if attempts != 3 { t.Errorf("attempt count = %d, want 3", attempts) } wantDelays := []time.Duration{500 * time.Millisecond, time.Second} if !reflect.DeepEqual(delays, wantDelays) { t.Errorf("retry delays = %v, want %v", delays, wantDelays) } } func TestRunWithRetryFiniteAttemptsExhausted(t *testing.T) { attemptErr := errors.New("attempt failed") attempts := 0 waits := 0 err := runWithRetry( context.Background(), testRetryPolicy(3), func(context.Context) (bool, error) { attempts++ return false, attemptErr }, func(error) bool { return true }, func(context.Context, time.Duration) error { waits++ return nil }, nil, ) if !errors.Is(err, attemptErr) { t.Fatalf("runWithRetry() error = %v, want %v", err, attemptErr) } if attempts != 3 { t.Errorf("attempt count = %d, want 3", attempts) } if waits != 2 { t.Errorf("wait count = %d, want 2", waits) } } func TestRunWithRetryUnlimitedEventuallySucceeds(t *testing.T) { attemptErr := errors.New("attempt failed") attempts := 0 err := runWithRetry( context.Background(), testRetryPolicy(0), func(context.Context) (bool, error) { attempts++ if attempts < 20 { return false, attemptErr } return false, nil }, func(error) bool { return true }, func(context.Context, time.Duration) error { return nil }, nil, ) if err != nil { t.Fatalf("runWithRetry() error = %v, want nil", err) } if attempts != 20 { t.Fatalf("attempt count = %d, want 20", attempts) } } func TestRunWithRetryStopsWhenErrorIsNotRetryable(t *testing.T) { attemptErr := errors.New("invalid configuration") attempts := 0 err := runWithRetry( context.Background(), testRetryPolicy(0), func(context.Context) (bool, error) { attempts++ return false, attemptErr }, func(error) bool { return false }, func(context.Context, time.Duration) error { t.Fatal("wait called for non-retryable error") return nil }, nil, ) if !errors.Is(err, attemptErr) { t.Fatalf("runWithRetry() error = %v, want %v", err, attemptErr) } if attempts != 1 { t.Fatalf("attempt count = %d, want 1", attempts) } } func TestRunWithRetryReturnsCancellationFromAttempt(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) attemptErr := errors.New("attempt failed") err := runWithRetry( ctx, testRetryPolicy(0), func(context.Context) (bool, error) { cancel() return false, attemptErr }, func(error) bool { return true }, func(context.Context, time.Duration) error { t.Fatal("wait called after cancellation") return nil }, nil, ) if !errors.Is(err, context.Canceled) { t.Fatalf("runWithRetry() error = %v, want context.Canceled", err) } } func TestRunWithRetryReturnsCancellationDuringBackoff(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) attemptErr := errors.New("attempt failed") err := runWithRetry( ctx, testRetryPolicy(0), func(context.Context) (bool, error) { return false, attemptErr }, func(error) bool { return true }, func(ctx context.Context, _ time.Duration) error { cancel() return ctx.Err() }, nil, ) if !errors.Is(err, context.Canceled) { t.Fatalf("runWithRetry() error = %v, want context.Canceled", err) } } func TestRunWithRetryReturnsWaitError(t *testing.T) { attemptErr := errors.New("attempt failed") waitErr := errors.New("wait failed") err := runWithRetry( context.Background(), testRetryPolicy(0), func(context.Context) (bool, error) { return false, attemptErr }, func(error) bool { return true }, func(context.Context, time.Duration) error { return waitErr }, nil, ) if !errors.Is(err, waitErr) { t.Fatalf("runWithRetry() error = %v, want %v", err, waitErr) } } func TestWaitForRetryReturnsCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() err := waitForRetry(ctx, time.Hour) if !errors.Is(err, context.Canceled) { t.Fatalf("waitForRetry() error = %v, want context.Canceled", err) } } func TestRetryObserverReportsFailuresBeforeSuccess(t *testing.T) { attemptErr := errors.New("attempt failed") attempts := 0 var events []retryEvent err := runWithRetry( context.Background(), testRetryPolicy(3), func(context.Context) (bool, error) { attempts++ if attempts < 3 { return false, attemptErr } return false, nil }, func(error) bool { return true }, func(context.Context, time.Duration) error { return nil }, func(event retryEvent) { events = append(events, event) }, ) if err != nil { t.Fatalf("runWithRetry() error = %v, want nil", err) } if len(events) != 2 { t.Fatalf("event count = %d, want 2", len(events)) } wantDelays := []time.Duration{500 * time.Millisecond, time.Second} for i, event := range events { wantAttempts := i + 1 if event.FailedAttempts != wantAttempts { t.Errorf("event %d failed attempts = %d, want %d", i, event.FailedAttempts, wantAttempts) } if !errors.Is(event.Err, attemptErr) { t.Errorf("event %d error = %v, want %v", i, event.Err, attemptErr) } if event.RetryIn != wantDelays[i] { t.Errorf("event %d retry delay = %s, want %s", i, event.RetryIn, wantDelays[i]) } if !event.WillRetry { t.Errorf("event %d WillRetry = false, want true", i) } } } func TestRetryObserverReportsExhaustion(t *testing.T) { attemptErr := errors.New("attempt failed") var events []retryEvent err := runWithRetry( context.Background(), testRetryPolicy(2), func(context.Context) (bool, error) { return false, attemptErr }, func(error) bool { return true }, func(context.Context, time.Duration) error { return nil }, func(event retryEvent) { events = append(events, event) }, ) if !errors.Is(err, attemptErr) { t.Fatalf("runWithRetry() error = %v, want %v", err, attemptErr) } if len(events) != 2 { t.Fatalf("event count = %d, want 2", len(events)) } if !events[0].WillRetry || events[0].RetryIn != 500*time.Millisecond { t.Errorf("first event = %+v, want retry after 500ms", events[0]) } final := events[1] if final.FailedAttempts != 2 { t.Errorf("final failed attempts = %d, want 2", final.FailedAttempts) } if final.WillRetry { t.Error("final WillRetry = true, want false") } if final.RetryIn != 0 { t.Errorf("final retry delay = %s, want 0", final.RetryIn) } if !errors.Is(final.Err, attemptErr) { t.Errorf("final error = %v, want %v", final.Err, attemptErr) } } func TestRetryObserverNotCalledOnImmediateSuccess(t *testing.T) { observerCalls := 0 err := runWithRetry( context.Background(), testRetryPolicy(3), func(context.Context) (bool, error) { return false, nil }, func(error) bool { return true }, func(context.Context, time.Duration) error { return nil }, func(retryEvent) { observerCalls++ }, ) if err != nil { t.Fatalf("runWithRetry() error = %v, want nil", err) } if observerCalls != 0 { t.Fatalf("observer call count = %d, want 0", observerCalls) } } func TestRetryObserverNotCalledWhenAttemptCancelsContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) observerCalls := 0 err := runWithRetry( ctx, testRetryPolicy(0), func(context.Context) (bool, error) { cancel() return false, errors.New("attempt interrupted") }, func(error) bool { return true }, func(context.Context, time.Duration) error { return nil }, func(retryEvent) { observerCalls++ }, ) if !errors.Is(err, context.Canceled) { t.Fatalf("runWithRetry() error = %v, want context.Canceled", err) } if observerCalls != 0 { t.Fatalf("observer call count = %d, want 0", observerCalls) } } func TestRunWithRetryResetsFailuresAfterStableAttempt(t *testing.T) { attemptErr := errors.New("attempt failed") attempts := 0 var events []retryEvent err := runWithRetry( context.Background(), testRetryPolicy(2), func(context.Context) (bool, error) { attempts++ switch attempts { case 1: return false, attemptErr case 2: return true, attemptErr default: return false, attemptErr } }, func(error) bool { return true }, func(context.Context, time.Duration) error { return nil }, func(event retryEvent) { events = append(events, event) }, ) if !errors.Is(err, attemptErr) { t.Fatalf("runWithRetry() error = %v, want %v", err, attemptErr) } if attempts != 3 { t.Fatalf("attempt count = %d, want 3", attempts) } if len(events) != 3 { t.Fatalf("event count = %d, want 3", len(events)) } wantFailedAttempts := []int{1, 1, 2} wantWillRetry := []bool{true, true, false} for i, event := range events { if event.FailedAttempts != wantFailedAttempts[i] { t.Errorf( "event %d failed attempts = %d, want %d", i, event.FailedAttempts, wantFailedAttempts[i], ) } if event.WillRetry != wantWillRetry[i] { t.Errorf( "event %d WillRetry = %t, want %t", i, event.WillRetry, wantWillRetry[i], ) } } }