Files
raven/internal/backoff/backoff_test.go

197 lines
3.9 KiB
Go
Raw Normal View History

package backoff
import (
"math"
"math/rand/v2"
"strings"
"testing"
"time"
)
func TestConfigValidate(t *testing.T) {
var tests = []struct {
name string
cfg Config
shouldError bool
}{
{
name: "initial zero",
cfg: Config{Initial: 0, Max: time.Second},
shouldError: true,
},
{
name: "initial negative",
cfg: Config{Initial: -1, Max: time.Second},
shouldError: true,
},
{
name: "max less than initial",
cfg: Config{
Initial: time.Second,
Max: 500 * time.Millisecond,
},
shouldError: true,
},
{
name: "max equals maxint64",
cfg: Config{
Initial: time.Second,
Max: math.MaxInt64,
},
shouldError: true,
},
{
name: "valid",
cfg: Config{
Initial: time.Second,
Max: time.Second,
},
shouldError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.Validate()
if tt.shouldError && err == nil {
t.Fatalf("expected error, got nil")
}
if !tt.shouldError && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestNewInvalidConfig(t *testing.T) {
_, err := New(Config{Initial: 0, Max: time.Second})
if err == nil {
t.Fatalf("expected error, got nil")
}
if !strings.Contains(err.Error(), "invalid config") {
t.Fatalf("expected invalid config error, got %v", err)
}
}
// TestDistribution uses a fixed seed to check the logic flow and jitter.
func TestDistribution(t *testing.T) {
// RNG with seed (1, 2)
rng := rand.New(rand.NewPCG(1, 2))
initial := 100 * time.Millisecond
maxDur := 400 * time.Millisecond
b, _ := New(Config{
Initial: initial,
Max: maxDur,
RNG: rng,
})
// Generate reference numbers using the same seed to predict outcomes.
refRng := rand.New(rand.NewPCG(1, 2))
expectedCaps := []time.Duration{
100 * time.Millisecond,
200 * time.Millisecond,
400 * time.Millisecond,
400 * time.Millisecond, // Clamped
}
for i, cap := range expectedCaps {
// Expect Next to pick rand(0, cap).
expected := time.Duration(refRng.Int64N(int64(cap) + 1))
got := b.Next()
if got != expected {
t.Errorf(
"step %d: expected %v, got %v (cap was %v)",
i, expected, got, cap,
)
}
if got > cap {
t.Errorf(
"step %d: got %v greater than cap %v",
i, got, cap,
)
}
}
}
// TestOverflowProtection ensures that large Max values do not cause overflow
// during the doubling phase.
func TestOverflowProtection(t *testing.T) {
// Use a Max that is near MaxInt64.
max := time.Duration(math.MaxInt64 - 1)
start := max / 4
b, err := New(Config{
Initial: start,
Max: max,
})
if err != nil {
t.Fatalf("New failed: %v", err)
}
// 1. Current = start.
// Next returns [0, start]. state becomes start*2 (max/2).
_ = b.Next()
// 2. Current = max/2.
// Next returns [0, max/2].
_ = b.Next()
// 3. Current = max.
// Next returns [0, max].
// Logic check: current >= max. state stays max.
val := b.Next()
if val < 0 {
t.Errorf("got negative duration %v, likely overflow", val)
}
// Verify clamp.
// No panic implies success.
for range 5 {
b.Next()
}
}
func TestDefaultRNG(t *testing.T) {
b, err := New(Config{
Initial: time.Millisecond,
Max: 10 * time.Millisecond,
})
if err != nil {
t.Fatalf("New failed: %v", err)
}
got := b.Next()
if got < 0 || got > time.Millisecond {
t.Errorf("out of bounds: %v", got)
}
}
func TestReset(t *testing.T) {
b, _ := New(Config{
Initial: 10 * time.Millisecond,
Max: 100 * time.Millisecond,
})
// Advance state
b.Next() // 10
b.Next() // 20
b.Next() // 40
b.Reset()
// Verify that the cap is reset by checking the bounds of the next
// call. If reset works, the next call is bounded by Initial (10ms).
for range 10 {
got := b.Next()
if got > 10*time.Millisecond {
t.Fatalf(
"call after Reset returned %v; expected <= 10ms",
got,
)
}
b.Reset()
}
}