diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 2d089991a..5173b5e7d 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -168,6 +168,10 @@ func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface // Eventuallyf asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { @@ -185,6 +189,12 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// and the exit was not via 'collect.FailNow()', the assertion fails immediately. +// This usually means that the condition called t.FailNow() on the outer 't'. +// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to +// only fail the current tick. +// // externalValue := false // go func() { // time.Sleep(8*time.Second) @@ -525,6 +535,10 @@ func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) bool // Neverf asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // assert.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index d8300af73..d06af8dc0 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -325,6 +325,10 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool { // Eventually asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { @@ -342,6 +346,12 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// and the exit was not via 'collect.FailNow()', the assertion fails immediately. +// This usually means that the condition called t.FailNow() on the outer 't'. +// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to +// only fail the current tick. +// // externalValue := false // go func() { // time.Sleep(8*time.Second) @@ -367,6 +377,12 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// and the exit was not via 'collect.FailNow()', the assertion fails immediately. +// This usually means that the condition called t.FailNow() on the outer 't'. +// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to +// only fail the current tick. +// // externalValue := false // go func() { // time.Sleep(8*time.Second) @@ -386,6 +402,10 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor // Eventuallyf asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") 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 { @@ -1039,6 +1059,10 @@ func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) b // Never asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { @@ -1050,6 +1074,10 @@ func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick ti // Neverf asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") 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 { diff --git a/assert/assertions.go b/assert/assertions.go index 6950636d3..ed892a45c 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -13,6 +13,7 @@ import ( "runtime" "runtime/debug" "strings" + "sync/atomic" "time" "unicode" "unicode/utf8" @@ -2003,14 +2004,34 @@ type tHelper = interface { // Eventually asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - ch := make(chan bool, 1) - checkCond := func() { ch <- condition() } + const ( + conditionExitedUnexpectedly = iota + conditionReturnedTrue + conditionReturnedFalse + ) + + resultCh := make(chan int, 1) + checkCond := func() { + result := conditionExitedUnexpectedly + defer func() { + resultCh <- result + }() + if condition() { + result = conditionReturnedTrue + } else { + result = conditionReturnedFalse + } + } timer := time.NewTimer(waitFor) defer timer.Stop() @@ -2030,11 +2051,20 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t case <-tickC: tickC = nil go checkCond() - case v := <-ch: - if v { + case result := <-resultCh: + switch result { + case conditionExitedUnexpectedly: + // Condition exited via [runtime.Goexit]. This usually means + // that the condition called t.FailNow() on the outer 't'. + return Fail(t, "Condition exited unexpectedly", msgAndArgs...) + case conditionReturnedTrue: return true + case conditionReturnedFalse: + // All good, continue checking. + fallthrough + default: + tickC = ticker.C } - tickC = ticker.C } } } @@ -2045,6 +2075,10 @@ type CollectT struct { // If it's non-nil but len(c.errors) == 0, this is also a failure // obtained by direct c.FailNow() call. errors []error + + // exited is set to true if FailNow was called to indicate that the test + // exited correctly via runtime.Goexit. + exited bool } // Helper is like [testing.T.Helper] but does nothing. @@ -2058,6 +2092,7 @@ func (c *CollectT) Errorf(format string, args ...interface{}) { // FailNow stops execution by calling runtime.Goexit. func (c *CollectT) FailNow() { c.fail() + c.exited = true runtime.Goexit() } @@ -2090,6 +2125,12 @@ func (c *CollectT) failed() bool { // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// and the exit was not via 'collect.FailNow()', the assertion fails immediately. +// This usually means that the condition called t.FailNow() on the outer 't'. +// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to +// only fail the current tick. +// // externalValue := false // go func() { // time.Sleep(8*time.Second) @@ -2104,15 +2145,51 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time h.Helper() } - var lastFinishedTickErrs []error - ch := make(chan *CollectT, 1) + const ( + conditionExitedUnexpectedly = iota + conditionFailed + conditionSucceeded + ) + + var lastFinishedTickErrs atomic.Value // of []error + ch := make(chan int, 1) checkCond := func() { + result := conditionExitedUnexpectedly collect := new(CollectT) defer func() { - ch <- collect + // At this point, the condition has returned or exited. It is safe + // to check collect.errors and collect.exited, unless the user has + // created additional goroutines that access 'collect', which would + // be a misuse and is not supported. + if collect.exited { + // Condition exited via [CollectT.FailNow], which is a regular + // way to fail the condition early and exit the goroutine. + result = conditionFailed + } + // Keep the collected tick errors, so that they can be copied to 't' + // when timeout is reached or there is an unexpected exit. + // Always store the actual value of collect.errors, even if nil + lastFinishedTickErrs.Store(collect.errors) + + ch <- result }() condition(collect) + if collect.failed() { + result = conditionFailed + } else { + result = conditionSucceeded + } + } + + copyLastFinishedTickErrs := func() { + errs, ok := lastFinishedTickErrs.Load().([]error) + if !ok { + return + } + for _, err := range errs { + t.Errorf("%v", err) + } } timer := time.NewTimer(waitFor) @@ -2129,20 +2206,26 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time for { select { case <-timer.C: - for _, err := range lastFinishedTickErrs { - t.Errorf("%v", err) - } + copyLastFinishedTickErrs() return Fail(t, "Condition never satisfied", msgAndArgs...) case <-tickC: tickC = nil go checkCond() - case collect := <-ch: - if !collect.failed() { + case result := <-ch: + switch result { + case conditionExitedUnexpectedly: + // Condition exited via [runtime.Goexit]. This usually means + // that the condition called t.FailNow() on the outer 't'. + copyLastFinishedTickErrs() + return Fail(t, "Condition exited unexpectedly", msgAndArgs...) + case conditionSucceeded: return true + case conditionFailed: + // All good, continue checking. + fallthrough + default: + tickC = ticker.C } - // Keep the errors from the last ended condition, so that they can be copied to t if timeout is reached. - lastFinishedTickErrs = collect.errors - tickC = ticker.C } } } @@ -2150,14 +2233,34 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time // Never asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - ch := make(chan bool, 1) - checkCond := func() { ch <- condition() } + const ( + conditionExitedUnexpectedly = iota + conditionReturnedTrue + conditionReturnedFalse + ) + + ch := make(chan int, 1) + checkCond := func() { + result := conditionExitedUnexpectedly + defer func() { + ch <- result + }() + if condition() { + result = conditionReturnedTrue + } else { + result = conditionReturnedFalse + } + } timer := time.NewTimer(waitFor) defer timer.Stop() @@ -2177,11 +2280,20 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D case <-tickC: tickC = nil go checkCond() - case v := <-ch: - if v { + case result := <-ch: + switch result { + case conditionExitedUnexpectedly: + // Condition exited via [runtime.Goexit]. This usually means + // that the condition called t.FailNow() on the outer 't'. + return Fail(t, "Condition exited unexpectedly", msgAndArgs...) + case conditionReturnedTrue: return Fail(t, "Condition satisfied", msgAndArgs...) + case conditionReturnedFalse: + // All good, continue checking. + fallthrough + default: + tickC = ticker.C } - tickC = ticker.C } } } diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 4975f5e41..c1f063ccf 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -3450,6 +3450,24 @@ func TestEventuallyTrue(t *testing.T) { True(t, Eventually(t, condition, 100*time.Millisecond, 20*time.Millisecond)) } +func TestEventuallyFailNow(t *testing.T) { + t.Parallel() + + mockT := new(CollectT) + state := 0 + condition := func() bool { + state += 1 + mockT.FailNow() + panic("unreachable") + } + + False(t, Eventually(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) + True(t, mockT.failed()) + Len(t, mockT.errors, 1) + ErrorContains(t, mockT.errors[0], "Condition exited unexpectedly") + Equal(t, 1, state, "Expected condition to be called exactly once") +} + // errorsCapturingT is a mock implementation of TestingT that captures errors reported with Errorf. type errorsCapturingT struct { errors []error @@ -3467,12 +3485,16 @@ func TestEventuallyWithTFalse(t *testing.T) { mockT := new(errorsCapturingT) + count := 0 + condition := func(collect *CollectT) { + count += 1 Fail(collect, "condition fixed failure") } False(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) Len(t, mockT.errors, 2) + Greater(t, count, 3, "Condition is expected to be called multiple times") } func TestEventuallyWithTTrue(t *testing.T) { @@ -3534,13 +3556,35 @@ func TestEventuallyWithTFailNow(t *testing.T) { t.Parallel() mockT := new(CollectT) + count := 0 condition := func(collect *CollectT) { + count += 1 collect.FailNow() + panic("unreachable") + } + + False(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) + Len(t, mockT.errors, 1) + Greater(t, count, 3, "Expected condition to be called multiple times") +} + +func TestEventuallyWithTOuterFailNow(t *testing.T) { + t.Parallel() + + mockT := new(CollectT) + count := 0 + condition := func(collect *CollectT) { + count += 1 + mockT.FailNow() + panic("unreachable") } False(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) + True(t, mockT.failed()) Len(t, mockT.errors, 1) + ErrorContains(t, mockT.errors[0], "Condition exited unexpectedly") + Equal(t, 1, count, "Expected condition to be called exactly once") } // Check that a long running condition doesn't block Eventually. @@ -3634,6 +3678,24 @@ func TestNeverFailQuickly(t *testing.T) { False(t, Never(mockT, condition, 100*time.Millisecond, time.Second)) } +func TestNeverFailNow(t *testing.T) { + t.Parallel() + + mockT := new(CollectT) + counter := 0 + condition := func() bool { + counter += 1 + mockT.FailNow() + panic("unreachable") + } + + False(t, Never(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) + True(t, mockT.failed(), "Expected Never to mark TestingT as failed") + Len(t, mockT.errors, 1) + ErrorContains(t, mockT.errors[0], "Condition exited unexpectedly") + Equal(t, 1, counter, "Expected condition to be called exactly once") +} + func Test_validateEqualArgs(t *testing.T) { t.Parallel() diff --git a/require/require.go b/require/require.go index 23a3be780..32409c088 100644 --- a/require/require.go +++ b/require/require.go @@ -404,6 +404,10 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) { // Eventually asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // require.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { @@ -424,6 +428,12 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// and the exit was not via 'collect.FailNow()', the assertion fails immediately. +// This usually means that the condition called t.FailNow() on the outer 't'. +// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to +// only fail the current tick. +// // externalValue := false // go func() { // time.Sleep(8*time.Second) @@ -452,6 +462,12 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// and the exit was not via 'collect.FailNow()', the assertion fails immediately. +// This usually means that the condition called t.FailNow() on the outer 't'. +// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to +// only fail the current tick. +// // externalValue := false // go func() { // time.Sleep(8*time.Second) @@ -474,6 +490,10 @@ func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), wait // Eventuallyf asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // require.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1310,6 +1330,10 @@ func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) { // Never asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // require.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1324,6 +1348,10 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D // Neverf asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // require.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") func Neverf(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 38d985a55..039ac6e7e 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -326,6 +326,10 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) { // Eventually asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { @@ -343,6 +347,12 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// and the exit was not via 'collect.FailNow()', the assertion fails immediately. +// This usually means that the condition called t.FailNow() on the outer 't'. +// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to +// only fail the current tick. +// // externalValue := false // go func() { // time.Sleep(8*time.Second) @@ -368,6 +378,12 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// and the exit was not via 'collect.FailNow()', the assertion fails immediately. +// This usually means that the condition called t.FailNow() on the outer 't'. +// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to +// only fail the current tick. +// // externalValue := false // go func() { // time.Sleep(8*time.Second) @@ -387,6 +403,10 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), // Eventuallyf asserts that given condition will be met in waitFor time, // periodically checking target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { @@ -1040,6 +1060,10 @@ func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) { // Never asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { @@ -1051,6 +1075,10 @@ func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick ti // Neverf asserts that the given condition doesn't satisfy in waitFor time, // periodically checking the target function each tick. // +// If the condition does not return normally, but instead calls [runtime.Goexit], +// the assertion fails immediately. This usually means that the condition called +// t.FailNow() on the outer 't'. +// // a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") func (a *Assertions) Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { diff --git a/require/requirements_test.go b/require/requirements_test.go index 7cb63a554..df4737346 100644 --- a/require/requirements_test.go +++ b/require/requirements_test.go @@ -3,6 +3,7 @@ package require import ( "encoding/json" "errors" + "runtime" "testing" "time" @@ -788,3 +789,22 @@ func TestEventuallyWithTTrue(t *testing.T) { False(t, mockT.Failed, "Check should pass") Equal(t, 2, counter, "Condition is expected to be called 2 times") } + +func TestEventuallyWithTOuterFailNow(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + + counter := 0 + condition := func(collect *assert.CollectT) { + counter += 1 + + // Simulate [testing.T.FailNow] on the outer 't' by calling + // runtime.Goexit() manually. + runtime.Goexit() + } + + EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond) + True(t, mockT.Failed, "Check should fail") + Equal(t, 1, counter, "Condition is expected to be called once") +}