mirror of https://github.com/hashicorp/consul
Browse Source
* Add a make target to run lint-consul-retry on all the modules * Cleanup sdk/testutil/retry * Fix a bunch of retry.Run* usage to not use the outer testing.T * Fix some more recent retry lint issues and pin to v1.4.0 of lint-consul-retry * Fix codegen copywrite lint issues * Don’t perform cleanup after each retry attempt by default. * Use the common testutil.TestingTB interface in test-integ/tenancy * Fix retry tests * Update otel access logging extension test to perform requests within the retry blockpull/19840/head
Matt Keeler
12 months ago
committed by
GitHub
45 changed files with 585 additions and 373 deletions
@ -0,0 +1,22 @@ |
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
// Package retry provides support for repeating operations in tests.
|
||||||
|
//
|
||||||
|
// A sample retry operation looks like this:
|
||||||
|
//
|
||||||
|
// func TestX(t *testing.T) {
|
||||||
|
// retry.Run(t, func(r *retry.R) {
|
||||||
|
// if err := foo(); err != nil {
|
||||||
|
// 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 |
@ -0,0 +1,35 @@ |
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package retry |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
) |
||||||
|
|
||||||
|
var nilInf TestingTB = nil |
||||||
|
|
||||||
|
// Assertion that our TestingTB can be passed to
|
||||||
|
var _ require.TestingT = nilInf |
||||||
|
var _ assert.TestingT = nilInf |
||||||
|
|
||||||
|
// TestingTB is an interface that describes the implementation of the testing object.
|
||||||
|
// Using an interface that describes testing.TB instead of the actual implementation
|
||||||
|
// makes testutil usable in a wider variety of contexts (e.g. use with ginkgo : https://godoc.org/github.com/onsi/ginkgo#GinkgoT)
|
||||||
|
type TestingTB interface { |
||||||
|
Cleanup(func()) |
||||||
|
Error(args ...any) |
||||||
|
Errorf(format string, args ...any) |
||||||
|
Fail() |
||||||
|
FailNow() |
||||||
|
Failed() bool |
||||||
|
Fatal(args ...any) |
||||||
|
Fatalf(format string, args ...any) |
||||||
|
Helper() |
||||||
|
Log(args ...any) |
||||||
|
Logf(format string, args ...any) |
||||||
|
Name() string |
||||||
|
Setenv(key, value string) |
||||||
|
TempDir() string |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package retry |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"runtime" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
func dedup(a []string) string { |
||||||
|
if len(a) == 0 { |
||||||
|
return "" |
||||||
|
} |
||||||
|
seen := map[string]struct{}{} |
||||||
|
var b bytes.Buffer |
||||||
|
for _, s := range a { |
||||||
|
if _, ok := seen[s]; ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
seen[s] = struct{}{} |
||||||
|
b.WriteString(s) |
||||||
|
b.WriteRune('\n') |
||||||
|
} |
||||||
|
return b.String() |
||||||
|
} |
||||||
|
|
||||||
|
func decorate(s string) string { |
||||||
|
_, file, line, ok := runtime.Caller(3) |
||||||
|
if ok { |
||||||
|
n := strings.LastIndex(file, "/") |
||||||
|
if n >= 0 { |
||||||
|
file = file[n+1:] |
||||||
|
} |
||||||
|
} else { |
||||||
|
file = "???" |
||||||
|
line = 1 |
||||||
|
} |
||||||
|
return fmt.Sprintf("%s:%d: %s", file, line, s) |
||||||
|
} |
@ -1,263 +1,232 @@ |
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
// Package retry provides support for repeating operations in tests.
|
|
||||||
//
|
|
||||||
// A sample retry operation looks like this:
|
|
||||||
//
|
|
||||||
// func TestX(t *testing.T) {
|
|
||||||
// retry.Run(t, func(r *retry.R) {
|
|
||||||
// if err := foo(); err != nil {
|
|
||||||
// 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 ( |
||||||
"bytes" |
|
||||||
"fmt" |
"fmt" |
||||||
"runtime" |
"os" |
||||||
"strings" |
|
||||||
"time" |
|
||||||
) |
) |
||||||
|
|
||||||
// Failer is an interface compatible with testing.T.
|
var _ TestingTB = &R{} |
||||||
type Failer interface { |
|
||||||
Helper() |
|
||||||
|
|
||||||
// Log is called for the final test output
|
type R struct { |
||||||
Log(args ...interface{}) |
wrapped TestingTB |
||||||
|
retryer Retryer |
||||||
|
|
||||||
// FailNow is called when the retrying is abandoned.
|
done bool |
||||||
FailNow() |
fullOutput bool |
||||||
} |
immediateCleanup bool |
||||||
|
|
||||||
// R provides context for the retryer.
|
attempts []*attempt |
||||||
//
|
} |
||||||
// 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 { |
|
||||||
// fail is set by FailNow and (Fatal|Error)(f). It indicates the pass
|
|
||||||
// did not succeed, and should be retried
|
|
||||||
fail bool |
|
||||||
// done is set by Stop. It indicates the entire run was a failure,
|
|
||||||
// and triggers t.FailNow()
|
|
||||||
done bool |
|
||||||
output []string |
|
||||||
|
|
||||||
cleanups []func() |
func (r *R) Cleanup(clean func()) { |
||||||
|
if r.immediateCleanup { |
||||||
|
a := r.getCurrentAttempt() |
||||||
|
a.cleanups = append(a.cleanups, clean) |
||||||
|
} else { |
||||||
|
r.wrapped.Cleanup(clean) |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
func (r *R) Logf(format string, args ...interface{}) { |
func (r *R) Error(args ...any) { |
||||||
r.log(fmt.Sprintf(format, args...)) |
r.Log(args...) |
||||||
|
r.Fail() |
||||||
} |
} |
||||||
|
|
||||||
func (r *R) Log(args ...interface{}) { |
func (r *R) Errorf(format string, args ...any) { |
||||||
r.log(fmt.Sprintln(args...)) |
r.Logf(format, args...) |
||||||
|
r.Fail() |
||||||
} |
} |
||||||
|
|
||||||
func (r *R) Helper() {} |
func (r *R) Fail() { |
||||||
|
r.getCurrentAttempt().failed = true |
||||||
|
} |
||||||
|
|
||||||
// Cleanup register a function to be run to cleanup resources that
|
func (r *R) FailNow() { |
||||||
// were allocated during the retry attempt. These functions are executed
|
r.Fail() |
||||||
// after a retry attempt. If they panic, it will not stop further retry
|
panic(attemptFailed{}) |
||||||
// attempts but will be cause for the overall test failure.
|
|
||||||
func (r *R) Cleanup(fn func()) { |
|
||||||
r.cleanups = append(r.cleanups, fn) |
|
||||||
} |
} |
||||||
|
|
||||||
func (r *R) runCleanup() { |
func (r *R) Failed() bool { |
||||||
|
return r.getCurrentAttempt().failed |
||||||
|
} |
||||||
|
|
||||||
// Make sure that if a cleanup function panics,
|
func (r *R) Fatal(args ...any) { |
||||||
// we still run the remaining cleanup functions.
|
r.Log(args...) |
||||||
defer func() { |
r.FailNow() |
||||||
err := recover() |
} |
||||||
if err != nil { |
|
||||||
r.Stop(fmt.Errorf("error when performing test cleanup: %v", err)) |
|
||||||
} |
|
||||||
if len(r.cleanups) > 0 { |
|
||||||
r.runCleanup() |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
for len(r.cleanups) > 0 { |
func (r *R) Fatalf(format string, args ...any) { |
||||||
var cleanup func() |
r.Logf(format, args...) |
||||||
if len(r.cleanups) > 0 { |
r.FailNow() |
||||||
last := len(r.cleanups) - 1 |
|
||||||
cleanup = r.cleanups[last] |
|
||||||
r.cleanups = r.cleanups[:last] |
|
||||||
} |
|
||||||
if cleanup != nil { |
|
||||||
cleanup() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
} |
||||||
|
|
||||||
// runFailed is a sentinel value to indicate that the func itself
|
func (r *R) Helper() { |
||||||
// didn't panic, rather that `FailNow` was called.
|
// *testing.T will just record which functions are helpers by their addresses and
|
||||||
type runFailed struct{} |
// it doesn't much matter where where we record that they are helpers
|
||||||
|
r.wrapped.Helper() |
||||||
// FailNow stops run execution. It is roughly equivalent to:
|
|
||||||
//
|
|
||||||
// r.Error("")
|
|
||||||
// return
|
|
||||||
//
|
|
||||||
// inside the function being run.
|
|
||||||
func (r *R) FailNow() { |
|
||||||
r.fail = true |
|
||||||
panic(runFailed{}) |
|
||||||
} |
} |
||||||
|
|
||||||
// Fatal is equivalent to r.Logf(args) followed by r.FailNow(), i.e. the run
|
func (r *R) Log(args ...any) { |
||||||
// function should be exited. Retries on the next run are allowed. Fatal is
|
r.log(fmt.Sprintln(args...)) |
||||||
// equivalent to
|
|
||||||
//
|
|
||||||
// r.Error(args)
|
|
||||||
// return
|
|
||||||
//
|
|
||||||
// inside the function being run.
|
|
||||||
func (r *R) Fatal(args ...interface{}) { |
|
||||||
r.log(fmt.Sprint(args...)) |
|
||||||
r.FailNow() |
|
||||||
} |
} |
||||||
|
|
||||||
// Fatalf is like Fatal but allows a format string
|
func (r *R) Logf(format string, args ...any) { |
||||||
func (r *R) Fatalf(format string, args ...interface{}) { |
|
||||||
r.log(fmt.Sprintf(format, args...)) |
r.log(fmt.Sprintf(format, args...)) |
||||||
r.FailNow() |
|
||||||
} |
} |
||||||
|
|
||||||
// Error indicates the current run encountered an error and should be retried.
|
// Name will return the name of the underlying TestingT.
|
||||||
// It *does not* stop execution of the rest of the run function.
|
func (r *R) Name() string { |
||||||
func (r *R) Error(args ...interface{}) { |
return r.wrapped.Name() |
||||||
r.log(fmt.Sprint(args...)) |
|
||||||
r.fail = true |
|
||||||
} |
} |
||||||
|
|
||||||
// Errorf is like Error but allows a format string
|
// Setenv will save the current value of the specified env var, set it to the
|
||||||
func (r *R) Errorf(format string, args ...interface{}) { |
// specified value and then restore it to the original value in a cleanup function
|
||||||
r.log(fmt.Sprintf(format, args...)) |
// once the retry attempt has finished.
|
||||||
r.fail = true |
func (r *R) Setenv(key, value string) { |
||||||
|
prevValue, ok := os.LookupEnv(key) |
||||||
|
|
||||||
|
if err := os.Setenv(key, value); err != nil { |
||||||
|
r.wrapped.Fatalf("cannot set environment variable: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
if ok { |
||||||
|
r.Cleanup(func() { |
||||||
|
os.Setenv(key, prevValue) |
||||||
|
}) |
||||||
|
} else { |
||||||
|
r.Cleanup(func() { |
||||||
|
os.Unsetenv(key) |
||||||
|
}) |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
// If err is non-nil, equivalent to r.Fatal(err.Error()) followed by
|
// TempDir will use the wrapped TestingT to create a temporary directory
|
||||||
// r.FailNow(). Otherwise a no-op.
|
// that will be cleaned up when ALL RETRYING has finished.
|
||||||
|
func (r *R) TempDir() string { |
||||||
|
return r.wrapped.TempDir() |
||||||
|
} |
||||||
|
|
||||||
|
// Check will call r.Fatal(err) if err is not nil
|
||||||
func (r *R) Check(err error) { |
func (r *R) Check(err error) { |
||||||
if err != nil { |
if err != nil { |
||||||
r.log(err.Error()) |
r.Fatal(err) |
||||||
r.FailNow() |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
func (r *R) log(s string) { |
|
||||||
r.output = append(r.output, decorate(s)) |
|
||||||
} |
|
||||||
|
|
||||||
// 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 |
||||||
} |
} |
||||||
|
|
||||||
func decorate(s string) string { |
func (r *R) failCurrentAttempt() { |
||||||
_, file, line, ok := runtime.Caller(3) |
r.getCurrentAttempt().failed = true |
||||||
if ok { |
|
||||||
n := strings.LastIndex(file, "/") |
|
||||||
if n >= 0 { |
|
||||||
file = file[n+1:] |
|
||||||
} |
|
||||||
} else { |
|
||||||
file = "???" |
|
||||||
line = 1 |
|
||||||
} |
|
||||||
return fmt.Sprintf("%s:%d: %s", file, line, s) |
|
||||||
} |
} |
||||||
|
|
||||||
func Run(t Failer, f func(r *R)) { |
func (r *R) log(s string) { |
||||||
t.Helper() |
a := r.getCurrentAttempt() |
||||||
run(DefaultFailer(), t, f) |
a.output = append(a.output, decorate(s)) |
||||||
} |
} |
||||||
|
|
||||||
func RunWith(r Retryer, t Failer, f func(r *R)) { |
func (r *R) getCurrentAttempt() *attempt { |
||||||
t.Helper() |
if len(r.attempts) == 0 { |
||||||
run(r, t, f) |
panic("no retry attempts have been started yet") |
||||||
|
} |
||||||
|
|
||||||
|
return r.attempts[len(r.attempts)-1] |
||||||
} |
} |
||||||
|
|
||||||
func dedup(a []string) string { |
// cleanupAttempt will perform all the register cleanup operations recorded
|
||||||
if len(a) == 0 { |
// during execution of the single round of the test function.
|
||||||
return "" |
func (r *R) cleanupAttempt(a *attempt) { |
||||||
} |
// Make sure that if a cleanup function panics,
|
||||||
seen := map[string]struct{}{} |
// we still run the remaining cleanup functions.
|
||||||
var b bytes.Buffer |
defer func() { |
||||||
for _, s := range a { |
err := recover() |
||||||
if _, ok := seen[s]; ok { |
if err != nil { |
||||||
continue |
r.Stop(fmt.Errorf("error when performing test cleanup: %v", err)) |
||||||
|
} |
||||||
|
if len(a.cleanups) > 0 { |
||||||
|
r.cleanupAttempt(a) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
for len(a.cleanups) > 0 { |
||||||
|
var cleanup func() |
||||||
|
if len(a.cleanups) > 0 { |
||||||
|
last := len(a.cleanups) - 1 |
||||||
|
cleanup = a.cleanups[last] |
||||||
|
a.cleanups = a.cleanups[:last] |
||||||
|
} |
||||||
|
if cleanup != nil { |
||||||
|
cleanup() |
||||||
} |
} |
||||||
seen[s] = struct{}{} |
|
||||||
b.WriteString(s) |
|
||||||
b.WriteRune('\n') |
|
||||||
} |
} |
||||||
return b.String() |
|
||||||
} |
} |
||||||
|
|
||||||
func run(r Retryer, t Failer, f func(r *R)) { |
// runAttempt will execute one round of the test function and handle cleanups and panic recovery
|
||||||
t.Helper() |
// of a failed attempt that should not stop retrying.
|
||||||
rr := &R{} |
func (r *R) runAttempt(f func(r *R)) { |
||||||
|
r.Helper() |
||||||
|
|
||||||
|
a := &attempt{} |
||||||
|
r.attempts = append(r.attempts, a) |
||||||
|
|
||||||
fail := func() { |
defer r.cleanupAttempt(a) |
||||||
t.Helper() |
defer func() { |
||||||
out := dedup(rr.output) |
if p := recover(); p != nil && p != (attemptFailed{}) { |
||||||
if out != "" { |
panic(p) |
||||||
t.Log(out) |
|
||||||
} |
} |
||||||
t.FailNow() |
}() |
||||||
} |
f(r) |
||||||
|
} |
||||||
|
|
||||||
|
func (r *R) run(f func(r *R)) { |
||||||
|
r.Helper() |
||||||
|
|
||||||
for r.Continue() { |
for r.retryer.Continue() { |
||||||
// run f(rr), but if recover yields a runFailed value, we know
|
r.runAttempt(f) |
||||||
// FailNow was called.
|
|
||||||
func() { |
|
||||||
defer rr.runCleanup() |
|
||||||
defer func() { |
|
||||||
if p := recover(); p != nil && p != (runFailed{}) { |
|
||||||
panic(p) |
|
||||||
} |
|
||||||
}() |
|
||||||
f(rr) |
|
||||||
}() |
|
||||||
|
|
||||||
switch { |
switch { |
||||||
case rr.done: |
case r.done: |
||||||
fail() |
r.recordRetryFailure() |
||||||
return |
return |
||||||
case !rr.fail: |
case !r.Failed(): |
||||||
|
// the current attempt did not fail so we can go ahead and return
|
||||||
return |
return |
||||||
} |
} |
||||||
rr.fail = false |
|
||||||
} |
} |
||||||
fail() |
|
||||||
|
// We cannot retry any more and no attempt has succeeded yet.
|
||||||
|
r.recordRetryFailure() |
||||||
} |
} |
||||||
|
|
||||||
// DefaultFailer provides default retry.Run() behavior for unit tests, namely
|
func (r *R) recordRetryFailure() { |
||||||
// 7s timeout with a wait of 25ms
|
r.Helper() |
||||||
func DefaultFailer() *Timer { |
output := r.getCurrentAttempt().output |
||||||
return &Timer{Timeout: 7 * time.Second, Wait: 25 * time.Millisecond} |
if r.fullOutput { |
||||||
|
var combined []string |
||||||
|
for _, attempt := range r.attempts { |
||||||
|
combined = append(combined, attempt.output...) |
||||||
|
} |
||||||
|
output = combined |
||||||
|
} |
||||||
|
|
||||||
|
out := dedup(output) |
||||||
|
if out != "" { |
||||||
|
r.wrapped.Log(out) |
||||||
|
} |
||||||
|
r.wrapped.FailNow() |
||||||
} |
} |
||||||
|
|
||||||
// Retryer provides an interface for repeating operations
|
type attempt struct { |
||||||
// until they succeed or an exit condition is met.
|
failed bool |
||||||
type Retryer interface { |
output []string |
||||||
// Continue returns true if the operation should be repeated, otherwise it
|
cleanups []func() |
||||||
// returns false to indicate retrying should stop.
|
|
||||||
Continue() bool |
|
||||||
} |
} |
||||||
|
|
||||||
|
// attemptFailed is a sentinel value to indicate that the func itself
|
||||||
|
// didn't panic, rather that `FailNow` was called.
|
||||||
|
type attemptFailed struct{} |
||||||
|
@ -0,0 +1,36 @@ |
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package retry |
||||||
|
|
||||||
|
import "time" |
||||||
|
|
||||||
|
// Retryer provides an interface for repeating operations
|
||||||
|
// until they succeed or an exit condition is met.
|
||||||
|
type Retryer interface { |
||||||
|
// Continue returns true if the operation should be repeated, otherwise it
|
||||||
|
// returns false to indicate retrying should stop.
|
||||||
|
Continue() bool |
||||||
|
} |
||||||
|
|
||||||
|
// DefaultRetryer provides default retry.Run() behavior for unit tests, namely
|
||||||
|
// 7s timeout with a wait of 25ms
|
||||||
|
func DefaultRetryer() Retryer { |
||||||
|
return &Timer{Timeout: 7 * time.Second, Wait: 25 * time.Millisecond} |
||||||
|
} |
||||||
|
|
||||||
|
// ThirtySeconds repeats an operation for thirty seconds and waits 500ms in between.
|
||||||
|
// Best for known slower operations like waiting on eventually consistent state.
|
||||||
|
func ThirtySeconds() *Timer { |
||||||
|
return &Timer{Timeout: 30 * time.Second, Wait: 500 * time.Millisecond} |
||||||
|
} |
||||||
|
|
||||||
|
// TwoSeconds repeats an operation for two seconds and waits 25ms in between.
|
||||||
|
func TwoSeconds() *Timer { |
||||||
|
return &Timer{Timeout: 2 * time.Second, Wait: 25 * time.Millisecond} |
||||||
|
} |
||||||
|
|
||||||
|
// ThreeTimes repeats an operation three times and waits 25ms in between.
|
||||||
|
func ThreeTimes() *Counter { |
||||||
|
return &Counter{Count: 3, Wait: 25 * time.Millisecond} |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package retry |
||||||
|
|
||||||
|
type Option func(r *R) |
||||||
|
|
||||||
|
func WithRetryer(retryer Retryer) Option { |
||||||
|
return func(r *R) { |
||||||
|
r.retryer = retryer |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func WithFullOutput() Option { |
||||||
|
return func(r *R) { |
||||||
|
r.fullOutput = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// WithImmediateCleanup will cause all cleanup operations added
|
||||||
|
// by calling the Cleanup method on *R to be performed after
|
||||||
|
// the retry attempt completes (regardless of pass/fail status)
|
||||||
|
// Use this only if all resources created during the retry loop should
|
||||||
|
// not persist after the retry has finished.
|
||||||
|
func WithImmediateCleanup() Option { |
||||||
|
return func(r *R) { |
||||||
|
r.immediateCleanup = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Run(t TestingTB, f func(r *R), opts ...Option) { |
||||||
|
t.Helper() |
||||||
|
r := &R{ |
||||||
|
wrapped: t, |
||||||
|
retryer: DefaultRetryer(), |
||||||
|
} |
||||||
|
|
||||||
|
for _, opt := range opts { |
||||||
|
opt(r) |
||||||
|
} |
||||||
|
|
||||||
|
r.run(f) |
||||||
|
} |
||||||
|
|
||||||
|
func RunWith(r Retryer, t TestingTB, f func(r *R)) { |
||||||
|
t.Helper() |
||||||
|
Run(t, f, WithRetryer(r)) |
||||||
|
} |
Loading…
Reference in new issue