mirror of https://github.com/status-im/consul.git
chore: document and unit test sdk/testutil/retry (#16049)
This commit is contained in:
parent
9d55cd1f18
commit
8997f2bff1
|
@ -5,10 +5,17 @@
|
||||||
// func TestX(t *testing.T) {
|
// func TestX(t *testing.T) {
|
||||||
// retry.Run(t, func(r *retry.R) {
|
// retry.Run(t, func(r *retry.R) {
|
||||||
// if err := foo(); err != nil {
|
// if err := foo(); err != nil {
|
||||||
// r.Fatal("f: ", err)
|
// r.Errorf("foo: %s", err)
|
||||||
|
// return
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
|
//
|
||||||
|
// Run uses the DefaultFailer, which is a Timer with a Timeout of 7s,
|
||||||
|
// and a Wait of 25ms. To customize, use RunWith.
|
||||||
|
//
|
||||||
|
// WARNING: unlike *testing.T, *retry.R#Fatal and FailNow *do not*
|
||||||
|
// fail the test function entirely, only the current run the retry func
|
||||||
package retry
|
package retry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -31,8 +38,16 @@ type Failer interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// R provides context for the retryer.
|
// R provides context for the retryer.
|
||||||
|
//
|
||||||
|
// Logs from Logf, (Error|Fatal)(f) are gathered in an internal buffer
|
||||||
|
// and printed only if the retryer fails. Printed logs are deduped and
|
||||||
|
// prefixed with source code line numbers
|
||||||
type R struct {
|
type R struct {
|
||||||
|
// fail is set by FailNow and (Fatal|Error)(f). It indicates the pass
|
||||||
|
// did not succeed, and should be retried
|
||||||
fail bool
|
fail bool
|
||||||
|
// done is set by Stop. It indicates the entire run was a failure,
|
||||||
|
// and triggers t.FailNow()
|
||||||
done bool
|
done bool
|
||||||
output []string
|
output []string
|
||||||
}
|
}
|
||||||
|
@ -43,33 +58,55 @@ func (r *R) Logf(format string, args ...interface{}) {
|
||||||
|
|
||||||
func (r *R) Helper() {}
|
func (r *R) Helper() {}
|
||||||
|
|
||||||
var runFailed = struct{}{}
|
// runFailed is a sentinel value to indicate that the func itself
|
||||||
|
// didn't panic, rather that `FailNow` was called.
|
||||||
|
type runFailed struct{}
|
||||||
|
|
||||||
|
// FailNow stops run execution. It is roughly equivalent to:
|
||||||
|
//
|
||||||
|
// r.Error("")
|
||||||
|
// return
|
||||||
|
//
|
||||||
|
// inside the function being run.
|
||||||
func (r *R) FailNow() {
|
func (r *R) FailNow() {
|
||||||
r.fail = true
|
r.fail = true
|
||||||
panic(runFailed)
|
panic(runFailed{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fatal is equivalent to r.Logf(args) followed by r.FailNow(), i.e. the run
|
||||||
|
// function should be exited. Retries on the next run are allowed. Fatal is
|
||||||
|
// equivalent to
|
||||||
|
//
|
||||||
|
// r.Error(args)
|
||||||
|
// return
|
||||||
|
//
|
||||||
|
// inside the function being run.
|
||||||
func (r *R) Fatal(args ...interface{}) {
|
func (r *R) Fatal(args ...interface{}) {
|
||||||
r.log(fmt.Sprint(args...))
|
r.log(fmt.Sprint(args...))
|
||||||
r.FailNow()
|
r.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fatalf is like Fatal but allows a format string
|
||||||
func (r *R) Fatalf(format string, args ...interface{}) {
|
func (r *R) Fatalf(format string, args ...interface{}) {
|
||||||
r.log(fmt.Sprintf(format, args...))
|
r.log(fmt.Sprintf(format, args...))
|
||||||
r.FailNow()
|
r.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error indicates the current run encountered an error and should be retried.
|
||||||
|
// It *does not* stop execution of the rest of the run function.
|
||||||
func (r *R) Error(args ...interface{}) {
|
func (r *R) Error(args ...interface{}) {
|
||||||
r.log(fmt.Sprint(args...))
|
r.log(fmt.Sprint(args...))
|
||||||
r.fail = true
|
r.fail = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Errorf is like Error but allows a format string
|
||||||
func (r *R) Errorf(format string, args ...interface{}) {
|
func (r *R) Errorf(format string, args ...interface{}) {
|
||||||
r.log(fmt.Sprintf(format, args...))
|
r.log(fmt.Sprintf(format, args...))
|
||||||
r.fail = true
|
r.fail = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If err is non-nil, equivalent to r.Fatal(err.Error()) followed by
|
||||||
|
// r.FailNow(). Otherwise a no-op.
|
||||||
func (r *R) Check(err error) {
|
func (r *R) Check(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.log(err.Error())
|
r.log(err.Error())
|
||||||
|
@ -81,7 +118,8 @@ func (r *R) log(s string) {
|
||||||
r.output = append(r.output, decorate(s))
|
r.output = append(r.output, decorate(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop retrying, and fail the test with the specified error.
|
// Stop retrying, and fail the test, logging the specified error.
|
||||||
|
// Does not stop execution, so return should be called after.
|
||||||
func (r *R) Stop(err error) {
|
func (r *R) Stop(err error) {
|
||||||
r.log(err.Error())
|
r.log(err.Error())
|
||||||
r.done = true
|
r.done = true
|
||||||
|
@ -142,9 +180,11 @@ func run(r Retryer, t Failer, f func(r *R)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for r.Continue() {
|
for r.Continue() {
|
||||||
|
// run f(rr), but if recover yields a runFailed value, we know
|
||||||
|
// FailNow was called.
|
||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if p := recover(); p != nil && p != runFailed {
|
if p := recover(); p != nil && p != (runFailed{}) {
|
||||||
panic(p)
|
panic(p)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -163,7 +203,8 @@ func run(r Retryer, t Failer, f func(r *R)) {
|
||||||
fail()
|
fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultFailer provides default retry.Run() behavior for unit tests.
|
// DefaultFailer provides default retry.Run() behavior for unit tests, namely
|
||||||
|
// 7s timeout with a wait of 25ms
|
||||||
func DefaultFailer() *Timer {
|
func DefaultFailer() *Timer {
|
||||||
return &Timer{Timeout: 7 * time.Second, Wait: 25 * time.Millisecond}
|
return &Timer{Timeout: 7 * time.Second, Wait: 25 * time.Millisecond}
|
||||||
}
|
}
|
||||||
|
@ -213,6 +254,7 @@ type Timer struct {
|
||||||
Wait time.Duration
|
Wait time.Duration
|
||||||
|
|
||||||
// stop is the timeout deadline.
|
// stop is the timeout deadline.
|
||||||
|
// TODO: Next()?
|
||||||
// Set on the first invocation of Next().
|
// Set on the first invocation of Next().
|
||||||
stop time.Time
|
stop time.Time
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,6 +42,56 @@ func TestRetryer(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestRunWith(t *testing.T) {
|
||||||
t.Run("calls FailNow after exceeding retries", func(t *testing.T) {
|
t.Run("calls FailNow after exceeding retries", func(t *testing.T) {
|
||||||
ft := &fakeT{}
|
ft := &fakeT{}
|
||||||
|
@ -65,6 +116,7 @@ func TestRunWith(t *testing.T) {
|
||||||
r.Fatalf("not yet")
|
r.Fatalf("not yet")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: these should all be assert
|
||||||
require.Equal(t, 2, iter)
|
require.Equal(t, 2, iter)
|
||||||
require.Equal(t, 1, ft.fails)
|
require.Equal(t, 1, ft.fails)
|
||||||
require.Len(t, ft.out, 1)
|
require.Len(t, ft.out, 1)
|
||||||
|
|
Loading…
Reference in New Issue