diff --git a/README.md b/README.md index 65ee1a5..33a2436 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,20 @@ This is an actively maintained fork of [avast/retry-go](https://github.com/avast/retry-go), focused on reliability and simplicity. We extend our gratitude to the original authors and contributors at Avast for creating this excellent library. +This fork retains the original v4 API provided by the retry-go codebase, and is a drop-in replacement. + **Key improvements in this fork:** + +- Zero dependencies (we removed usage of testify and it's dependencies) - Enhanced reliability and edge case handling -- Simplified API design - Active maintenance and bug fixes -- Focus on production-grade stability +- Adherance to Go best practices, namely: + - https://go.dev/wiki/TestComments + - https://go.dev/wiki/CodeReviewComments + - https://go.dev/doc/effective_go **Original Project:** [github.com/avast/retry-go](https://github.com/avast/retry-go) -Simple library for retry mechanism - -Slightly inspired by -[Try::Tiny::Retry](https://metacpan.org/pod/Try::Tiny::Retry) - # SYNOPSIS HTTP GET with retry: @@ -97,387 +98,6 @@ slightly similar as this package, don't have 'simple' `Retry` method * [matryer/try](https://github.com/matryer/try) - very popular package, nonintuitive interface (for me) -# BREAKING CHANGES - -* 4.0.0 - - - infinity retry is possible by set `Attempts(0)` by PR [#49](https://github.com/avast/retry-go/pull/49) (original project) - -* 3.0.0 - - - `DelayTypeFunc` accepts a new parameter `err` - this breaking change affects only your custom Delay Functions. This change allow [make delay functions based on error](examples/delay_based_on_error_test.go). - -* 1.0.2 -> 2.0.0 - - - argument of `retry.Delay` is final delay (no multiplication by `retry.Units` anymore) - - function `retry.Units` are removed - - [more about this breaking change](https://github.com/avast/retry-go/issues/7) (original project) - -* 0.3.0 -> 1.0.0 - - - `retry.Retry` function are changed to `retry.Do` function - - `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are now implement via functions produces Options (aka `retry.OnRetry`) - -## Usage - -#### func BackOffDelay - -```go -func BackOffDelay(n uint, _ error, config *Config) time.Duration -``` -BackOffDelay is a DelayType which increases delay between consecutive retries - -#### func Do - -```go -func Do(retryableFunc RetryableFunc, opts ...Option) error -``` - -#### func DoWithData - -```go -func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error) -``` - -#### func FixedDelay - -```go -func FixedDelay(_ uint, _ error, config *Config) time.Duration -``` -FixedDelay is a DelayType which keeps delay the same through all iterations - -#### func IsRecoverable - -```go -func IsRecoverable(err error) bool -``` -IsRecoverable checks if error is an instance of `unrecoverableError` - -#### func RandomDelay - -```go -func RandomDelay(_ uint, _ error, config *Config) time.Duration -``` -RandomDelay is a DelayType which picks a random delay up to config.maxJitter - -#### func Unrecoverable - -```go -func Unrecoverable(err error) error -``` -Unrecoverable wraps an error in `unrecoverableError` struct - -#### type Config - -```go -type Config struct { -} -``` - - -#### type DelayTypeFunc - -```go -type DelayTypeFunc func(n uint, err error, config *Config) time.Duration -``` - -DelayTypeFunc is called to return the next delay to wait after the retriable -function fails on `err` after `n` attempts. - -#### func CombineDelay - -```go -func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc -``` -CombineDelay is a DelayType the combines all of the specified delays into a new -DelayTypeFunc - -#### type Error - -```go -type Error []error -``` - -Error type represents list of errors in retry - -#### func (Error) As - -```go -func (e Error) As(target interface{}) bool -``` - -#### func (Error) Error - -```go -func (e Error) Error() string -``` -Error method return string representation of Error It is an implementation of -error interface - -#### func (Error) Is - -```go -func (e Error) Is(target error) bool -``` - -#### func (Error) Unwrap - -```go -func (e Error) Unwrap() error -``` -Unwrap the last error for compatibility with `errors.Unwrap()`. When you need to -unwrap all errors, you should use `WrappedErrors()` instead. - - err := Do( - func() error { - return errors.New("original error") - }, - Attempts(1), - ) - - fmt.Println(errors.Unwrap(err)) # "original error" is printed - -Added in version 4.2.0. - -#### func (Error) WrappedErrors - -```go -func (e Error) WrappedErrors() []error -``` -WrappedErrors returns the list of errors that this Error is wrapping. It is an -implementation of the `errwrap.Wrapper` interface in package -[errwrap](https://github.com/hashicorp/errwrap) so that `retry.Error` can be -used with that library. - -#### type OnRetryFunc - -```go -type OnRetryFunc func(attempt uint, err error) -``` - -Function signature of OnRetry function - -#### type Option - -```go -type Option func(*Config) -``` - -Option represents an option for retry. - -#### func Attempts - -```go -func Attempts(attempts uint) Option -``` -Attempts set count of retry. Setting to 0 will retry until the retried function -succeeds. default is 10 - -#### func AttemptsForError - -```go -func AttemptsForError(attempts uint, err error) Option -``` -AttemptsForError sets count of retry in case execution results in given `err` -Retries for the given `err` are also counted against total retries. The retry -will stop if any of given retries is exhausted. - -added in 4.3.0 - -#### func Context - -```go -func Context(ctx context.Context) Option -``` -Context allow to set context of retry default are Background context - -example of immediately cancellation (maybe it isn't the best example, but it -describes behavior enough; I hope) - - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - retry.Do( - func() error { - ... - }, - retry.Context(ctx), - ) - -#### func Delay - -```go -func Delay(delay time.Duration) Option -``` -Delay set delay between retry default is 100ms - -#### func DelayType - -```go -func DelayType(delayType DelayTypeFunc) Option -``` -DelayType set type of the delay between retries default is BackOff - -#### func LastErrorOnly - -```go -func LastErrorOnly(lastErrorOnly bool) Option -``` -return the direct last error that came from the retried function default is -false (return wrapped errors with everything) - -#### func MaxDelay - -```go -func MaxDelay(maxDelay time.Duration) Option -``` -MaxDelay set maximum delay between retry does not apply by default - -#### func MaxJitter - -```go -func MaxJitter(maxJitter time.Duration) Option -``` -MaxJitter sets the maximum random Jitter between retries for RandomDelay - -#### func OnRetry - -```go -func OnRetry(onRetry OnRetryFunc) Option -``` -OnRetry function callback are called each retry - -log each retry example: - - retry.Do( - func() error { - return errors.New("some error") - }, - retry.OnRetry(func(n uint, err error) { - log.Printf("#%d: %s\n", n, err) - }), - ) - -#### func RetryIf - -```go -func RetryIf(retryIf RetryIfFunc) Option -``` -RetryIf controls whether a retry should be attempted after an error (assuming -there are any retry attempts remaining) - -skip retry if special error example: - - retry.Do( - func() error { - return errors.New("special error") - }, - retry.RetryIf(func(err error) bool { - if err.Error() == "special error" { - return false - } - return true - }) - ) - -By default RetryIf stops execution if the error is wrapped using -`retry.Unrecoverable`, so above example may also be shortened to: - - retry.Do( - func() error { - return retry.Unrecoverable(errors.New("special error")) - } - ) - -#### func UntilSucceeded - -```go -func UntilSucceeded() Option -``` -UntilSucceeded will retry until the retried function succeeds. Equivalent to -setting Attempts(0). - -#### func WithTimer - -```go -func WithTimer(t Timer) Option -``` -WithTimer provides a way to swap out timer module implementations. This -primarily is useful for mocking/testing, where you may not want to explicitly -wait for a set duration for retries. - -example of augmenting time.After with a print statement - - type struct MyTimer {} - - func (t *MyTimer) After(d time.Duration) <- chan time.Time { - fmt.Print("Timer called!") - return time.After(d) - } - - retry.Do( - func() error { ... }, - retry.WithTimer(&MyTimer{}) - ) - -#### func WrapContextErrorWithLastError - -```go -func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option -``` -WrapContextErrorWithLastError allows the context error to be returned wrapped -with the last error that the retried function returned. This is only applicable -when Attempts is set to 0 to retry indefinitly and when using a context to -cancel / timeout - -default is false - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - retry.Do( - func() error { - ... - }, - retry.Context(ctx), - retry.Attempts(0), - retry.WrapContextErrorWithLastError(true), - ) - -#### type RetryIfFunc - -```go -type RetryIfFunc func(error) bool -``` - -Function signature of retry if function - -#### type RetryableFunc - -```go -type RetryableFunc func() error -``` - -Function signature of retryable function - -#### type RetryableFuncWithData - -```go -type RetryableFuncWithData[T any] func() (T, error) -``` - -Function signature of retryable function with data - -#### type Timer - -```go -type Timer interface { - After(time.Duration) <-chan time.Time -} -``` - -Timer represents the timer used to track time for a retry. - ## Contributing Contributions are very much welcome. @@ -496,9 +116,3 @@ please try: * run tests (`make test`) * run linter (`make lint`) * if your IDE don't automaticaly do `go fmt`, run `go fmt` (`make fmt`) - -### README - -README.md are generate from template [.godocdown.tmpl](.godocdown.tmpl) and code documentation via [godocdown](https://github.com/robertkrimen/godocdown). - -Never edit README.md direct, because your change will be lost. diff --git a/current.txt b/current.txt deleted file mode 100644 index 406b14f..0000000 --- a/current.txt +++ /dev/null @@ -1,26 +0,0 @@ -goos: darwin -goarch: amd64 -pkg: github.com/avast/retry-go/v4 -cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz -BenchmarkDo-16 3 474128987 ns/op 2730 B/op 48 allocs/op -BenchmarkDo-16 3 441499631 ns/op 2725 B/op 47 allocs/op -BenchmarkDo-16 3 449390845 ns/op 2693 B/op 47 allocs/op -BenchmarkDo-16 3 488695333 ns/op 2725 B/op 47 allocs/op -BenchmarkDo-16 2 601685067 ns/op 2704 B/op 48 allocs/op -BenchmarkDo-16 3 336872997 ns/op 2693 B/op 47 allocs/op -BenchmarkDo-16 3 384347911 ns/op 2725 B/op 47 allocs/op -BenchmarkDo-16 3 480906307 ns/op 2693 B/op 47 allocs/op -BenchmarkDo-16 3 455362447 ns/op 2693 B/op 47 allocs/op -BenchmarkDo-16 3 443170384 ns/op 2693 B/op 47 allocs/op -BenchmarkDoNoErrors-16 6872852 159.4 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7650360 161.3 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7235683 159.3 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7465636 160.2 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7549692 160.7 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7510610 159.8 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7438124 160.3 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7416504 160.2 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7356183 160.4 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7393480 160.1 ns/op 208 B/op 4 allocs/op -PASS -ok github.com/avast/retry-go/v4 35.971s diff --git a/examples/custom_retry_function_test.go b/examples/custom_retry_function_test.go index 5f3a236..e429aa2 100644 --- a/examples/custom_retry_function_test.go +++ b/examples/custom_retry_function_test.go @@ -9,9 +9,7 @@ import ( "testing" "time" - "github.com/codeGROOVE-dev/retry-go" - "github.com/stretchr/testify/assert" -) + "github.com/codeGROOVE-dev/retry-go") // RetriableError is a custom error that contains a positive duration for the next retry type RetriableError struct { @@ -91,7 +89,10 @@ func TestCustomRetryFunction(t *testing.T) { ) fmt.Println("Server responds with: " + string(body)) - - assert.NoError(t, err) - assert.Equal(t, "hello", string(body)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(body) != "hello" { + t.Errorf("expected %v, got %v", "hello", string(body)) + } } diff --git a/examples/delay_based_on_error_test.go b/examples/delay_based_on_error_test.go index e35bad1..1c9c159 100644 --- a/examples/delay_based_on_error_test.go +++ b/examples/delay_based_on_error_test.go @@ -10,9 +10,7 @@ import ( "testing" "time" - "github.com/codeGROOVE-dev/retry-go" - "github.com/stretchr/testify/assert" -) + "github.com/codeGROOVE-dev/retry-go") type RetryAfterError struct { response http.Response @@ -74,8 +72,12 @@ func TestCustomRetryFunctionBasedOnKindOfError(t *testing.T) { }), ) - assert.NoError(t, err) - assert.NotEmpty(t, body) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(body) == 0 { + t.Error("expected non-empty value") + } } // use https://github.com/aereal/go-httpretryafter instead diff --git a/examples/errors_history_test.go b/examples/errors_history_test.go index 355a283..4fdba3b 100644 --- a/examples/errors_history_test.go +++ b/examples/errors_history_test.go @@ -6,9 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/codeGROOVE-dev/retry-go" - "github.com/stretchr/testify/assert" -) + "github.com/codeGROOVE-dev/retry-go") // TestErrorHistory shows an example of how to get all the previous errors when // retry.Do ends in success @@ -40,6 +38,10 @@ func TestErrorHistory(t *testing.T) { allErrors = append(allErrors, err) }), ) - assert.NoError(t, err) - assert.Len(t, allErrors, 3) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(allErrors) != 3 { + t.Errorf("expected len(allErrors) = 3, got %d", len(allErrors)) + } } diff --git a/examples/http_get_test.go b/examples/http_get_test.go index a06221d..b2d76b9 100644 --- a/examples/http_get_test.go +++ b/examples/http_get_test.go @@ -7,9 +7,7 @@ import ( "net/http/httptest" "testing" - "github.com/codeGROOVE-dev/retry-go" - "github.com/stretchr/testify/assert" -) + "github.com/codeGROOVE-dev/retry-go") func TestGet(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -36,6 +34,10 @@ func TestGet(t *testing.T) { }, ) - assert.NoError(t, err) - assert.NotEmpty(t, body) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(body) == 0 { + t.Error("expected non-empty value") + } } diff --git a/generic.txt b/generic.txt deleted file mode 100644 index 116a096..0000000 --- a/generic.txt +++ /dev/null @@ -1,46 +0,0 @@ -goos: darwin -goarch: amd64 -pkg: github.com/avast/retry-go/v4 -cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz -BenchmarkDo-16 3 406306609 ns/op 2701 B/op 48 allocs/op -BenchmarkDo-16 3 419470846 ns/op 2693 B/op 47 allocs/op -BenchmarkDo-16 2 567716303 ns/op 2696 B/op 47 allocs/op -BenchmarkDo-16 2 562713288 ns/op 2696 B/op 47 allocs/op -BenchmarkDo-16 3 418301987 ns/op 2693 B/op 47 allocs/op -BenchmarkDo-16 2 541207332 ns/op 2696 B/op 47 allocs/op -BenchmarkDo-16 2 526211617 ns/op 2696 B/op 47 allocs/op -BenchmarkDo-16 2 517419526 ns/op 2696 B/op 47 allocs/op -BenchmarkDo-16 3 478391497 ns/op 2693 B/op 47 allocs/op -BenchmarkDo-16 3 452548175 ns/op 2725 B/op 47 allocs/op -BenchmarkDoWithData-16 3 463040866 ns/op 2693 B/op 47 allocs/op -BenchmarkDoWithData-16 3 496158943 ns/op 2693 B/op 47 allocs/op -BenchmarkDoWithData-16 3 488367012 ns/op 2725 B/op 47 allocs/op -BenchmarkDoWithData-16 3 454618897 ns/op 2693 B/op 47 allocs/op -BenchmarkDoWithData-16 3 435430056 ns/op 2693 B/op 47 allocs/op -BenchmarkDoWithData-16 2 552289967 ns/op 2744 B/op 48 allocs/op -BenchmarkDoWithData-16 3 569748815 ns/op 2693 B/op 47 allocs/op -BenchmarkDoWithData-16 3 416597207 ns/op 2725 B/op 47 allocs/op -BenchmarkDoWithData-16 3 358455415 ns/op 2725 B/op 47 allocs/op -BenchmarkDoWithData-16 3 455297803 ns/op 2725 B/op 47 allocs/op -BenchmarkDoNoErrors-16 7035135 161.9 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7389806 161.3 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7394016 161.5 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7380039 162.2 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7424865 162.2 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7111860 160.5 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7285305 162.6 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7410627 160.7 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7340961 161.6 ns/op 208 B/op 4 allocs/op -BenchmarkDoNoErrors-16 7295727 164.1 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 7357304 159.9 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 6649852 166.9 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 6938404 176.3 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 7181965 160.4 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 7311484 166.2 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 6939157 169.7 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 6648344 179.0 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 6794847 177.0 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 6782588 171.4 ns/op 208 B/op 4 allocs/op -BenchmarkDoWithDataNoErrors-16 7279119 166.9 ns/op 208 B/op 4 allocs/op -PASS -ok github.com/avast/retry-go/v4 73.128s diff --git a/go.mod b/go.mod index bedf232..6a2655d 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,3 @@ module github.com/codeGROOVE-dev/retry-go go 1.20 - -require github.com/stretchr/testify v1.10.0 - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum index 713a0b4..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/retry_test.go b/retry_test.go index 1d6e45e..ea2c6ab 100644 --- a/retry_test.go +++ b/retry_test.go @@ -6,10 +6,9 @@ import ( "fmt" "math" "os" + "reflect" "testing" "time" - - "github.com/stretchr/testify/assert" ) func TestDoWithDataAllFailed(t *testing.T) { @@ -19,8 +18,12 @@ func TestDoWithDataAllFailed(t *testing.T) { OnRetry(func(n uint, err error) { retrySum += n }), Delay(time.Nanosecond), ) - assert.Error(t, err) - assert.Equal(t, 0, v) + if err == nil { + t.Fatal("expected error, got nil") + } + if v != 0 { + t.Errorf("got v=%d, want 0", v) + } expectedErrorFormat := `All attempts fail: #1: test @@ -33,10 +36,20 @@ func TestDoWithDataAllFailed(t *testing.T) { #8: test #9: test #10: test` - assert.Len(t, err, 10) + if retryErr, ok := err.(Error); ok { + if len(retryErr) != 10 { + t.Errorf("error count: got %d, want 10", len(retryErr)) + } + } else { + t.Fatalf("expected Error type, got %T", err) + } fmt.Println(err.Error()) - assert.Equal(t, expectedErrorFormat, err.Error(), "retry error format") - assert.Equal(t, uint(36), retrySum, "right count of retry") + if err.Error() != expectedErrorFormat { + t.Errorf("error message: got %q, want %q", err.Error(), expectedErrorFormat) + } + if retrySum != uint(36) { + t.Errorf("retry sum: got %d, want 36", retrySum) + } } func TestDoFirstOk(t *testing.T) { @@ -45,8 +58,12 @@ func TestDoFirstOk(t *testing.T) { func() error { return nil }, OnRetry(func(n uint, err error) { retrySum += n }), ) - assert.NoError(t, err) - assert.Equal(t, uint(0), retrySum, "no retry") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if uint(0) != retrySum { + t.Errorf("retrySum (expected no retries): got %d, want 0", retrySum) + } } func TestDoWithDataFirstOk(t *testing.T) { @@ -57,9 +74,15 @@ func TestDoWithDataFirstOk(t *testing.T) { func() (int, error) { return returnVal, nil }, OnRetry(func(n uint, err error) { retrySum += n }), ) - assert.NoError(t, err) - assert.Equal(t, returnVal, val) - assert.Equal(t, uint(0), retrySum, "no retry") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if returnVal != val { + t.Errorf("return value: got %d, want %d", val, returnVal) + } + if uint(0) != retrySum { + t.Errorf("retrySum (expected no retries): got %d, want 0", retrySum) + } } func TestRetryIf(t *testing.T) { @@ -78,15 +101,27 @@ func TestRetryIf(t *testing.T) { }), Delay(time.Nanosecond), ) - assert.Error(t, err) + if err == nil { + t.Fatal("expected error, got nil") + } expectedErrorFormat := `All attempts fail: #1: test #2: test #3: special` - assert.Len(t, err, 3) - assert.Equal(t, expectedErrorFormat, err.Error(), "retry error format") - assert.Equal(t, uint(2), retryCount, "right count of retry") + if retryErr, ok := err.(Error); ok { + if len(retryErr) != 3 { + t.Errorf("error count: got %d, want 3", len(retryErr)) + } + } else { + t.Fatalf("expected Error type, got %T", err) + } + if err.Error() != expectedErrorFormat { + t.Errorf("error message: got %q, want %q", err.Error(), expectedErrorFormat) + } + if uint(2) != retryCount { + t.Errorf("retry count: got %d, want 2", retryCount) + } } func TestRetryIf_ZeroAttempts(t *testing.T) { @@ -108,10 +143,16 @@ func TestRetryIf_ZeroAttempts(t *testing.T) { LastErrorOnly(true), Attempts(0), ) - assert.Error(t, err) + if err == nil { + t.Fatal("expected error, got nil") + } - assert.Equal(t, "special", err.Error(), "retry error format") - assert.Equal(t, retryCount, onRetryCount+1, "right count of retry") + if "special" != err.Error() { + t.Errorf("error message: got %q, want %q", err.Error(), "special") + } + if retryCount != onRetryCount+1 { + t.Errorf("retry count vs onRetry count: got retryCount=%d, onRetryCount=%d, want retryCount=onRetryCount+1", retryCount, onRetryCount) + } } func TestZeroAttemptsWithError(t *testing.T) { @@ -130,9 +171,13 @@ func TestZeroAttemptsWithError(t *testing.T) { Attempts(0), MaxDelay(time.Nanosecond), ) - assert.NoError(t, err) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - assert.Equal(t, count, maxErrors) + if maxErrors != count { + t.Errorf("execution count: got %d, want %d", count, maxErrors) + } } func TestZeroAttemptsWithoutError(t *testing.T) { @@ -146,21 +191,30 @@ func TestZeroAttemptsWithoutError(t *testing.T) { }, Attempts(0), ) - assert.NoError(t, err) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - assert.Equal(t, count, 1) + if 1 != count { + t.Errorf("execution count: got %d, want 1", count) + } } func TestZeroAttemptsWithUnrecoverableError(t *testing.T) { err := Do( func() error { - return Unrecoverable(assert.AnError) + return Unrecoverable(errors.New("test error")) }, Attempts(0), MaxDelay(time.Nanosecond), ) - assert.Error(t, err) - assert.Equal(t, Unrecoverable(assert.AnError), err) + if err == nil { + t.Fatal("expected error, got nil") + } + expectedErr := Unrecoverable(errors.New("test error")) + if err.Error() != expectedErr.Error() { + t.Errorf("got %v, want %v", err, expectedErr) + } } func TestAttemptsForError(t *testing.T) { @@ -175,8 +229,12 @@ func TestAttemptsForError(t *testing.T) { AttemptsForError(attemptsForTestError, testErr), Attempts(5), ) - assert.Error(t, err) - assert.Equal(t, attemptsForTestError, count) + if err == nil { + t.Fatal("expected error, got nil") + } + if count != attemptsForTestError { + t.Errorf("attempt count: got %d, want %d", count, attemptsForTestError) + } } func TestDefaultSleep(t *testing.T) { @@ -186,8 +244,12 @@ func TestDefaultSleep(t *testing.T) { Attempts(3), ) dur := time.Since(start) - assert.Error(t, err) - assert.Greater(t, dur, 300*time.Millisecond, "3 times default retry is longer then 300ms") + if err == nil { + t.Fatal("expected error, got nil") + } + if dur <= 300*time.Millisecond { + t.Errorf("retry duration too short: got %v, want >300ms (3 retries with default delay)", dur) + } } func TestFixedSleep(t *testing.T) { @@ -198,8 +260,12 @@ func TestFixedSleep(t *testing.T) { DelayType(FixedDelay), ) dur := time.Since(start) - assert.Error(t, err) - assert.Less(t, dur, 500*time.Millisecond, "3 times default retry is shorter then 500ms") + if err == nil { + t.Fatal("expected error, got nil") + } + if dur >= 500*time.Millisecond { + t.Errorf("retry duration too long: got %v, want <500ms (3 retries with fixed delay)", dur) + } } func TestLastErrorOnly(t *testing.T) { @@ -210,8 +276,12 @@ func TestLastErrorOnly(t *testing.T) { Delay(time.Nanosecond), LastErrorOnly(true), ) - assert.Error(t, err) - assert.Equal(t, "9", err.Error()) + if err == nil { + t.Fatal("expected error, got nil") + } + if err.Error() != "9" { + t.Errorf("error message: got %q, want %q", err.Error(), "9") + } } func TestUnrecoverableError(t *testing.T) { @@ -224,9 +294,16 @@ func TestUnrecoverableError(t *testing.T) { }, Attempts(2), ) - assert.Error(t, err) - assert.Equal(t, Unrecoverable(testErr), err) - assert.Equal(t, 1, attempts, "unrecoverable error broke the loop") + if err == nil { + t.Fatal("expected error, got nil") + } + expectedErr := Unrecoverable(testErr) + if err.Error() != expectedErr.Error() { + t.Errorf("error: got %v, want %v", err, expectedErr) + } + if attempts != 1 { + t.Errorf("attempts with unrecoverable error: got %d, want 1", attempts) + } } func TestCombineFixedDelays(t *testing.T) { @@ -241,9 +318,15 @@ func TestCombineFixedDelays(t *testing.T) { DelayType(CombineDelay(FixedDelay, FixedDelay)), ) dur := time.Since(start) - assert.Error(t, err) - assert.Greater(t, dur, 400*time.Millisecond, "3 times combined, fixed retry is greater then 400ms") - assert.Less(t, dur, 500*time.Millisecond, "3 times combined, fixed retry is less then 500ms") + if err == nil { + t.Fatal("expected error, got nil") + } + if dur <= 400*time.Millisecond { + t.Errorf("combined delay duration too short: got %v, want >400ms", dur) + } + if dur >= 500*time.Millisecond { + t.Errorf("combined delay duration too long: got %v, want <500ms", dur) + } } func TestRandomDelay(t *testing.T) { @@ -259,9 +342,15 @@ func TestRandomDelay(t *testing.T) { MaxJitter(50*time.Millisecond), ) dur := time.Since(start) - assert.Error(t, err) - assert.Greater(t, dur, 2*time.Millisecond, "3 times random retry is longer then 2ms") - assert.Less(t, dur, 150*time.Millisecond, "3 times random retry is shorter then 150ms") + if err == nil { + t.Fatal("expected error, got nil") + } + if dur <= 2*time.Millisecond { + t.Errorf("random delay duration too short: got %v, want >2ms", dur) + } + if dur >= 150*time.Millisecond { + t.Errorf("random delay duration too long: got %v, want <150ms", dur) + } } func TestMaxDelay(t *testing.T) { @@ -277,9 +366,15 @@ func TestMaxDelay(t *testing.T) { MaxDelay(50*time.Millisecond), ) dur := time.Since(start) - assert.Error(t, err) - assert.Greater(t, dur, 120*time.Millisecond, "5 times with maximum delay retry is less than 120ms") - assert.Less(t, dur, 275*time.Millisecond, "5 times with maximum delay retry is longer than 275ms") + if err == nil { + t.Fatal("expected error, got nil") + } + if dur <= 120*time.Millisecond { + t.Errorf("max delay duration too short: got %v, want >120ms (5 retries with max delay)", dur) + } + if dur >= 275*time.Millisecond { + t.Errorf("max delay duration too long: got %v, want <275ms (5 retries with max delay)", dur) + } } func TestBackOffDelay(t *testing.T) { @@ -326,8 +421,12 @@ func TestBackOffDelay(t *testing.T) { delay: c.delay, } delay := BackOffDelay(c.n, nil, &config) - assert.Equal(t, c.expectedMaxN, config.maxBackOffN, "max n mismatch") - assert.Equal(t, c.expectedDelay, delay, "delay duration mismatch") + if c.expectedMaxN != config.maxBackOffN { + t.Errorf("max n mismatch: got %v, want %v", config.maxBackOffN, c.expectedMaxN) + } + if c.expectedDelay != delay { + t.Errorf("delay duration mismatch: got %v, want %v", delay, c.expectedDelay) + } }, ) } @@ -381,7 +480,9 @@ func TestCombineDelay(t *testing.T) { funcs[i] = f(d) } actual := CombineDelay(funcs...)(0, nil, nil) - assert.Equal(t, c.expected, actual, "delay duration mismatch") + if c.expected != actual { + t.Errorf("delay duration mismatch: got %v, want %v", actual, c.expected) + } }, ) } @@ -401,9 +502,15 @@ func TestContext(t *testing.T) { Context(ctx), ) dur := time.Since(start) - assert.Error(t, err) - assert.True(t, dur < defaultDelay, "immediately cancellation") - assert.Equal(t, 0, retrySum, "called at most once") + if err == nil { + t.Fatal("expected error, got nil") + } + if !(dur < defaultDelay) { + t.Errorf("cancellation timing: got duration=%v, want = 50*time.Millisecond { + t.Errorf("first attempt timing: got %v, want <50ms (should be immediate)", firstAttemptTime) + } // Second attempt should be after delay - assert.Greater(t, secondAttemptTime, 150*time.Millisecond, "second attempt should be after delay") - assert.Less(t, secondAttemptTime, 250*time.Millisecond, "second attempt should not be too delayed") + if secondAttemptTime <= 150*time.Millisecond { + t.Errorf("second attempt timing: got %v, want >150ms (should be after 200ms delay)", secondAttemptTime) + } + if secondAttemptTime >= 250*time.Millisecond { + t.Errorf("second attempt timing: got %v, want <250ms", secondAttemptTime) + } // Total time should not include delay after final attempt - assert.Less(t, totalTime, 300*time.Millisecond, "should not delay after final attempt") + if totalTime >= 300*time.Millisecond { + t.Errorf("total duration: got %v, want <300ms (no delay after final attempt)", totalTime) + } } func TestOnRetryNotCalledOnLastAttempt(t *testing.T) { @@ -703,21 +870,35 @@ func TestOnRetryNotCalledOnLastAttempt(t *testing.T) { Delay(time.Nanosecond), ) - assert.Error(t, err) - assert.Equal(t, 3, callCount, "function should be called 3 times") - assert.Equal(t, []uint{0, 1}, onRetryCalls, "onRetry should only be called for first 2 attempts, not the final one") - assert.Len(t, onRetryCalls, 2, "onRetry should be called exactly 2 times (not on last attempt)") + if err == nil { + t.Fatal("expected error, got nil") + } + if callCount != 3 { + t.Errorf("function call count: got %d, want 3", callCount) + } + if !reflect.DeepEqual(onRetryCalls, []uint{0, 1}) { + t.Errorf("onRetry calls: got %v, want %v (should not be called on final attempt)", onRetryCalls, []uint{0, 1}) + } + if len(onRetryCalls) != 2 { + t.Errorf("onRetry call count: got %d, want 2 (not called on last attempt)", len(onRetryCalls)) + } } func TestIsRecoverable(t *testing.T) { err := errors.New("err") - assert.True(t, IsRecoverable(err)) + if !IsRecoverable(err) { + t.Error("IsRecoverable(err) = false, want true") + } err = Unrecoverable(err) - assert.False(t, IsRecoverable(err)) + if IsRecoverable(err) { + t.Error("IsRecoverable(err) = true, want false") + } err = fmt.Errorf("wrapping: %w", err) - assert.False(t, IsRecoverable(err)) + if IsRecoverable(err) { + t.Error("IsRecoverable(err) = true, want false") + } } func TestFullJitterBackoffDelay(t *testing.T) { @@ -748,9 +929,12 @@ func TestFullJitterBackoffDelay(t *testing.T) { expectedMaxCeiling = float64(maxDelay) } - assert.True(t, delay >= 0, "Delay should be non-negative. Got: %v for attempt %d", delay, n) - assert.True(t, delay <= time.Duration(expectedMaxCeiling), - "Delay %v should be less than or equal to current backoff ceiling %v for attempt %d", delay, time.Duration(expectedMaxCeiling), n) + if delay < 0 { + t.Errorf("Delay should be non-negative. Got: %v for attempt %d", delay, n) + } + if delay > time.Duration(expectedMaxCeiling) { + t.Errorf("Delay %v should be less than or equal to current backoff ceiling %v for attempt %d", delay, time.Duration(expectedMaxCeiling), n) + } t.Logf("Attempt %d: BaseDelay=%v, MaxDelay=%v, Calculated Ceiling=~%v, Actual Delay=%v", n, baseDelay, maxDelay, time.Duration(expectedMaxCeiling), delay) @@ -762,18 +946,25 @@ func TestFullJitterBackoffDelay(t *testing.T) { if expectedCeilingNoMax > float64(10*time.Minute) { // Avoid overflow for very large N expectedCeilingNoMax = float64(10 * time.Minute) } - assert.True(t, delayNoMax >= 0, "Delay (no max) should be non-negative. Got: %v for attempt %d", delayNoMax, n) - assert.True(t, delayNoMax <= time.Duration(expectedCeilingNoMax), - "Delay (no max) %v should be less than or equal to current backoff ceiling %v for attempt %d", delayNoMax, time.Duration(expectedCeilingNoMax), n) + if delayNoMax < 0 { + t.Errorf("Delay (no max) should be non-negative. Got: %v for attempt %d", delayNoMax, n) + } + if delayNoMax > time.Duration(expectedCeilingNoMax) { + t.Errorf("Delay (no max) %v should be less than or equal to current backoff ceiling %v for attempt %d", delayNoMax, time.Duration(expectedCeilingNoMax), n) + } } // Test case where baseDelay might be zero configZeroBase := &Config{delay: 0, maxDelay: maxDelay} delayZeroBase := FullJitterBackoffDelay(0, errors.New("test error"), configZeroBase) - assert.Equal(t, time.Duration(0), delayZeroBase, "Delay with zero base delay should be 0") + if time.Duration(0) != delayZeroBase { + t.Errorf("delay with zero base: got %v, want 0", delayZeroBase) + } delayZeroBaseAttempt1 := FullJitterBackoffDelay(1, errors.New("test error"), configZeroBase) - assert.Equal(t, time.Duration(0), delayZeroBaseAttempt1, "Delay with zero base delay (attempt > 0) should be 0") + if time.Duration(0) != delayZeroBaseAttempt1 { + t.Errorf("delay with zero base (attempt>0): got %v, want 0", delayZeroBaseAttempt1) + } // Test with very small base delay smallBaseDelay := 1 * time.Nanosecond @@ -784,6 +975,8 @@ func TestFullJitterBackoffDelay(t *testing.T) { if ceil > 100 { ceil = 100 } - assert.True(t, d <= time.Duration(ceil)) + if d > time.Duration(ceil) { + t.Errorf("delay ceiling: got %v, want <=%v", d, time.Duration(ceil)) + } } }