diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 190634165..561b5c759 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -16,6 +16,27 @@ 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 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, 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, times, tick, append([]interface{}{msg}, args...)...) +} + // Containsf asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // @@ -162,6 +183,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 [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 { h.Helper() @@ -169,6 +194,32 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick return Eventually(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) } +// 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.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, 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 EventuallyTimes(t, condition, times, 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 +238,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 +// [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() @@ -507,6 +562,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..a1fbade0b 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -24,6 +24,48 @@ func (a *Assertions) Conditionf(comp Comparison, msg string, args ...interface{} return Conditionf(a.t, comp, msg, args...) } +// 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, 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, times, tick, msgAndArgs...) +} + +// 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, 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, times, tick, msg, args...) +} + // Contains asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // @@ -312,6 +354,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 [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 { h.Helper() @@ -319,6 +365,58 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti return Eventually(a.t, condition, waitFor, tick, msgAndArgs...) } +// 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.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, 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 EventuallyTimes(a.t, condition, times, tick, msgAndArgs...) +} + +// 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.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, 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 EventuallyTimesf(a.t, condition, times, 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 +435,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 +// [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() @@ -362,6 +464,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 +// [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() @@ -373,6 +479,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 [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 { h.Helper() @@ -1002,6 +1112,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 +1127,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..44ca8f541 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 [EventuallyTimes] +// 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() @@ -1932,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: @@ -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 +// [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() @@ -2019,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: @@ -2052,10 +2060,72 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time } } +// 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.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, 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) + ticker := (<-chan time.Time)(tickerCh) + if tick > 0 { + timeTicker := time.NewTicker(tick) + defer timeTicker.Stop() + ticker = timeTicker.C + } + + var lastErrors []error + for i := 0; i < times; i++ { + collect := new(CollectT) + + wait := make(chan struct{}) + go func() { + defer close(wait) + condition(collect) + }() + <-wait + lastErrors = collect.errors + + if !collect.failed() { + return true + } + + <-ticker + } + + 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, // 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() @@ -2063,12 +2133,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: @@ -2085,6 +2155,57 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D } } +// 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, 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) + ticker := (<-chan time.Time)(tickerCh) + if tick > 0 { + timeTicker := time.NewTicker(tick) + defer timeTicker.Stop() + ticker = timeTicker.C + } + + for i := 0; i < times; i++ { + 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 + } + + 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..368f139ab 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,24 +3013,348 @@ 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") + } }) } +func TestEventuallyTimes(t *testing.T) { + for name, test := range map[string]struct { + condition func(*CollectT) + times int + expected bool + expectedErrors []error + }{ + "passes": { + condition: func(t *CollectT) {}, + times: 10, + expected: true, + }, + "fails": { + condition: func(t *CollectT) { t.Errorf("") }, + times: 10, + 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") + } + }(), + times: 10, + expected: true, + }, + "passes_22nd": { + condition: func() func(*CollectT) { + counter := 0 + return func(t *CollectT) { + counter++ + if counter >= 22 { + return + } + t.Errorf("oops") + } + }(), + 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 + return func(t *CollectT) { + counter++ + t.Errorf("%d", counter) + } + }(), + times: 3, + 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() + } + }(), + times: 10, + 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") + }, + times: 10, + 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, 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) + times int + expected bool + expectedErrors []error + }{ + "passes": { + condition: func(*CollectT) {}, + times: 10, + expected: true, + }, + "fails_immediately": { + condition: func(t *CollectT) { t.Errorf("") }, + times: 10, + 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") + } + } + }(), + times: 10, + 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") + } + } + }(), + 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 + return func(t *CollectT) { + counter++ + if counter <= 2 { + return + } + t.Errorf("%d", counter) + } + }(), + times: 10, + 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") + }, + times: 10, + 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.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 50ec19e13..987e4282a 100644 --- a/require/require.go +++ b/require/require.go @@ -31,6 +31,54 @@ func Conditionf(t TestingT, comp assert.Comparison, msg string, args ...interfac t.FailNow() } +// 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, 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, times, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// 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, 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, times, 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 +439,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 [EventuallyTimes] +// 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 +453,64 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t t.FailNow() } +// 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.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, 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.EventuallyTimes(t, condition, times, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// 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.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, 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.EventuallyTimesf(t, condition, times, 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 +529,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 +// [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() @@ -447,6 +561,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 +// [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() @@ -461,6 +579,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 [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 { h.Helper() @@ -1267,6 +1389,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 +1407,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..2bb95135b 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -25,6 +25,48 @@ func (a *Assertions) Conditionf(comp assert.Comparison, msg string, args ...inte Conditionf(a.t, comp, msg, args...) } +// 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, 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, times, tick, msgAndArgs...) +} + +// 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, 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, times, tick, msg, args...) +} + // Contains asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // @@ -313,6 +355,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 [EventuallyTimes] +// 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 +366,58 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti Eventually(a.t, condition, waitFor, tick, msgAndArgs...) } +// 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.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, 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() + } + EventuallyTimes(a.t, condition, times, tick, msgAndArgs...) +} + +// 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.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, 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() + } + EventuallyTimesf(a.t, condition, times, 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 +436,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 +// [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() @@ -363,6 +465,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 +// [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() @@ -374,6 +480,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 [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 { h.Helper() @@ -1003,6 +1113,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 +1128,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()