Ctrl K
ComunidadGuías…

pkg/utils/retry_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package utils

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestRetryWithBackoffSucceedsAfterRetry(t *testing.T) {
	errTemporary := errors.New("temporary")
	calls := 0

	err := RetryWithBackoff(context.Background(), RetryOptions{
		Attempts: 3,
	}, func() error {
		calls++
		if calls < 3 {
			return errTemporary
		}
		return nil
	})

	require.NoError(t, err)
	assert.Equal(t, 3, calls)
}

func TestRetryWithBackoffValueReturnsSuccessfulValue(t *testing.T) {
	errTemporary := errors.New("temporary")
	calls := 0

	value, err := RetryWithBackoffValue(context.Background(), RetryOptions{
		Attempts: 3,
	}, func() (string, error) {
		calls++
		if calls < 2 {
			return "", errTemporary
		}
		return "done", nil
	})

	require.NoError(t, err)
	assert.Equal(t, "done", value)
	assert.Equal(t, 2, calls)
}

func TestRetryWithBackoffStopsOnNonRetryableError(t *testing.T) {
	errRetryable := errors.New("retryable")
	errFatal := errors.New("fatal")
	calls := 0

	err := RetryWithBackoff(context.Background(), RetryOptions{
		Attempts: 5,
		ShouldRetry: func(err error) bool {
			return errors.Is(err, errRetryable)
		},
	}, func() error {
		calls++
		if calls == 1 {
			return errRetryable
		}
		return errFatal
	})

	require.ErrorIs(t, err, errFatal)
	assert.Equal(t, 2, calls)
}

func TestRetryWithBackoffCapsDelay(t *testing.T) {
	errTemporary := errors.New("temporary")
	var delays []time.Duration

	err := RetryWithBackoff(context.Background(), RetryOptions{
		Attempts: 4,
		Delay:    time.Nanosecond,
		MaxDelay: 2 * time.Nanosecond,
		OnRetry: func(_ int, _ error, delay time.Duration) {
			delays = append(delays, delay)
		},
	}, func() error {
		return errTemporary
	})

	require.ErrorIs(t, err, errTemporary)
	assert.Equal(t, []time.Duration{
		time.Nanosecond,
		2 * time.Nanosecond,
		2 * time.Nanosecond,
	}, delays)
}

func TestRetryDelaySaturatesOnOverflow(t *testing.T) {
	assert.Equal(t, maxRetryDelay, backoffDelay(maxRetryDelay, 1, 0))
	assert.Equal(t, time.Hour, backoffDelay(maxRetryDelay, 1, time.Hour))
}

func TestRetryWithBackoffStopsWhenContextIsCanceled(t *testing.T) {
	errTemporary := errors.New("temporary")
	ctx, cancel := context.WithCancel(context.Background())
	calls := 0

	err := RetryWithBackoff(ctx, RetryOptions{
		Attempts: 3,
		Delay:    time.Hour,
		OnRetry: func(_ int, _ error, _ time.Duration) {
			cancel()
		},
	}, func() error {
		calls++
		return errTemporary
	})

	require.ErrorIs(t, err, context.Canceled)
	assert.Equal(t, 1, calls)
}

func TestRetryDelayWithJitter(t *testing.T) {
	base := 4 * time.Second
	hasJitter := false

	for i := 0; i < 100; i++ {
		delay := retryDelay(base, 0, 0, 0.25)

		assert.GreaterOrEqual(t, delay, base)
		assert.Less(t, delay, base+time.Second)
		hasJitter = hasJitter || delay > base
	}
	assert.True(t, hasJitter)
}

func TestRetryWithExpBackoffRunsAtLeastOnce(t *testing.T) {
	errTemporary := errors.New("temporary")
	calls := 0

	err := RetryWithExpBackoff(0, time.Nanosecond, func() error {
		calls++
		return errTemporary
	})

	require.ErrorIs(t, err, errTemporary)
	assert.Equal(t, 1, calls)
}