consul/sdk/testutil/retry/retry_test.go
Matt Keeler 37636eab71
Catalog V2 Container Based Integration Test (#17674)
* Implement the Catalog V2 controller integration container tests

This now allows the container tests to import things from the root module. However for now we want to be very restrictive about which packages we allow importing.

* Add an upgrade test for the new catalog

Currently this should be dormant and not executed. However its put in place to detect breaking changes in the future and show an example of how to do an upgrade test with integration tests structured like catalog v2.

* Make testutil.Retry capable of performing cleanup operations

These cleanup operations are executed after each retry attempt.

* Move TestContext to taking an interface instead of a concrete testing.T

This allows this to be used on a retry.R or generally anything that meets the interface.

* Move to using TestContext instead of background contexts

Also this forces all test methods to implement the Cleanup method now instead of that being an optional interface.


Co-authored-by: Daniel Upton <daniel@floppy.co>
2023-06-16 16:29:50 -04:00

210 lines
4.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package retry
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// delta defines the time band a test run should complete in.
var delta = 25 * time.Millisecond
func TestRetryer(t *testing.T) {
tests := []struct {
desc string
r Retryer
}{
{"counter", &Counter{Count: 3, Wait: 100 * time.Millisecond}},
{"timer", &Timer{Timeout: 200 * time.Millisecond, Wait: 100 * time.Millisecond}},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
var iters int
start := time.Now()
for tt.r.Continue() {
iters++
}
dur := time.Since(start)
if got, want := iters, 3; got != want {
t.Fatalf("got %d retries want %d", got, want)
}
// since the first iteration happens immediately
// the retryer waits only twice for three iterations.
// order of events: (true, (wait) true, (wait) true, false)
if got, want := dur, 200*time.Millisecond; got < (want-delta) || got > (want+delta) {
t.Fatalf("loop took %v want %v (+/- %v)", got, want, delta)
}
})
}
}
func TestBasics(t *testing.T) {
t.Run("Error allows retry", func(t *testing.T) {
i := 0
Run(t, func(r *R) {
i++
t.Logf("i: %d; r: %#v", i, r)
if i == 1 {
r.Errorf("Errorf, i: %d", i)
return
}
})
assert.Equal(t, i, 2)
})
t.Run("Fatal returns from func, but does not fail test", func(t *testing.T) {
i := 0
gotHere := false
ft := &fakeT{}
Run(ft, func(r *R) {
i++
t.Logf("i: %d; r: %#v", i, r)
if i == 1 {
r.Fatalf("Fatalf, i: %d", i)
gotHere = true
}
})
assert.False(t, gotHere)
assert.Equal(t, i, 2)
// surprisingly, r.FailNow() *does not* trigger ft.FailNow()!
assert.Equal(t, ft.fails, 0)
})
t.Run("Func being run can panic with struct{}{}", func(t *testing.T) {
gotPanic := false
func() {
defer func() {
if p := recover(); p != nil {
gotPanic = true
}
}()
Run(t, func(r *R) {
panic(struct{}{})
})
}()
assert.True(t, gotPanic)
})
}
func TestRunWith(t *testing.T) {
t.Run("calls FailNow after exceeding retries", func(t *testing.T) {
ft := &fakeT{}
iter := 0
RunWith(&Counter{Count: 3, Wait: time.Millisecond}, ft, func(r *R) {
iter++
r.FailNow()
})
require.Equal(t, 3, iter)
require.Equal(t, 1, ft.fails)
})
t.Run("Stop ends the retrying", func(t *testing.T) {
ft := &fakeT{}
iter := 0
RunWith(&Counter{Count: 5, Wait: time.Millisecond}, ft, func(r *R) {
iter++
if iter == 2 {
r.Stop(fmt.Errorf("do not proceed"))
}
r.Fatalf("not yet")
})
// TODO: these should all be assert
require.Equal(t, 2, iter)
require.Equal(t, 1, ft.fails)
require.Len(t, ft.out, 1)
require.Contains(t, ft.out[0], "not yet\n")
require.Contains(t, ft.out[0], "do not proceed\n")
})
}
func TestCleanup(t *testing.T) {
t.Run("basic", func(t *testing.T) {
ft := &fakeT{}
cleanupsExecuted := 0
RunWith(&Counter{Count: 2, Wait: time.Millisecond}, ft, func(r *R) {
r.Cleanup(func() {
cleanupsExecuted += 1
})
})
require.Equal(t, 0, ft.fails)
require.Equal(t, 1, cleanupsExecuted)
})
t.Run("cleanup-panic-recovery", func(t *testing.T) {
ft := &fakeT{}
cleanupsExecuted := 0
RunWith(&Counter{Count: 2, Wait: time.Millisecond}, ft, func(r *R) {
r.Cleanup(func() {
cleanupsExecuted += 1
})
r.Cleanup(func() {
cleanupsExecuted += 1
panic(fmt.Errorf("fake test error"))
})
r.Cleanup(func() {
cleanupsExecuted += 1
})
// test is successful but should fail due to the cleanup panicing
})
require.Equal(t, 3, cleanupsExecuted)
require.Equal(t, 1, ft.fails)
require.Contains(t, ft.out[0], "fake test error")
})
t.Run("cleanup-per-retry", func(t *testing.T) {
ft := &fakeT{}
iter := 0
cleanupsExecuted := 0
RunWith(&Counter{Count: 3, Wait: time.Millisecond}, ft, func(r *R) {
if cleanupsExecuted != iter {
r.Stop(fmt.Errorf("cleanups not executed between retries"))
return
}
iter += 1
r.Cleanup(func() {
cleanupsExecuted += 1
})
r.FailNow()
})
require.Equal(t, 3, cleanupsExecuted)
// ensure that r.Stop hadn't been called. If it was then we would
// have log output
require.Len(t, ft.out, 0)
})
}
type fakeT struct {
fails int
out []string
}
func (f *fakeT) Helper() {}
func (f *fakeT) Log(args ...interface{}) {
f.out = append(f.out, fmt.Sprint(args...))
}
func (f *fakeT) FailNow() {
f.fails++
}
var _ Failer = &fakeT{}