From 38e7dcb180d7a459039b2b5b220d6810a93aad55 Mon Sep 17 00:00:00 2001 From: Bracken Dawson Date: Fri, 4 Oct 2024 23:34:21 +0100 Subject: [PATCH 1/3] Replace Never & Eventually functions with synchronous versions --- assert/assertion_format.go | 62 +++++++++++ assert/assertion_forward.go | 124 +++++++++++++++++++++ assert/assertions.go | 122 +++++++++++++++++++++ assert/assertions_test.go | 208 ++++++++++++++++++++++++++++++++++++ require/require.go | 136 +++++++++++++++++++++++ require/require_forward.go | 124 +++++++++++++++++++++ 6 files changed, 776 insertions(+) diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 190634165..2e83bbe8d 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -16,6 +16,30 @@ func Conditionf(t TestingT, comp Comparison, msg string, args ...interface{}) bo return Condition(t, comp, append([]interface{}{msg}, args...)...) } +// Consistentlyf asserts that a given condition will always be met until +// waitFor time plus the time taken for the last call to condition to return has +// elapsed. The condition is considered met when the condition function does not +// report failures on the CollectT passed to it. The supplied CollectT collects +// all errors from one tick (if there are any) and if the condition is ever not +// met, then the collected errors are copied to t. It is safe to use assertions +// from the require package in the condition function, these will immediately +// cease execution of the condition function, but not of the test. The condition +// function is called synchronously immediately and then every tick duration +// after that. Consistentlyf will adjust the time interval or drop ticks to make +// up for slow condition functions. +// +// assert.Consistentlyf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// i, err := shouldError() +// require.Error(c, err) +// require.Equal(c, 7, i) +// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") +func Consistentlyf(t TestingT, condition func(*CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Consistently(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) +} + // Containsf asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // @@ -162,6 +186,10 @@ func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface // periodically checking target function each tick. // // assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// +// Deprecated: For some values of waitFor and tick; Eventuallyf may fail having +// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfSync] +// instead. func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -169,6 +197,32 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick return Eventually(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) } +// EventuallySyncf asserts that a given condition will be met before waitFor time +// plus the time taken for the last call to condition to return. The condition +// is considered met when the condition function does not report failures on the +// CollectT passed to it. The supplied CollectT collects all errors from one +// tick (if there are any) and if the condition is not met before EventuallySyncf +// returns, then the collected errors of the last tick are copied to t. The +// condition function is called synchronously immediately and then every tick +// duration after that. EventuallySyncf will adjust the time interval or drop +// ticks to make up for slow condition functions. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// assert.EventuallySyncf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallySyncf(t TestingT, condition func(*CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return EventuallySync(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) +} + // EventuallyWithTf asserts that given condition will be met in waitFor time, // periodically checking target function each tick. In contrast to Eventually, // it supplies a CollectT to the condition function, so that the condition @@ -187,6 +241,10 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// +// Deprecated: For some values of waitFor and tick; EventuallyWithTf may fail +// having never called condition. EventuallyWithTf may leak goroutines. Use +// [EventuallySync] instead. func EventuallyWithTf(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -507,6 +565,10 @@ func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) bool // periodically checking the target function each tick. // // assert.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// +// Deprecated: For some values of waitFor and tick; Neverf may pass without +// calling condition. Neverf may leak goroutines. Use [Consistently] and invert +// the logic of condition instead. func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index 21629087b..4d6564327 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -24,6 +24,54 @@ func (a *Assertions) Conditionf(comp Comparison, msg string, args ...interface{} return Conditionf(a.t, comp, msg, args...) } +// Consistently asserts that a given condition will always be met until +// waitFor time plus the time taken for the last call to condition to return has +// elapsed. The condition is considered met when the condition function does not +// report failures on the CollectT passed to it. The supplied CollectT collects +// all errors from one tick (if there are any) and if the condition is ever not +// met, then the collected errors are copied to t. It is safe to use assertions +// from the require package in the condition function, these will immediately +// cease execution of the condition function, but not of the test. The condition +// function is called synchronously immediately and then every tick duration +// after that. Consistently will adjust the time interval or drop ticks to make +// up for slow condition functions. +// +// a.Consistently(func(c *assert.CollectT) { +// i, err := shouldError() +// require.Error(c, err) +// require.Equal(c, 7, i) +// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") +func (a *Assertions) Consistently(condition func(*CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Consistently(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// Consistentlyf asserts that a given condition will always be met until +// waitFor time plus the time taken for the last call to condition to return has +// elapsed. The condition is considered met when the condition function does not +// report failures on the CollectT passed to it. The supplied CollectT collects +// all errors from one tick (if there are any) and if the condition is ever not +// met, then the collected errors are copied to t. It is safe to use assertions +// from the require package in the condition function, these will immediately +// cease execution of the condition function, but not of the test. The condition +// function is called synchronously immediately and then every tick duration +// after that. Consistentlyf will adjust the time interval or drop ticks to make +// up for slow condition functions. +// +// a.Consistentlyf(func(c *assert.CollectT, "error message %s", "formatted") { +// i, err := shouldError() +// require.Error(c, err) +// require.Equal(c, 7, i) +// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") +func (a *Assertions) Consistentlyf(condition func(*CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Consistentlyf(a.t, condition, waitFor, tick, msg, args...) +} + // Contains asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // @@ -312,6 +360,10 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool { // periodically checking target function each tick. // // a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +// +// Deprecated: For some values of waitFor and tick; Eventually may fail having +// never called condition. Eventually may leak goroutines. Use [EventuallySync] +// instead. func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -319,6 +371,58 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti return Eventually(a.t, condition, waitFor, tick, msgAndArgs...) } +// EventuallySync asserts that a given condition will be met before waitFor time +// plus the time taken for the last call to condition to return. The condition +// is considered met when the condition function does not report failures on the +// CollectT passed to it. The supplied CollectT collects all errors from one +// tick (if there are any) and if the condition is not met before EventuallySync +// returns, then the collected errors of the last tick are copied to t. The +// condition function is called synchronously immediately and then every tick +// duration after that. EventuallySync will adjust the time interval or drop +// ticks to make up for slow condition functions. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallySync(func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallySync(condition func(*CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EventuallySync(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallySyncf asserts that a given condition will be met before waitFor time +// plus the time taken for the last call to condition to return. The condition +// is considered met when the condition function does not report failures on the +// CollectT passed to it. The supplied CollectT collects all errors from one +// tick (if there are any) and if the condition is not met before EventuallySyncf +// returns, then the collected errors of the last tick are copied to t. The +// condition function is called synchronously immediately and then every tick +// duration after that. EventuallySyncf will adjust the time interval or drop +// ticks to make up for slow condition functions. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallySyncf(func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallySyncf(condition func(*CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EventuallySyncf(a.t, condition, waitFor, tick, msg, args...) +} + // EventuallyWithT asserts that given condition will be met in waitFor time, // periodically checking target function each tick. In contrast to Eventually, // it supplies a CollectT to the condition function, so that the condition @@ -337,6 +441,10 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// +// Deprecated: For some values of waitFor and tick; EventuallyWithT may fail +// having never called condition. EventuallyWithT may leak goroutines. Use +// [EventuallySync] instead. func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -362,6 +470,10 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// +// Deprecated: For some values of waitFor and tick; EventuallyWithTf may fail +// having never called condition. EventuallyWithTf may leak goroutines. Use +// [EventuallySync] instead. func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -373,6 +485,10 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor // periodically checking target function each tick. // // a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// +// Deprecated: For some values of waitFor and tick; Eventuallyf may fail having +// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfSync] +// instead. func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1002,6 +1118,10 @@ func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) b // periodically checking the target function each tick. // // a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +// +// Deprecated: For some values of waitFor and tick; Never may pass without +// calling condition. Never may leak goroutines. Use [Consistently] and invert +// the logic of condition instead. func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1013,6 +1133,10 @@ func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick ti // periodically checking the target function each tick. // // a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// +// Deprecated: For some values of waitFor and tick; Neverf may pass without +// calling condition. Neverf may leak goroutines. Use [Consistently] and invert +// the logic of condition instead. func (a *Assertions) Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() diff --git a/assert/assertions.go b/assert/assertions.go index 44b854da6..420460730 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -1925,6 +1925,10 @@ type tHelper = interface { // periodically checking target function each tick. // // assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +// +// Deprecated: For some values of waitFor and tick; Eventually may fail having +// never called condition. Eventually may leak goroutines. Use [EventuallySync] +// instead. func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -2011,6 +2015,10 @@ func (c *CollectT) failed() bool { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// +// Deprecated: For some values of waitFor and tick; EventuallyWithT may fail +// having never called condition. EventuallyWithT may leak goroutines. Use +// [EventuallySync] instead. func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -2052,10 +2060,70 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time } } +// EventuallySync asserts that a given condition will be met before waitFor time +// plus the time taken for the last call to condition to return. The condition +// is considered met when the condition function does not report failures on the +// CollectT passed to it. The supplied CollectT collects all errors from one +// tick (if there are any) and if the condition is not met before EventuallySync +// returns, then the collected errors of the last tick are copied to t. The +// condition function is called synchronously immediately and then every tick +// duration after that. EventuallySync will adjust the time interval or drop +// ticks to make up for slow condition functions. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// assert.EventuallySync(t, func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallySync(t TestingT, condition func(*CollectT), waitFor, tick time.Duration, msgAndArgs ...interface{}) bool { + deadline := time.Now().Add(waitFor) + + tickerCh := make(chan time.Time) + close(tickerCh) + ticker := (<-chan time.Time)(tickerCh) + if tick > 0 { + timeTicker := time.NewTicker(tick) + defer timeTicker.Stop() + ticker = timeTicker.C + } + + for { + collect := new(CollectT) + + wait := make(chan struct{}) + go func() { + defer close(wait) + condition(collect) + }() + <-wait + + if !collect.failed() { + return true + } + + <-ticker + + if time.Now().After(deadline) { + for _, err := range collect.errors { + t.Errorf("%v", err) + } + return Fail(t, "Condition never satisfied", msgAndArgs...) + } + } +} + // Never asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // // assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +// +// Deprecated: For some values of waitFor and tick; Never may pass without +// calling condition. Never may leak goroutines. Use [Consistently] and invert +// the logic of condition instead. func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -2085,6 +2153,60 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D } } +// Consistently asserts that a given condition will always be met until +// waitFor time plus the time taken for the last call to condition to return has +// elapsed. The condition is considered met when the condition function does not +// report failures on the CollectT passed to it. The supplied CollectT collects +// all errors from one tick (if there are any) and if the condition is ever not +// met, then the collected errors are copied to t. It is safe to use assertions +// from the require package in the condition function, these will immediately +// cease execution of the condition function, but not of the test. The condition +// function is called synchronously immediately and then every tick duration +// after that. Consistently will adjust the time interval or drop ticks to make +// up for slow condition functions. +// +// assert.Consistently(t, func(c *assert.CollectT) { +// i, err := shouldError() +// require.Error(c, err) +// require.Equal(c, 7, i) +// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") +func Consistently(t TestingT, condition func(*CollectT), waitFor, tick time.Duration, msgAndArgs ...interface{}) bool { + deadline := time.Now().Add(waitFor) + + tickerCh := make(chan time.Time) + close(tickerCh) + ticker := (<-chan time.Time)(tickerCh) + if tick > 0 { + timeTicker := time.NewTicker(tick) + defer timeTicker.Stop() + ticker = timeTicker.C + } + + for { + collect := new(CollectT) + + wait := make(chan struct{}) + go func() { + defer close(wait) + condition(collect) + }() + <-wait + + if collect.failed() { + for _, err := range collect.errors { + t.Errorf("%v", err) + } + return Fail(t, "Condition was not satisfied", msgAndArgs...) + } + + <-ticker + + if time.Now().After(deadline) { + return true + } + } +} + // ErrorIs asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { diff --git a/assert/assertions_test.go b/assert/assertions_test.go index e158688f2..0b2696bb4 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -3030,6 +3030,214 @@ func TestEventuallyTimeout(t *testing.T) { }) } +func TestEventuallySync(t *testing.T) { + for name, test := range map[string]struct { + condition func(*CollectT) + waitFor, tick time.Duration + expected bool + expectedErrors []error + }{ + "zero_values": {func(t *CollectT) {}, 0, 0, true, nil}, + "fails": { + condition: func(t *CollectT) { t.Errorf("") }, + waitFor: 0, + tick: 0, + expected: false, + expectedErrors: []error{ + errors.New(""), + errors.New("\n\tError Trace:\t\n\tError: \tCondition never satisfied\n"), + }, + }, + "passes_2nd": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + if counter >= 2 { + return + } + t.Errorf("oops") + } + }(), + waitFor: time.Second, + tick: 0, + expected: true, + }, + "passes_22nd": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + if counter >= 22 { + return + } + t.Errorf("oops") + } + }(), + waitFor: time.Second, + tick: 0, + expected: true, + }, + "fails_3_times": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + t.Errorf("%d", counter) + } + }(), + waitFor: 100 * time.Millisecond, + tick: 35 * time.Millisecond, + expected: false, + expectedErrors: []error{ + errors.New("3"), + errors.New("\n\tError Trace:\t\n\tError: \tCondition never satisfied\n"), + }, + }, + "passes_after_fail_now": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + if counter >= 2 { + return + } + t.FailNow() + } + }(), + waitFor: time.Second, + tick: time.Millisecond, + expected: true, + }, + "fails_with_fail_now": { + condition: func(t *CollectT) { + t.Errorf("should be seen") + t.Errorf("should also be seen") + t.FailNow() + t.Errorf("should not be seen") + }, + waitFor: time.Millisecond, + tick: time.Millisecond, + expected: false, + expectedErrors: []error{ + errors.New("should be seen"), + errors.New("should also be seen"), + errors.New("\n\tError Trace:\t\n\tError: \tCondition never satisfied\n"), + }, + }, + } { + t.Run(name, func(t *testing.T) { + test := test + t.Parallel() + + mockT := new(errorsCapturingT) + Equal(t, test.expected, EventuallySync(mockT, test.condition, test.waitFor, test.tick)) + Equal(t, test.expectedErrors, mockT.errors) + }) + } +} + +func TestConsistently(t *testing.T) { + for name, test := range map[string]struct { + condition func(*CollectT) + waitFor, tick time.Duration + expected bool + expectedErrors []error + }{ + "zero_values": {func(*CollectT) {}, 0, 0, true, nil}, + "fails_immediately": { + condition: func(t *CollectT) { t.Errorf("") }, + waitFor: 0, + tick: 0, + expected: false, + expectedErrors: []error{ + errors.New(""), + errors.New("\n\tError Trace:\t\n\tError: \tCondition was not satisfied\n"), + }, + }, + "fails_2nd": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + if counter >= 2 { + t.Errorf("oops") + } + } + }(), + waitFor: time.Second, + tick: 0, + expected: false, + expectedErrors: []error{ + errors.New("oops"), + errors.New("\n\tError Trace:\t\n\tError: \tCondition was not satisfied\n"), + }, + }, + "fails_22nd": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + if counter >= 22 { + t.Errorf("oops") + } + } + }(), + waitFor: time.Second, + tick: 0, + expected: false, + expectedErrors: []error{ + errors.New("oops"), + errors.New("\n\tError Trace:\t\n\tError: \tCondition was not satisfied\n"), + }, + }, + "passes_2_times": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + if counter <= 2 { + return + } + t.Errorf("%d", counter) + } + }(), + waitFor: 100 * time.Millisecond, + tick: 35 * time.Millisecond, + expected: false, + expectedErrors: []error{ + errors.New("3"), + errors.New("\n\tError Trace:\t\n\tError: \tCondition was not satisfied\n"), + }, + }, + "fails_with_fail_now": { + condition: func(t *CollectT) { + t.Errorf("should be seen") + t.Errorf("should also be seen") + t.FailNow() + t.Errorf("should not be seen") + }, + waitFor: time.Millisecond, + tick: time.Millisecond, + expected: false, + expectedErrors: []error{ + errors.New("should be seen"), + errors.New("should also be seen"), + errors.New("\n\tError Trace:\t\n\tError: \tCondition was not satisfied\n"), + }, + }, + } { + t.Run(name, func(t *testing.T) { + test := test + t.Parallel() + + mockT := new(errorsCapturingT) + Equal(t, test.expected, Consistently(mockT, test.condition, test.waitFor, test.tick)) + Equal(t, test.expectedErrors, mockT.errors) + }) + } +} + func Test_validateEqualArgs(t *testing.T) { if validateEqualArgs(func() {}, func() {}) == nil { t.Error("non-nil functions should error") diff --git a/require/require.go b/require/require.go index 50ec19e13..432a1014e 100644 --- a/require/require.go +++ b/require/require.go @@ -31,6 +31,60 @@ func Conditionf(t TestingT, comp assert.Comparison, msg string, args ...interfac t.FailNow() } +// Consistently asserts that a given condition will always be met until +// waitFor time plus the time taken for the last call to condition to return has +// elapsed. The condition is considered met when the condition function does not +// report failures on the CollectT passed to it. The supplied CollectT collects +// all errors from one tick (if there are any) and if the condition is ever not +// met, then the collected errors are copied to t. It is safe to use assertions +// from the require package in the condition function, these will immediately +// cease execution of the condition function, but not of the test. The condition +// function is called synchronously immediately and then every tick duration +// after that. Consistently will adjust the time interval or drop ticks to make +// up for slow condition functions. +// +// assert.Consistently(t, func(c *assert.CollectT) { +// i, err := shouldError() +// require.Error(c, err) +// require.Equal(c, 7, i) +// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") +func Consistently(t TestingT, condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Consistently(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// Consistentlyf asserts that a given condition will always be met until +// waitFor time plus the time taken for the last call to condition to return has +// elapsed. The condition is considered met when the condition function does not +// report failures on the CollectT passed to it. The supplied CollectT collects +// all errors from one tick (if there are any) and if the condition is ever not +// met, then the collected errors are copied to t. It is safe to use assertions +// from the require package in the condition function, these will immediately +// cease execution of the condition function, but not of the test. The condition +// function is called synchronously immediately and then every tick duration +// after that. Consistentlyf will adjust the time interval or drop ticks to make +// up for slow condition functions. +// +// assert.Consistentlyf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// i, err := shouldError() +// require.Error(c, err) +// require.Equal(c, 7, i) +// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") +func Consistentlyf(t TestingT, condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Consistentlyf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + // Contains asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // @@ -391,6 +445,10 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) { // periodically checking target function each tick. // // assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +// +// Deprecated: For some values of waitFor and tick; Eventually may fail having +// never called condition. Eventually may leak goroutines. Use [EventuallySync] +// instead. func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -401,6 +459,64 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t t.FailNow() } +// EventuallySync asserts that a given condition will be met before waitFor time +// plus the time taken for the last call to condition to return. The condition +// is considered met when the condition function does not report failures on the +// CollectT passed to it. The supplied CollectT collects all errors from one +// tick (if there are any) and if the condition is not met before EventuallySync +// returns, then the collected errors of the last tick are copied to t. The +// condition function is called synchronously immediately and then every tick +// duration after that. EventuallySync will adjust the time interval or drop +// ticks to make up for slow condition functions. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// assert.EventuallySync(t, func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallySync(t TestingT, condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EventuallySync(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// EventuallySyncf asserts that a given condition will be met before waitFor time +// plus the time taken for the last call to condition to return. The condition +// is considered met when the condition function does not report failures on the +// CollectT passed to it. The supplied CollectT collects all errors from one +// tick (if there are any) and if the condition is not met before EventuallySyncf +// returns, then the collected errors of the last tick are copied to t. The +// condition function is called synchronously immediately and then every tick +// duration after that. EventuallySyncf will adjust the time interval or drop +// ticks to make up for slow condition functions. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// assert.EventuallySyncf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallySyncf(t TestingT, condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EventuallySyncf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + // EventuallyWithT asserts that given condition will be met in waitFor time, // periodically checking target function each tick. In contrast to Eventually, // it supplies a CollectT to the condition function, so that the condition @@ -419,6 +535,10 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// +// Deprecated: For some values of waitFor and tick; EventuallyWithT may fail +// having never called condition. EventuallyWithT may leak goroutines. Use +// [EventuallySync] instead. func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -447,6 +567,10 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// +// Deprecated: For some values of waitFor and tick; EventuallyWithTf may fail +// having never called condition. EventuallyWithTf may leak goroutines. Use +// [EventuallySync] instead. func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -461,6 +585,10 @@ func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), wait // periodically checking target function each tick. // // assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// +// Deprecated: For some values of waitFor and tick; Eventuallyf may fail having +// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfSync] +// instead. func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1267,6 +1395,10 @@ func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) { // periodically checking the target function each tick. // // assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +// +// Deprecated: For some values of waitFor and tick; Never may pass without +// calling condition. Never may leak goroutines. Use [Consistently] and invert +// the logic of condition instead. func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1281,6 +1413,10 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D // periodically checking the target function each tick. // // assert.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// +// Deprecated: For some values of waitFor and tick; Neverf may pass without +// calling condition. Neverf may leak goroutines. Use [Consistently] and invert +// the logic of condition instead. func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() diff --git a/require/require_forward.go b/require/require_forward.go index 1bd87304f..783d168a7 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -25,6 +25,54 @@ func (a *Assertions) Conditionf(comp assert.Comparison, msg string, args ...inte Conditionf(a.t, comp, msg, args...) } +// Consistently asserts that a given condition will always be met until +// waitFor time plus the time taken for the last call to condition to return has +// elapsed. The condition is considered met when the condition function does not +// report failures on the CollectT passed to it. The supplied CollectT collects +// all errors from one tick (if there are any) and if the condition is ever not +// met, then the collected errors are copied to t. It is safe to use assertions +// from the require package in the condition function, these will immediately +// cease execution of the condition function, but not of the test. The condition +// function is called synchronously immediately and then every tick duration +// after that. Consistently will adjust the time interval or drop ticks to make +// up for slow condition functions. +// +// a.Consistently(func(c *assert.CollectT) { +// i, err := shouldError() +// require.Error(c, err) +// require.Equal(c, 7, i) +// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") +func (a *Assertions) Consistently(condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Consistently(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// Consistentlyf asserts that a given condition will always be met until +// waitFor time plus the time taken for the last call to condition to return has +// elapsed. The condition is considered met when the condition function does not +// report failures on the CollectT passed to it. The supplied CollectT collects +// all errors from one tick (if there are any) and if the condition is ever not +// met, then the collected errors are copied to t. It is safe to use assertions +// from the require package in the condition function, these will immediately +// cease execution of the condition function, but not of the test. The condition +// function is called synchronously immediately and then every tick duration +// after that. Consistentlyf will adjust the time interval or drop ticks to make +// up for slow condition functions. +// +// a.Consistentlyf(func(c *assert.CollectT, "error message %s", "formatted") { +// i, err := shouldError() +// require.Error(c, err) +// require.Equal(c, 7, i) +// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") +func (a *Assertions) Consistentlyf(condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Consistentlyf(a.t, condition, waitFor, tick, msg, args...) +} + // Contains asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // @@ -313,6 +361,10 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) { // periodically checking target function each tick. // // a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +// +// Deprecated: For some values of waitFor and tick; Eventually may fail having +// never called condition. Eventually may leak goroutines. Use [EventuallySync] +// instead. func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -320,6 +372,58 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti Eventually(a.t, condition, waitFor, tick, msgAndArgs...) } +// EventuallySync asserts that a given condition will be met before waitFor time +// plus the time taken for the last call to condition to return. The condition +// is considered met when the condition function does not report failures on the +// CollectT passed to it. The supplied CollectT collects all errors from one +// tick (if there are any) and if the condition is not met before EventuallySync +// returns, then the collected errors of the last tick are copied to t. The +// condition function is called synchronously immediately and then every tick +// duration after that. EventuallySync will adjust the time interval or drop +// ticks to make up for slow condition functions. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallySync(func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallySync(condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EventuallySync(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallySyncf asserts that a given condition will be met before waitFor time +// plus the time taken for the last call to condition to return. The condition +// is considered met when the condition function does not report failures on the +// CollectT passed to it. The supplied CollectT collects all errors from one +// tick (if there are any) and if the condition is not met before EventuallySyncf +// returns, then the collected errors of the last tick are copied to t. The +// condition function is called synchronously immediately and then every tick +// duration after that. EventuallySyncf will adjust the time interval or drop +// ticks to make up for slow condition functions. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallySyncf(func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallySyncf(condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EventuallySyncf(a.t, condition, waitFor, tick, msg, args...) +} + // EventuallyWithT asserts that given condition will be met in waitFor time, // periodically checking target function each tick. In contrast to Eventually, // it supplies a CollectT to the condition function, so that the condition @@ -338,6 +442,10 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// +// Deprecated: For some values of waitFor and tick; EventuallyWithT may fail +// having never called condition. EventuallyWithT may leak goroutines. Use +// [EventuallySync] instead. func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -363,6 +471,10 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// +// Deprecated: For some values of waitFor and tick; EventuallyWithTf may fail +// having never called condition. EventuallyWithTf may leak goroutines. Use +// [EventuallySync] instead. func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -374,6 +486,10 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), // periodically checking target function each tick. // // a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// +// Deprecated: For some values of waitFor and tick; Eventuallyf may fail having +// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfSync] +// instead. func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1003,6 +1119,10 @@ func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) { // periodically checking the target function each tick. // // a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +// +// Deprecated: For some values of waitFor and tick; Never may pass without +// calling condition. Never may leak goroutines. Use [Consistently] and invert +// the logic of condition instead. func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1014,6 +1134,10 @@ func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick ti // periodically checking the target function each tick. // // a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// +// Deprecated: For some values of waitFor and tick; Neverf may pass without +// calling condition. Neverf may leak goroutines. Use [Consistently] and invert +// the logic of condition instead. func (a *Assertions) Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() From f11704214580bf558154b6ddd0ac4d7a5163105e Mon Sep 17 00:00:00 2001 From: Bracken Dawson Date: Sat, 5 Oct 2024 00:15:29 +0100 Subject: [PATCH 2/3] Make TestEventuallyTimeout less likelt to flake And if it does flake, make it do so quickly rather than deadlocking. --- assert/assertions.go | 18 +++++++++--------- assert/assertions_test.go | 18 +++++++++++++++--- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/assert/assertions.go b/assert/assertions.go index 420460730..99910db2f 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -1936,12 +1936,12 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t ch := make(chan bool, 1) - timer := time.NewTimer(waitFor) - defer timer.Stop() - ticker := time.NewTicker(tick) defer ticker.Stop() + timer := time.NewTimer(waitFor) + defer timer.Stop() + for tick := ticker.C; ; { select { case <-timer.C: @@ -2027,12 +2027,12 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time var lastFinishedTickErrs []error ch := make(chan *CollectT, 1) - timer := time.NewTimer(waitFor) - defer timer.Stop() - ticker := time.NewTicker(tick) defer ticker.Stop() + timer := time.NewTimer(waitFor) + defer timer.Stop() + for tick := ticker.C; ; { select { case <-timer.C: @@ -2131,12 +2131,12 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D ch := make(chan bool, 1) - timer := time.NewTimer(waitFor) - defer timer.Stop() - ticker := time.NewTicker(tick) defer ticker.Stop() + timer := time.NewTimer(waitFor) + defer timer.Stop() + for tick := ticker.C; ; { select { case <-timer.C: diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 0b2696bb4..1204bab8d 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -3,6 +3,7 @@ package assert import ( "bufio" "bytes" + "context" "encoding/json" "errors" "fmt" @@ -3012,21 +3013,32 @@ func TestNeverTrue(t *testing.T) { func TestEventuallyTimeout(t *testing.T) { mockT := new(testing.T) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + NotPanics(t, func() { done, done2 := make(chan struct{}), make(chan struct{}) // A condition function that returns after the Eventually timeout condition := func() bool { // Wait until Eventually times out and terminates - <-done + select { + case <-done: + case <-ctx.Done(): + panic("test timed out") + } close(done2) return true } - False(t, Eventually(mockT, condition, time.Millisecond, time.Microsecond)) + False(t, Eventually(mockT, condition, 10*time.Millisecond, time.Microsecond)) close(done) - <-done2 + select { + case <-done2: + case <-ctx.Done(): + panic("test timed out") + } }) } From bb0403b9544ee0b05ba39ff2e69cbb3529254bf9 Mon Sep 17 00:00:00 2001 From: Bracken Dawson Date: Sat, 5 Oct 2024 17:38:23 +0100 Subject: [PATCH 3/3] Number of calls is better than time for a synchronous Eventually/Consistently. --- assert/assertion_format.go | 55 ++++++------ assert/assertion_forward.go | 110 ++++++++++++------------ assert/assertions.go | 81 +++++++++--------- assert/assertions_test.go | 163 +++++++++++++++++++++++++++++------- require/require.go | 110 ++++++++++++------------ require/require_forward.go | 110 ++++++++++++------------ 6 files changed, 356 insertions(+), 273 deletions(-) diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 2e83bbe8d..561b5c759 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -16,28 +16,25 @@ func Conditionf(t TestingT, comp Comparison, msg string, args ...interface{}) bo return Condition(t, comp, append([]interface{}{msg}, args...)...) } -// Consistentlyf asserts that a given condition will always be met until -// waitFor time plus the time taken for the last call to condition to return has -// elapsed. The condition is considered met when the condition function does not -// report failures on the CollectT passed to it. The supplied CollectT collects -// all errors from one tick (if there are any) and if the condition is ever not -// met, then the collected errors are copied to t. It is safe to use assertions -// from the require package in the condition function, these will immediately -// cease execution of the condition function, but not of the test. The condition -// function is called synchronously immediately and then every tick duration -// after that. Consistentlyf will adjust the time interval or drop ticks to make -// up for slow condition functions. +// Consistentlyf asserts that a given condition will be met for times calls of +// the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met then +// the collected errors of the last call are copied to t. The condition function +// is called synchronously immediately and then every tick duration after that. +// If condition returns after the tick interval then the next call will be made +// immediately. Consistentlyf will panic if called with non-positive times. // // assert.Consistentlyf(t, func(c *assert.CollectT, "error message %s", "formatted") { // i, err := shouldError() // require.Error(c, err) // require.Equal(c, 7, i) -// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") -func Consistentlyf(t TestingT, condition func(*CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { +// }, 10, 1*time.Second, "shouldError() did not return 7 and an error") +func Consistentlyf(t TestingT, condition func(*CollectT), times int, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - return Consistently(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) + return Consistently(t, condition, times, tick, append([]interface{}{msg}, args...)...) } // Containsf asserts that the specified string, list(array, slice...) or map contains the @@ -188,7 +185,7 @@ func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface // assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") // // Deprecated: For some values of waitFor and tick; Eventuallyf may fail having -// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfSync] +// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfTimes] // instead. func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { @@ -197,30 +194,30 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick return Eventually(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) } -// EventuallySyncf asserts that a given condition will be met before waitFor time -// plus the time taken for the last call to condition to return. The condition -// is considered met when the condition function does not report failures on the -// CollectT passed to it. The supplied CollectT collects all errors from one -// tick (if there are any) and if the condition is not met before EventuallySyncf -// returns, then the collected errors of the last tick are copied to t. The -// condition function is called synchronously immediately and then every tick -// duration after that. EventuallySyncf will adjust the time interval or drop -// ticks to make up for slow condition functions. +// EventuallyTimesf asserts that a given condition will be met within times calls +// of the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met after +// the last call, then the collected errors of the last call are copied to t. +// The condition function is called synchronously immediately and then every +// tick duration after that. If condition returns after the tick interval then +// the next call will be made immediately. EventuallyTimesf will panic if called +// with non-positive times. // // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallySyncf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// assert.EventuallyTimesf(t, func(c *assert.CollectT, "error message %s", "formatted") { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") -func EventuallySyncf(t TestingT, condition func(*CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { +// }, 10, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyTimesf(t TestingT, condition func(*CollectT), times int, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - return EventuallySync(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) + return EventuallyTimes(t, condition, times, tick, append([]interface{}{msg}, args...)...) } // EventuallyWithTf asserts that given condition will be met in waitFor time, @@ -244,7 +241,7 @@ func EventuallySyncf(t TestingT, condition func(*CollectT), waitFor time.Duratio // // Deprecated: For some values of waitFor and tick; EventuallyWithTf may fail // having never called condition. EventuallyWithTf may leak goroutines. Use -// [EventuallySync] instead. +// [EventuallyTimes] instead. func EventuallyWithTf(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index 4d6564327..a1fbade0b 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -24,52 +24,46 @@ func (a *Assertions) Conditionf(comp Comparison, msg string, args ...interface{} return Conditionf(a.t, comp, msg, args...) } -// Consistently asserts that a given condition will always be met until -// waitFor time plus the time taken for the last call to condition to return has -// elapsed. The condition is considered met when the condition function does not -// report failures on the CollectT passed to it. The supplied CollectT collects -// all errors from one tick (if there are any) and if the condition is ever not -// met, then the collected errors are copied to t. It is safe to use assertions -// from the require package in the condition function, these will immediately -// cease execution of the condition function, but not of the test. The condition -// function is called synchronously immediately and then every tick duration -// after that. Consistently will adjust the time interval or drop ticks to make -// up for slow condition functions. +// Consistently asserts that a given condition will be met for times calls of +// the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met then +// the collected errors of the last call are copied to t. The condition function +// is called synchronously immediately and then every tick duration after that. +// If condition returns after the tick interval then the next call will be made +// immediately. Consistently will panic if called with non-positive times. // // a.Consistently(func(c *assert.CollectT) { // i, err := shouldError() // require.Error(c, err) // require.Equal(c, 7, i) -// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") -func (a *Assertions) Consistently(condition func(*CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { +// }, 10, 1*time.Second, "shouldError() did not return 7 and an error") +func (a *Assertions) Consistently(condition func(*CollectT), times int, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() } - return Consistently(a.t, condition, waitFor, tick, msgAndArgs...) + return Consistently(a.t, condition, times, tick, msgAndArgs...) } -// Consistentlyf asserts that a given condition will always be met until -// waitFor time plus the time taken for the last call to condition to return has -// elapsed. The condition is considered met when the condition function does not -// report failures on the CollectT passed to it. The supplied CollectT collects -// all errors from one tick (if there are any) and if the condition is ever not -// met, then the collected errors are copied to t. It is safe to use assertions -// from the require package in the condition function, these will immediately -// cease execution of the condition function, but not of the test. The condition -// function is called synchronously immediately and then every tick duration -// after that. Consistentlyf will adjust the time interval or drop ticks to make -// up for slow condition functions. +// Consistentlyf asserts that a given condition will be met for times calls of +// the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met then +// the collected errors of the last call are copied to t. The condition function +// is called synchronously immediately and then every tick duration after that. +// If condition returns after the tick interval then the next call will be made +// immediately. Consistentlyf will panic if called with non-positive times. // // a.Consistentlyf(func(c *assert.CollectT, "error message %s", "formatted") { // i, err := shouldError() // require.Error(c, err) // require.Equal(c, 7, i) -// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") -func (a *Assertions) Consistentlyf(condition func(*CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { +// }, 10, 1*time.Second, "shouldError() did not return 7 and an error") +func (a *Assertions) Consistentlyf(condition func(*CollectT), times int, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() } - return Consistentlyf(a.t, condition, waitFor, tick, msg, args...) + return Consistentlyf(a.t, condition, times, tick, msg, args...) } // Contains asserts that the specified string, list(array, slice...) or map contains the @@ -362,7 +356,7 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool { // a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) // // Deprecated: For some values of waitFor and tick; Eventually may fail having -// never called condition. Eventually may leak goroutines. Use [EventuallySync] +// never called condition. Eventually may leak goroutines. Use [EventuallyTimes] // instead. func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { @@ -371,56 +365,56 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti return Eventually(a.t, condition, waitFor, tick, msgAndArgs...) } -// EventuallySync asserts that a given condition will be met before waitFor time -// plus the time taken for the last call to condition to return. The condition -// is considered met when the condition function does not report failures on the -// CollectT passed to it. The supplied CollectT collects all errors from one -// tick (if there are any) and if the condition is not met before EventuallySync -// returns, then the collected errors of the last tick are copied to t. The -// condition function is called synchronously immediately and then every tick -// duration after that. EventuallySync will adjust the time interval or drop -// ticks to make up for slow condition functions. +// EventuallyTimes asserts that a given condition will be met within times calls +// of the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met after +// the last call, then the collected errors of the last call are copied to t. +// The condition function is called synchronously immediately and then every +// tick duration after that. If condition returns after the tick interval then +// the next call will be made immediately. EventuallyTimes will panic if called +// with non-positive times. // // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// a.EventuallySync(func(c *assert.CollectT) { +// a.EventuallyTimes(func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") -func (a *Assertions) EventuallySync(condition func(*CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { +// }, 10, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyTimes(condition func(*CollectT), times int, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() } - return EventuallySync(a.t, condition, waitFor, tick, msgAndArgs...) + return EventuallyTimes(a.t, condition, times, tick, msgAndArgs...) } -// EventuallySyncf asserts that a given condition will be met before waitFor time -// plus the time taken for the last call to condition to return. The condition -// is considered met when the condition function does not report failures on the -// CollectT passed to it. The supplied CollectT collects all errors from one -// tick (if there are any) and if the condition is not met before EventuallySyncf -// returns, then the collected errors of the last tick are copied to t. The -// condition function is called synchronously immediately and then every tick -// duration after that. EventuallySyncf will adjust the time interval or drop -// ticks to make up for slow condition functions. +// EventuallyTimesf asserts that a given condition will be met within times calls +// of the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met after +// the last call, then the collected errors of the last call are copied to t. +// The condition function is called synchronously immediately and then every +// tick duration after that. If condition returns after the tick interval then +// the next call will be made immediately. EventuallyTimesf will panic if called +// with non-positive times. // // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// a.EventuallySyncf(func(c *assert.CollectT, "error message %s", "formatted") { +// a.EventuallyTimesf(func(c *assert.CollectT, "error message %s", "formatted") { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") -func (a *Assertions) EventuallySyncf(condition func(*CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { +// }, 10, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyTimesf(condition func(*CollectT), times int, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() } - return EventuallySyncf(a.t, condition, waitFor, tick, msg, args...) + return EventuallyTimesf(a.t, condition, times, tick, msg, args...) } // EventuallyWithT asserts that given condition will be met in waitFor time, @@ -444,7 +438,7 @@ func (a *Assertions) EventuallySyncf(condition func(*CollectT), waitFor time.Dur // // Deprecated: For some values of waitFor and tick; EventuallyWithT may fail // having never called condition. EventuallyWithT may leak goroutines. Use -// [EventuallySync] instead. +// [EventuallyTimes] instead. func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -473,7 +467,7 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // // Deprecated: For some values of waitFor and tick; EventuallyWithTf may fail // having never called condition. EventuallyWithTf may leak goroutines. Use -// [EventuallySync] instead. +// [EventuallyTimes] instead. func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -487,7 +481,7 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor // a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") // // Deprecated: For some values of waitFor and tick; Eventuallyf may fail having -// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfSync] +// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfTimes] // instead. func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { diff --git a/assert/assertions.go b/assert/assertions.go index 99910db2f..44ca8f541 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -1927,7 +1927,7 @@ type tHelper = interface { // assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) // // Deprecated: For some values of waitFor and tick; Eventually may fail having -// never called condition. Eventually may leak goroutines. Use [EventuallySync] +// never called condition. Eventually may leak goroutines. Use [EventuallyTimes] // instead. func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { @@ -2018,7 +2018,7 @@ func (c *CollectT) failed() bool { // // Deprecated: For some values of waitFor and tick; EventuallyWithT may fail // having never called condition. EventuallyWithT may leak goroutines. Use -// [EventuallySync] instead. +// [EventuallyTimes] instead. func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -2060,27 +2060,29 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time } } -// EventuallySync asserts that a given condition will be met before waitFor time -// plus the time taken for the last call to condition to return. The condition -// is considered met when the condition function does not report failures on the -// CollectT passed to it. The supplied CollectT collects all errors from one -// tick (if there are any) and if the condition is not met before EventuallySync -// returns, then the collected errors of the last tick are copied to t. The -// condition function is called synchronously immediately and then every tick -// duration after that. EventuallySync will adjust the time interval or drop -// ticks to make up for slow condition functions. +// EventuallyTimes asserts that a given condition will be met within times calls +// of the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met after +// the last call, then the collected errors of the last call are copied to t. +// The condition function is called synchronously immediately and then every +// tick duration after that. If condition returns after the tick interval then +// the next call will be made immediately. EventuallyTimes will panic if called +// with non-positive times. // // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallySync(t, func(c *assert.CollectT) { +// assert.EventuallyTimes(t, func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") -func EventuallySync(t TestingT, condition func(*CollectT), waitFor, tick time.Duration, msgAndArgs ...interface{}) bool { - deadline := time.Now().Add(waitFor) +// }, 10, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyTimes(t TestingT, condition func(*CollectT), times int, tick time.Duration, msgAndArgs ...interface{}) bool { + if times < 1 { + panic("non-positive times for EventuallyTimes") + } tickerCh := make(chan time.Time) close(tickerCh) @@ -2091,7 +2093,8 @@ func EventuallySync(t TestingT, condition func(*CollectT), waitFor, tick time.Du ticker = timeTicker.C } - for { + var lastErrors []error + for i := 0; i < times; i++ { collect := new(CollectT) wait := make(chan struct{}) @@ -2100,20 +2103,19 @@ func EventuallySync(t TestingT, condition func(*CollectT), waitFor, tick time.Du condition(collect) }() <-wait + lastErrors = collect.errors if !collect.failed() { return true } <-ticker + } - if time.Now().After(deadline) { - for _, err := range collect.errors { - t.Errorf("%v", err) - } - return Fail(t, "Condition never satisfied", msgAndArgs...) - } + for _, err := range lastErrors { + t.Errorf("%v", err) } + return Fail(t, "Condition never satisfied", msgAndArgs...) } // Never asserts that the given condition doesn't satisfy in waitFor time, @@ -2153,25 +2155,24 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D } } -// Consistently asserts that a given condition will always be met until -// waitFor time plus the time taken for the last call to condition to return has -// elapsed. The condition is considered met when the condition function does not -// report failures on the CollectT passed to it. The supplied CollectT collects -// all errors from one tick (if there are any) and if the condition is ever not -// met, then the collected errors are copied to t. It is safe to use assertions -// from the require package in the condition function, these will immediately -// cease execution of the condition function, but not of the test. The condition -// function is called synchronously immediately and then every tick duration -// after that. Consistently will adjust the time interval or drop ticks to make -// up for slow condition functions. +// Consistently asserts that a given condition will be met for times calls of +// the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met then +// the collected errors of the last call are copied to t. The condition function +// is called synchronously immediately and then every tick duration after that. +// If condition returns after the tick interval then the next call will be made +// immediately. Consistently will panic if called with non-positive times. // // assert.Consistently(t, func(c *assert.CollectT) { // i, err := shouldError() // require.Error(c, err) // require.Equal(c, 7, i) -// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") -func Consistently(t TestingT, condition func(*CollectT), waitFor, tick time.Duration, msgAndArgs ...interface{}) bool { - deadline := time.Now().Add(waitFor) +// }, 10, 1*time.Second, "shouldError() did not return 7 and an error") +func Consistently(t TestingT, condition func(*CollectT), times int, tick time.Duration, msgAndArgs ...interface{}) bool { + if times < 1 { + panic("non-positive times for Consistently") + } tickerCh := make(chan time.Time) close(tickerCh) @@ -2182,7 +2183,7 @@ func Consistently(t TestingT, condition func(*CollectT), waitFor, tick time.Dura ticker = timeTicker.C } - for { + for i := 0; i < times; i++ { collect := new(CollectT) wait := make(chan struct{}) @@ -2200,11 +2201,9 @@ func Consistently(t TestingT, condition func(*CollectT), waitFor, tick time.Dura } <-ticker - - if time.Now().After(deadline) { - return true - } } + + return true } // ErrorIs asserts that at least one of the errors in err's chain matches target. diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 1204bab8d..368f139ab 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -3042,18 +3042,21 @@ func TestEventuallyTimeout(t *testing.T) { }) } -func TestEventuallySync(t *testing.T) { +func TestEventuallyTimes(t *testing.T) { for name, test := range map[string]struct { condition func(*CollectT) - waitFor, tick time.Duration + times int expected bool expectedErrors []error }{ - "zero_values": {func(t *CollectT) {}, 0, 0, true, nil}, + "passes": { + condition: func(t *CollectT) {}, + times: 10, + expected: true, + }, "fails": { condition: func(t *CollectT) { t.Errorf("") }, - waitFor: 0, - tick: 0, + times: 10, expected: false, expectedErrors: []error{ errors.New(""), @@ -3071,8 +3074,7 @@ func TestEventuallySync(t *testing.T) { t.Errorf("oops") } }(), - waitFor: time.Second, - tick: 0, + times: 10, expected: true, }, "passes_22nd": { @@ -3086,10 +3088,27 @@ func TestEventuallySync(t *testing.T) { t.Errorf("oops") } }(), - waitFor: time.Second, - tick: 0, + times: 30, expected: true, }, + "would_pass_22nd": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + if counter >= 22 { + return + } + t.Errorf("%d", counter) + } + }(), + times: 21, + expected: false, + expectedErrors: []error{ + errors.New("21"), + errors.New("\n\tError Trace:\t\n\tError: \tCondition never satisfied\n"), + }, + }, "fails_3_times": { condition: func() func(*CollectT) { counter := 0 @@ -3098,8 +3117,7 @@ func TestEventuallySync(t *testing.T) { t.Errorf("%d", counter) } }(), - waitFor: 100 * time.Millisecond, - tick: 35 * time.Millisecond, + times: 3, expected: false, expectedErrors: []error{ errors.New("3"), @@ -3117,8 +3135,7 @@ func TestEventuallySync(t *testing.T) { t.FailNow() } }(), - waitFor: time.Second, - tick: time.Millisecond, + times: 10, expected: true, }, "fails_with_fail_now": { @@ -3128,8 +3145,7 @@ func TestEventuallySync(t *testing.T) { t.FailNow() t.Errorf("should not be seen") }, - waitFor: time.Millisecond, - tick: time.Millisecond, + times: 10, expected: false, expectedErrors: []error{ errors.New("should be seen"), @@ -3143,24 +3159,67 @@ func TestEventuallySync(t *testing.T) { t.Parallel() mockT := new(errorsCapturingT) - Equal(t, test.expected, EventuallySync(mockT, test.condition, test.waitFor, test.tick)) + Equal(t, test.expected, EventuallyTimes(mockT, test.condition, test.times, 0)) Equal(t, test.expectedErrors, mockT.errors) }) } } +func TestEventuallyTimes0Times(t *testing.T) { + PanicsWithValue(t, "non-positive times for EventuallyTimes", func() { + EventuallyTimes(new(mockTestingT), func(ct *CollectT) {}, 0, 0) + }) +} + +func TestEventuallyTimesTick(t *testing.T) { + for name, test := range map[string]struct { + condition func(*CollectT) + times int + tick time.Duration + expectedMinTime time.Duration + }{ + "ticks_of_10ms": { + condition: func(t *CollectT) { t.FailNow() }, + times: 5, + tick: 10 * time.Millisecond, + expectedMinTime: 50 * time.Millisecond, + }, + "slow_condition": { + condition: func(t *CollectT) { + time.Sleep(10 * time.Millisecond) + t.FailNow() + }, + times: 5, + tick: 1 * time.Millisecond, + expectedMinTime: 50 * time.Millisecond, + }, + } { + t.Run(name, func(t *testing.T) { + test := test + t.Parallel() + + start := time.Now() + EventuallyTimes(new(mockTestingT), test.condition, test.times, test.tick) + Greater(t, time.Since(start), test.expectedMinTime) + }) + } +} + func TestConsistently(t *testing.T) { for name, test := range map[string]struct { condition func(*CollectT) - waitFor, tick time.Duration + times int expected bool expectedErrors []error }{ - "zero_values": {func(*CollectT) {}, 0, 0, true, nil}, + "passes": { + condition: func(*CollectT) {}, + times: 10, + expected: true, + }, "fails_immediately": { condition: func(t *CollectT) { t.Errorf("") }, - waitFor: 0, - tick: 0, + times: 10, expected: false, expectedErrors: []error{ errors.New(""), @@ -3177,8 +3236,7 @@ func TestConsistently(t *testing.T) { } } }(), - waitFor: time.Second, - tick: 0, + times: 10, expected: false, expectedErrors: []error{ errors.New("oops"), @@ -3195,14 +3253,26 @@ func TestConsistently(t *testing.T) { } } }(), - waitFor: time.Second, - tick: 0, + times: 30, expected: false, expectedErrors: []error{ errors.New("oops"), errors.New("\n\tError Trace:\t\n\tError: \tCondition was not satisfied\n"), }, }, + "would_fail_22nd": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + if counter >= 22 { + t.Errorf("oops") + } + } + }(), + times: 21, + expected: true, + }, "passes_2_times": { condition: func() func(*CollectT) { counter := 0 @@ -3214,8 +3284,7 @@ func TestConsistently(t *testing.T) { t.Errorf("%d", counter) } }(), - waitFor: 100 * time.Millisecond, - tick: 35 * time.Millisecond, + times: 10, expected: false, expectedErrors: []error{ errors.New("3"), @@ -3229,8 +3298,7 @@ func TestConsistently(t *testing.T) { t.FailNow() t.Errorf("should not be seen") }, - waitFor: time.Millisecond, - tick: time.Millisecond, + times: 10, expected: false, expectedErrors: []error{ errors.New("should be seen"), @@ -3244,12 +3312,49 @@ func TestConsistently(t *testing.T) { t.Parallel() mockT := new(errorsCapturingT) - Equal(t, test.expected, Consistently(mockT, test.condition, test.waitFor, test.tick)) + Equal(t, test.expected, Consistently(mockT, test.condition, test.times, 0)) Equal(t, test.expectedErrors, mockT.errors) }) } } +func TestConsistently0Times(t *testing.T) { + PanicsWithValue(t, "non-positive times for Consistently", func() { + Consistently(new(mockTestingT), func(ct *CollectT) {}, 0, 0) + }) +} + +func TestConsistentlyTick(t *testing.T) { + for name, test := range map[string]struct { + condition func(*CollectT) + times int + tick time.Duration + expectedMinTime time.Duration + }{ + "ticks_of_10ms": { + condition: func(ct *CollectT) {}, + times: 5, + tick: 10 * time.Millisecond, + expectedMinTime: 50 * time.Millisecond, + }, + "slow_condition": { + condition: func(ct *CollectT) { time.Sleep(10 * time.Millisecond) }, + times: 5, + tick: 1 * time.Millisecond, + expectedMinTime: 50 * time.Millisecond, + }, + } { + t.Run(name, func(t *testing.T) { + test := test + t.Parallel() + + start := time.Now() + Consistently(new(mockTestingT), test.condition, test.times, test.tick) + Greater(t, time.Since(start), test.expectedMinTime) + }) + } +} + func Test_validateEqualArgs(t *testing.T) { if validateEqualArgs(func() {}, func() {}) == nil { t.Error("non-nil functions should error") diff --git a/require/require.go b/require/require.go index 432a1014e..987e4282a 100644 --- a/require/require.go +++ b/require/require.go @@ -31,55 +31,49 @@ func Conditionf(t TestingT, comp assert.Comparison, msg string, args ...interfac t.FailNow() } -// Consistently asserts that a given condition will always be met until -// waitFor time plus the time taken for the last call to condition to return has -// elapsed. The condition is considered met when the condition function does not -// report failures on the CollectT passed to it. The supplied CollectT collects -// all errors from one tick (if there are any) and if the condition is ever not -// met, then the collected errors are copied to t. It is safe to use assertions -// from the require package in the condition function, these will immediately -// cease execution of the condition function, but not of the test. The condition -// function is called synchronously immediately and then every tick duration -// after that. Consistently will adjust the time interval or drop ticks to make -// up for slow condition functions. +// Consistently asserts that a given condition will be met for times calls of +// the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met then +// the collected errors of the last call are copied to t. The condition function +// is called synchronously immediately and then every tick duration after that. +// If condition returns after the tick interval then the next call will be made +// immediately. Consistently will panic if called with non-positive times. // // assert.Consistently(t, func(c *assert.CollectT) { // i, err := shouldError() // require.Error(c, err) // require.Equal(c, 7, i) -// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") -func Consistently(t TestingT, condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { +// }, 10, 1*time.Second, "shouldError() did not return 7 and an error") +func Consistently(t TestingT, condition func(*assert.CollectT), times int, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() } - if assert.Consistently(t, condition, waitFor, tick, msgAndArgs...) { + if assert.Consistently(t, condition, times, tick, msgAndArgs...) { return } t.FailNow() } -// Consistentlyf asserts that a given condition will always be met until -// waitFor time plus the time taken for the last call to condition to return has -// elapsed. The condition is considered met when the condition function does not -// report failures on the CollectT passed to it. The supplied CollectT collects -// all errors from one tick (if there are any) and if the condition is ever not -// met, then the collected errors are copied to t. It is safe to use assertions -// from the require package in the condition function, these will immediately -// cease execution of the condition function, but not of the test. The condition -// function is called synchronously immediately and then every tick duration -// after that. Consistentlyf will adjust the time interval or drop ticks to make -// up for slow condition functions. +// Consistentlyf asserts that a given condition will be met for times calls of +// the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met then +// the collected errors of the last call are copied to t. The condition function +// is called synchronously immediately and then every tick duration after that. +// If condition returns after the tick interval then the next call will be made +// immediately. Consistentlyf will panic if called with non-positive times. // // assert.Consistentlyf(t, func(c *assert.CollectT, "error message %s", "formatted") { // i, err := shouldError() // require.Error(c, err) // require.Equal(c, 7, i) -// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") -func Consistentlyf(t TestingT, condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { +// }, 10, 1*time.Second, "shouldError() did not return 7 and an error") +func Consistentlyf(t TestingT, condition func(*assert.CollectT), times int, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() } - if assert.Consistentlyf(t, condition, waitFor, tick, msg, args...) { + if assert.Consistentlyf(t, condition, times, tick, msg, args...) { return } t.FailNow() @@ -447,7 +441,7 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) { // assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) // // Deprecated: For some values of waitFor and tick; Eventually may fail having -// never called condition. Eventually may leak goroutines. Use [EventuallySync] +// never called condition. Eventually may leak goroutines. Use [EventuallyTimes] // instead. func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { @@ -459,59 +453,59 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t t.FailNow() } -// EventuallySync asserts that a given condition will be met before waitFor time -// plus the time taken for the last call to condition to return. The condition -// is considered met when the condition function does not report failures on the -// CollectT passed to it. The supplied CollectT collects all errors from one -// tick (if there are any) and if the condition is not met before EventuallySync -// returns, then the collected errors of the last tick are copied to t. The -// condition function is called synchronously immediately and then every tick -// duration after that. EventuallySync will adjust the time interval or drop -// ticks to make up for slow condition functions. +// EventuallyTimes asserts that a given condition will be met within times calls +// of the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met after +// the last call, then the collected errors of the last call are copied to t. +// The condition function is called synchronously immediately and then every +// tick duration after that. If condition returns after the tick interval then +// the next call will be made immediately. EventuallyTimes will panic if called +// with non-positive times. // // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallySync(t, func(c *assert.CollectT) { +// assert.EventuallyTimes(t, func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") -func EventuallySync(t TestingT, condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { +// }, 10, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyTimes(t TestingT, condition func(*assert.CollectT), times int, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() } - if assert.EventuallySync(t, condition, waitFor, tick, msgAndArgs...) { + if assert.EventuallyTimes(t, condition, times, tick, msgAndArgs...) { return } t.FailNow() } -// EventuallySyncf asserts that a given condition will be met before waitFor time -// plus the time taken for the last call to condition to return. The condition -// is considered met when the condition function does not report failures on the -// CollectT passed to it. The supplied CollectT collects all errors from one -// tick (if there are any) and if the condition is not met before EventuallySyncf -// returns, then the collected errors of the last tick are copied to t. The -// condition function is called synchronously immediately and then every tick -// duration after that. EventuallySyncf will adjust the time interval or drop -// ticks to make up for slow condition functions. +// EventuallyTimesf asserts that a given condition will be met within times calls +// of the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met after +// the last call, then the collected errors of the last call are copied to t. +// The condition function is called synchronously immediately and then every +// tick duration after that. If condition returns after the tick interval then +// the next call will be made immediately. EventuallyTimesf will panic if called +// with non-positive times. // // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallySyncf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// assert.EventuallyTimesf(t, func(c *assert.CollectT, "error message %s", "formatted") { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") -func EventuallySyncf(t TestingT, condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { +// }, 10, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyTimesf(t TestingT, condition func(*assert.CollectT), times int, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() } - if assert.EventuallySyncf(t, condition, waitFor, tick, msg, args...) { + if assert.EventuallyTimesf(t, condition, times, tick, msg, args...) { return } t.FailNow() @@ -538,7 +532,7 @@ func EventuallySyncf(t TestingT, condition func(*assert.CollectT), waitFor time. // // Deprecated: For some values of waitFor and tick; EventuallyWithT may fail // having never called condition. EventuallyWithT may leak goroutines. Use -// [EventuallySync] instead. +// [EventuallyTimes] instead. func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -570,7 +564,7 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF // // Deprecated: For some values of waitFor and tick; EventuallyWithTf may fail // having never called condition. EventuallyWithTf may leak goroutines. Use -// [EventuallySync] instead. +// [EventuallyTimes] instead. func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -587,7 +581,7 @@ func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), wait // assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") // // Deprecated: For some values of waitFor and tick; Eventuallyf may fail having -// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfSync] +// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfTimes] // instead. func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { diff --git a/require/require_forward.go b/require/require_forward.go index 783d168a7..2bb95135b 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -25,52 +25,46 @@ func (a *Assertions) Conditionf(comp assert.Comparison, msg string, args ...inte Conditionf(a.t, comp, msg, args...) } -// Consistently asserts that a given condition will always be met until -// waitFor time plus the time taken for the last call to condition to return has -// elapsed. The condition is considered met when the condition function does not -// report failures on the CollectT passed to it. The supplied CollectT collects -// all errors from one tick (if there are any) and if the condition is ever not -// met, then the collected errors are copied to t. It is safe to use assertions -// from the require package in the condition function, these will immediately -// cease execution of the condition function, but not of the test. The condition -// function is called synchronously immediately and then every tick duration -// after that. Consistently will adjust the time interval or drop ticks to make -// up for slow condition functions. +// Consistently asserts that a given condition will be met for times calls of +// the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met then +// the collected errors of the last call are copied to t. The condition function +// is called synchronously immediately and then every tick duration after that. +// If condition returns after the tick interval then the next call will be made +// immediately. Consistently will panic if called with non-positive times. // // a.Consistently(func(c *assert.CollectT) { // i, err := shouldError() // require.Error(c, err) // require.Equal(c, 7, i) -// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") -func (a *Assertions) Consistently(condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { +// }, 10, 1*time.Second, "shouldError() did not return 7 and an error") +func (a *Assertions) Consistently(condition func(*assert.CollectT), times int, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() } - Consistently(a.t, condition, waitFor, tick, msgAndArgs...) + Consistently(a.t, condition, times, tick, msgAndArgs...) } -// Consistentlyf asserts that a given condition will always be met until -// waitFor time plus the time taken for the last call to condition to return has -// elapsed. The condition is considered met when the condition function does not -// report failures on the CollectT passed to it. The supplied CollectT collects -// all errors from one tick (if there are any) and if the condition is ever not -// met, then the collected errors are copied to t. It is safe to use assertions -// from the require package in the condition function, these will immediately -// cease execution of the condition function, but not of the test. The condition -// function is called synchronously immediately and then every tick duration -// after that. Consistentlyf will adjust the time interval or drop ticks to make -// up for slow condition functions. +// Consistentlyf asserts that a given condition will be met for times calls of +// the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met then +// the collected errors of the last call are copied to t. The condition function +// is called synchronously immediately and then every tick duration after that. +// If condition returns after the tick interval then the next call will be made +// immediately. Consistentlyf will panic if called with non-positive times. // // a.Consistentlyf(func(c *assert.CollectT, "error message %s", "formatted") { // i, err := shouldError() // require.Error(c, err) // require.Equal(c, 7, i) -// }, 10*time.Second, 1*time.Second, "shouldError() did not return 7 and an error") -func (a *Assertions) Consistentlyf(condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { +// }, 10, 1*time.Second, "shouldError() did not return 7 and an error") +func (a *Assertions) Consistentlyf(condition func(*assert.CollectT), times int, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() } - Consistentlyf(a.t, condition, waitFor, tick, msg, args...) + Consistentlyf(a.t, condition, times, tick, msg, args...) } // Contains asserts that the specified string, list(array, slice...) or map contains the @@ -363,7 +357,7 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) { // a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) // // Deprecated: For some values of waitFor and tick; Eventually may fail having -// never called condition. Eventually may leak goroutines. Use [EventuallySync] +// never called condition. Eventually may leak goroutines. Use [EventuallyTimes] // instead. func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { @@ -372,56 +366,56 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti Eventually(a.t, condition, waitFor, tick, msgAndArgs...) } -// EventuallySync asserts that a given condition will be met before waitFor time -// plus the time taken for the last call to condition to return. The condition -// is considered met when the condition function does not report failures on the -// CollectT passed to it. The supplied CollectT collects all errors from one -// tick (if there are any) and if the condition is not met before EventuallySync -// returns, then the collected errors of the last tick are copied to t. The -// condition function is called synchronously immediately and then every tick -// duration after that. EventuallySync will adjust the time interval or drop -// ticks to make up for slow condition functions. +// EventuallyTimes asserts that a given condition will be met within times calls +// of the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met after +// the last call, then the collected errors of the last call are copied to t. +// The condition function is called synchronously immediately and then every +// tick duration after that. If condition returns after the tick interval then +// the next call will be made immediately. EventuallyTimes will panic if called +// with non-positive times. // // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// a.EventuallySync(func(c *assert.CollectT) { +// a.EventuallyTimes(func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") -func (a *Assertions) EventuallySync(condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { +// }, 10, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyTimes(condition func(*assert.CollectT), times int, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() } - EventuallySync(a.t, condition, waitFor, tick, msgAndArgs...) + EventuallyTimes(a.t, condition, times, tick, msgAndArgs...) } -// EventuallySyncf asserts that a given condition will be met before waitFor time -// plus the time taken for the last call to condition to return. The condition -// is considered met when the condition function does not report failures on the -// CollectT passed to it. The supplied CollectT collects all errors from one -// tick (if there are any) and if the condition is not met before EventuallySyncf -// returns, then the collected errors of the last tick are copied to t. The -// condition function is called synchronously immediately and then every tick -// duration after that. EventuallySyncf will adjust the time interval or drop -// ticks to make up for slow condition functions. +// EventuallyTimesf asserts that a given condition will be met within times calls +// of the condition function. The condition is considered met when the condition +// function does not report errors on the CollectT passed to it. The supplied +// CollectT collects errors from each call and if the condition is not met after +// the last call, then the collected errors of the last call are copied to t. +// The condition function is called synchronously immediately and then every +// tick duration after that. If condition returns after the tick interval then +// the next call will be made immediately. EventuallyTimesf will panic if called +// with non-positive times. // // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// a.EventuallySyncf(func(c *assert.CollectT, "error message %s", "formatted") { +// a.EventuallyTimesf(func(c *assert.CollectT, "error message %s", "formatted") { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") -func (a *Assertions) EventuallySyncf(condition func(*assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { +// }, 10, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyTimesf(condition func(*assert.CollectT), times int, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() } - EventuallySyncf(a.t, condition, waitFor, tick, msg, args...) + EventuallyTimesf(a.t, condition, times, tick, msg, args...) } // EventuallyWithT asserts that given condition will be met in waitFor time, @@ -445,7 +439,7 @@ func (a *Assertions) EventuallySyncf(condition func(*assert.CollectT), waitFor t // // Deprecated: For some values of waitFor and tick; EventuallyWithT may fail // having never called condition. EventuallyWithT may leak goroutines. Use -// [EventuallySync] instead. +// [EventuallyTimes] instead. func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -474,7 +468,7 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w // // Deprecated: For some values of waitFor and tick; EventuallyWithTf may fail // having never called condition. EventuallyWithTf may leak goroutines. Use -// [EventuallySync] instead. +// [EventuallyTimes] instead. func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -488,7 +482,7 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), // a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") // // Deprecated: For some values of waitFor and tick; Eventuallyf may fail having -// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfSync] +// never called condition. Eventuallyf may leak goroutines. Use [EventuallyfTimes] // instead. func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok {