197 lines
3.9 KiB
Go
197 lines
3.9 KiB
Go
|
|
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()
|
||
|
|
}
|
||
|
|
}
|