From 0213fa4a4c346f0c325f8ecaf53c10340b8767cf Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Tue, 14 Oct 2025 09:11:11 +0200 Subject: [PATCH 01/16] do not hang on failed condition --- assert/assertions.go | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/assert/assertions.go b/assert/assertions.go index a27e70546..12e8696e9 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -2010,8 +2010,22 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t h.Helper() } - ch := make(chan bool, 1) - checkCond := func() { ch <- condition() } + const failed = 0 + const stop = 1 + const noStop = 2 + + resultCh := make(chan int, 1) + checkCond := func() { + result := failed + defer func() { + resultCh <- result + }() + if condition() { + result = stop + } else { + result = noStop + } + } timer := time.NewTimer(waitFor) defer timer.Stop() @@ -2019,6 +2033,7 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t ticker := time.NewTicker(tick) defer ticker.Stop() + // Use a nillable channel to control ticks. var tickC <-chan time.Time // Check the condition once first on the initial call. @@ -2029,13 +2044,23 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t case <-timer.C: return Fail(t, "Condition never satisfied", msgAndArgs...) case <-tickC: - tickC = nil - go checkCond() - case v := <-ch: - if v { + tickC = nil // Do not check again until we get a result. + go checkCond() // Schedule the next check. + case v := <-resultCh: + switch v { + case failed: + // Conditon pannicked or test failed and finished. + // Cannot determine correct result. + // Cannot decide if we should continue or not. + // Stop here and now, and mark test as failed. + return Fail(t, "Condition aborted") + case stop: return true + case noStop: + fallthrough + default: + tickC = ticker.C // Enable ticks to check again. } - tickC = ticker.C } } } From 73ecd5785cd8ba7100b9a1b11cd018ea58b63e18 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Tue, 14 Oct 2025 14:16:49 +0200 Subject: [PATCH 02/16] use same error message as timeout --- assert/assertions.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/assert/assertions.go b/assert/assertions.go index 12e8696e9..558201ff5 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -2049,11 +2049,12 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t case v := <-resultCh: switch v { case failed: - // Conditon pannicked or test failed and finished. + // Condition panicked or test failed and finished. // Cannot determine correct result. - // Cannot decide if we should continue or not. - // Stop here and now, and mark test as failed. - return Fail(t, "Condition aborted") + // Cannot decide if we should continue gracefully or not. + // We can stop here and now, and mark test as failed with + // the same error message as the timeout case. + return Fail(t, "Condition never satisfied", msgAndArgs...) case stop: return true case noStop: From 88c707954fa64ac6db1ba7a0dc224a4bc168c02d Mon Sep 17 00:00:00 2001 From: Uwe Jugel <532284+ubunatic@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:55:48 +0200 Subject: [PATCH 03/16] Update assert/assertions.go Co-authored-by: Bracken --- assert/assertions.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/assert/assertions.go b/assert/assertions.go index 558201ff5..d66b95608 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -2010,9 +2010,11 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t h.Helper() } - const failed = 0 - const stop = 1 - const noStop = 2 + const ( + failed = iota + stop + noStop + ) resultCh := make(chan int, 1) checkCond := func() { From 4a05215b01d2e98c995279e9a465878b16d88676 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Sun, 19 Oct 2025 02:19:08 +0200 Subject: [PATCH 04/16] update codegen to support links, use replacer --- _codegen/main.go | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/_codegen/main.go b/_codegen/main.go index 574985181..38a013249 100644 --- a/_codegen/main.go +++ b/_codegen/main.go @@ -285,11 +285,29 @@ func (f *testFunc) Comment() string { } func (f *testFunc) CommentFormat() string { - search := fmt.Sprintf("%s", f.DocInfo.Name) - replace := fmt.Sprintf("%sf", f.DocInfo.Name) - comment := strings.Replace(f.Comment(), search, replace, -1) - exp := regexp.MustCompile(replace + `\(((\(\)|[^\n])+)\)`) - return exp.ReplaceAllString(comment, replace+`($1, "error message %s", "formatted")`) + name := f.DocInfo.Name + nameF := name + "f" + comment := f.Comment() + + // 1. Best effort replacer for mentions, calls, etc. + // that can preserve references to the original function. + bestEffortReplacer := strings.NewReplacer( + "["+name+"]", "["+name+"]", // ref to origin func, keep as is + "["+nameF+"]", "["+nameF+"]", // ref to format func code, keep as is + name+" ", nameF+" ", // mention in text -> replace + name+"(", nameF+"(", // function call -> replace + name+",", nameF+",", // mention enumeration -> replace + name+".", nameF+".", // closure of sentence -> replace + name+"\n", nameF+"\n", // end of line -> replace + ) + + // 2. Regex for (replaced) function calls + callExp := regexp.MustCompile(nameF + `\(((\(\)|[^\n])+)\)`) + + comment = bestEffortReplacer.Replace(comment) + comment = callExp.ReplaceAllString(comment, nameF+`($1, "error message %s", "formatted")`) + + return comment } func (f *testFunc) CommentWithoutT(receiver string) string { From 92d1656c2f4082b698b46a6ce48d746ae4ad353b Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Sun, 19 Oct 2025 02:20:19 +0200 Subject: [PATCH 05/16] defer to avoid potential channel block --- mock/mock_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mock/mock_test.go b/mock/mock_test.go index 3dc9e0b1e..7845f36b5 100644 --- a/mock/mock_test.go +++ b/mock/mock_test.go @@ -1342,7 +1342,11 @@ func Test_Mock_Called(t *testing.T) { } func asyncCall(m *Mock, ch chan Arguments) { - ch <- m.Called(1, 2, 3) + var args Arguments + defer func() { + ch <- args + }() + args = m.Called(1, 2, 3) } func Test_Mock_Called_blocks(t *testing.T) { From 58662c23982b19a8e71cef6ddf5b365e6067ddf3 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Sun, 19 Oct 2025 02:21:15 +0200 Subject: [PATCH 06/16] catch runtime.Goexit, add comments --- assert/assertion_format.go | 120 +++++++++++++--- assert/assertion_forward.go | 240 ++++++++++++++++++++++++++----- assert/assertions.go | 226 +++++++++++++++++++++++------ assert/assertions_exit_test.go | 250 +++++++++++++++++++++++++++++++++ require/require.go | 240 ++++++++++++++++++++++++++----- require/require_forward.go | 240 ++++++++++++++++++++++++++----- 6 files changed, 1148 insertions(+), 168 deletions(-) create mode 100644 assert/assertions_exit_test.go diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 2d089991a..0c348467b 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -165,10 +165,58 @@ func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface return ErrorIs(t, err, target, append([]interface{}{msg}, args...)...) } -// Eventuallyf asserts that given condition will be met in waitFor time, -// periodically checking target function each tick. +// Eventuallyf asserts that the given condition will be met in waitFor time, +// periodically checking result and completion of the target function each tick. +// If the condition is not met, the test fails with "Condition never satisfied". +// +// ⚠️ A condition function may exit unexpectedly, which is a common pitfall, +// since [Eventually] runs the condition function in a separate goroutine. +// An unexpected exit happens in the following cases: +// +// 1. The condition function panics. In this case the entire test will panic +// immediately and exit. This is normal Go runtime behavior and not +// specific to the testing framework. Condition panics are currently not +// recovered by [Eventually]. +// +// 2. The condition function calls [runtime.Goexit], which exits the goroutine +// without panicking. In this case the test fails immediately with +// "Condition exited unexpectedly". This is new behavior since v1.X.X. +// +// Note that [runtime.Goexit] is called by t.FailNow() and thus by all failing +// 'require' functions. You can call [require.Fail] and similar requirements +// inside the condition, to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// Also see [EventuallyWithT] for a version that allows using assertions in the +// condition function instead of returning a simple boolean value. +// +// Eventuallyf is often used to check conditions against values that are set by +// other goroutines. In such cases, always use thread-safe variables. +// It is also recommended to run 'go test' with the '-race' flag to detect +// race conditions in your tests and code. The following example demonstrates +// the correct usage of Eventuallyf with a thread-safe variable, including a +// call to a 'require' function inside the condition function to fail the test +// immediately on error: +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(time.Second) +// externalValue.Store(true) +// }() +// +// assert.Eventuallyf(t, func(, "error message %s", "formatted") bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error // -// assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// return gotValue +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -176,24 +224,51 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick return Eventually(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 -// function can use the CollectT to call other assertions. -// The condition is considered "met" if no errors are raised in a tick. -// The supplied CollectT collects all errors from one tick (if there are any). -// If the condition is not met before waitFor, the collected errors of -// the last tick are copied to t. +// EventuallyWithTf asserts that the given condition will be met in waitFor +// time, periodically checking the success of target function each tick. +// In contrast to [Eventually], it supplies a [CollectT] to the condition +// function that the condition function can use to call assertions on. +// These assertions are specific to the condition run in one tick. +// +// The supplied [CollectT] collects all errors from one tick. If no errors are +// collected, the condition is considered successful ("met") and EventuallyWithTf +// returns true. If there are collected errors, the condition is considered +// failed for that tick ("not met") and the next tick is scheduled until +// waitFor duration is reached. +// +// If the condition does not complete successfully before waitFor expires, the +// collected errors of the last tick are copied to t before EventuallyWithTf +// fails the test with "Condition never satisfied" and returns false. +// +// If the condition exits unexpectedly, a corresponding error: +// "Condition exited unexpectedly" is collected for that tick. // -// externalValue := false +// ⚠️ See [Eventually] for more details about unexpected exits, which are a +// common pitfall when using 'require' functions inside condition functions. +// +// Since version 1.X.X, You can call [require.Fail] and similar requirements +// inside the condition to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} // go func() { -// time.Sleep(8*time.Second) -// externalValue = true +// time.Sleep(time.Second) +// externalValue.Store(true) // }() -// assert.EventuallyWithTf(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") +// assert.EventuallyWithTf(t, func(collect *assert.CollectT, "error message %s", "formatted") { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // Use assertions with 'collect' and not with 't', so they are scoped to the current tick. +// assert.True(collect, gotValue, "expected 'externalValue' to become true") +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -525,6 +600,15 @@ 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. // +// Since version 1.X.X, if the condition exits unexpectedly, this is treated as +// a failure and the test fails immediately with: "Condition exited unexpectedly". +// Before version 1.X.X, unexpected exits lead to a blocked channel and a falsely +// passing [Never]. See [Eventually] for more details about unexpected exits. +// +// You can call [require.Fail] and similar requirements inside the condition +// to fail the test immediately. The blocking behavior from before version 1.X.X +// prevented this. Now it works as expected. Please adapt your tests accordingly. +// // 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..4d986a971 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -322,10 +322,58 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool { return Errorf(a.t, err, msg, args...) } -// Eventually asserts that given condition will be met in waitFor time, -// periodically checking target function each tick. +// Eventually asserts that the given condition will be met in waitFor time, +// periodically checking result and completion of the target function each tick. +// If the condition is not met, the test fails with "Condition never satisfied". +// +// ⚠️ A condition function may exit unexpectedly, which is a common pitfall, +// since [Eventually] runs the condition function in a separate goroutine. +// An unexpected exit happens in the following cases: +// +// 1. The condition function panics. In this case the entire test will panic +// immediately and exit. This is normal Go runtime behavior and not +// specific to the testing framework. Condition panics are currently not +// recovered by [Eventually]. +// +// 2. The condition function calls [runtime.Goexit], which exits the goroutine +// without panicking. In this case the test fails immediately with +// "Condition exited unexpectedly". This is new behavior since v1.X.X. +// +// Note that [runtime.Goexit] is called by t.FailNow() and thus by all failing +// 'require' functions. You can call [require.Fail] and similar requirements +// inside the condition, to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// Also see [EventuallyWithT] for a version that allows using assertions in the +// condition function instead of returning a simple boolean value. +// +// Eventually is often used to check conditions against values that are set by +// other goroutines. In such cases, always use thread-safe variables. +// It is also recommended to run 'go test' with the '-race' flag to detect +// race conditions in your tests and code. The following example demonstrates +// the correct usage of Eventually with a thread-safe variable, including a +// call to a 'require' function inside the condition function to fail the test +// immediately on error: +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(time.Second) +// externalValue.Store(true) +// }() +// +// a.Eventually(func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// return gotValue // -// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -333,24 +381,51 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti return Eventually(a.t, condition, waitFor, tick, msgAndArgs...) } -// 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 -// function can use the CollectT to call other assertions. -// The condition is considered "met" if no errors are raised in a tick. -// The supplied CollectT collects all errors from one tick (if there are any). -// If the condition is not met before waitFor, the collected errors of -// the last tick are copied to t. +// EventuallyWithT asserts that the given condition will be met in waitFor +// time, periodically checking the success of target function each tick. +// In contrast to [Eventually], it supplies a [CollectT] to the condition +// function that the condition function can use to call assertions on. +// These assertions are specific to the condition run in one tick. +// +// The supplied [CollectT] collects all errors from one tick. If no errors are +// collected, the condition is considered successful ("met") and EventuallyWithT +// returns true. If there are collected errors, the condition is considered +// failed for that tick ("not met") and the next tick is scheduled until +// waitFor duration is reached. +// +// If the condition does not complete successfully before waitFor expires, the +// collected errors of the last tick are copied to t before EventuallyWithT +// fails the test with "Condition never satisfied" and returns false. +// +// If the condition exits unexpectedly, a corresponding error: +// "Condition exited unexpectedly" is collected for that tick. +// +// ⚠️ See [Eventually] for more details about unexpected exits, which are a +// common pitfall when using 'require' functions inside condition functions. +// +// Since version 1.X.X, You can call [require.Fail] and similar requirements +// inside the condition to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. // -// externalValue := false +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} // go func() { -// time.Sleep(8*time.Second) -// externalValue = true +// time.Sleep(time.Second) +// externalValue.Store(true) // }() -// a.EventuallyWithT(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") +// a.EventuallyWithT(func(collect *assert.CollectT) { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // Use assertions with 'collect' and not with 't', so they are scoped to the current tick. +// assert.True(collect, gotValue, "expected 'externalValue' to become true") +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -358,24 +433,51 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor return EventuallyWithT(a.t, condition, waitFor, tick, msgAndArgs...) } -// 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 -// function can use the CollectT to call other assertions. -// The condition is considered "met" if no errors are raised in a tick. -// The supplied CollectT collects all errors from one tick (if there are any). -// If the condition is not met before waitFor, the collected errors of -// the last tick are copied to t. +// EventuallyWithTf asserts that the given condition will be met in waitFor +// time, periodically checking the success of target function each tick. +// In contrast to [Eventually], it supplies a [CollectT] to the condition +// function that the condition function can use to call assertions on. +// These assertions are specific to the condition run in one tick. +// +// The supplied [CollectT] collects all errors from one tick. If no errors are +// collected, the condition is considered successful ("met") and EventuallyWithTf +// returns true. If there are collected errors, the condition is considered +// failed for that tick ("not met") and the next tick is scheduled until +// waitFor duration is reached. +// +// If the condition does not complete successfully before waitFor expires, the +// collected errors of the last tick are copied to t before EventuallyWithTf +// fails the test with "Condition never satisfied" and returns false. // -// externalValue := false +// If the condition exits unexpectedly, a corresponding error: +// "Condition exited unexpectedly" is collected for that tick. +// +// ⚠️ See [Eventually] for more details about unexpected exits, which are a +// common pitfall when using 'require' functions inside condition functions. +// +// Since version 1.X.X, You can call [require.Fail] and similar requirements +// inside the condition to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} // go func() { -// time.Sleep(8*time.Second) -// externalValue = true +// time.Sleep(time.Second) +// externalValue.Store(true) // }() -// a.EventuallyWithTf(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") +// a.EventuallyWithTf(func(collect *assert.CollectT, "error message %s", "formatted") { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // Use assertions with 'collect' and not with 't', so they are scoped to the current tick. +// assert.True(collect, gotValue, "expected 'externalValue' to become true") +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -383,10 +485,58 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor return EventuallyWithTf(a.t, condition, waitFor, tick, msg, args...) } -// Eventuallyf asserts that given condition will be met in waitFor time, -// periodically checking target function each tick. +// Eventuallyf asserts that the given condition will be met in waitFor time, +// periodically checking result and completion of the target function each tick. +// If the condition is not met, the test fails with "Condition never satisfied". +// +// ⚠️ A condition function may exit unexpectedly, which is a common pitfall, +// since [Eventually] runs the condition function in a separate goroutine. +// An unexpected exit happens in the following cases: +// +// 1. The condition function panics. In this case the entire test will panic +// immediately and exit. This is normal Go runtime behavior and not +// specific to the testing framework. Condition panics are currently not +// recovered by [Eventually]. +// +// 2. The condition function calls [runtime.Goexit], which exits the goroutine +// without panicking. In this case the test fails immediately with +// "Condition exited unexpectedly". This is new behavior since v1.X.X. +// +// Note that [runtime.Goexit] is called by t.FailNow() and thus by all failing +// 'require' functions. You can call [require.Fail] and similar requirements +// inside the condition, to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// Also see [EventuallyWithT] for a version that allows using assertions in the +// condition function instead of returning a simple boolean value. +// +// Eventuallyf is often used to check conditions against values that are set by +// other goroutines. In such cases, always use thread-safe variables. +// It is also recommended to run 'go test' with the '-race' flag to detect +// race conditions in your tests and code. The following example demonstrates +// the correct usage of Eventuallyf with a thread-safe variable, including a +// call to a 'require' function inside the condition function to fail the test +// immediately on error: +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(time.Second) +// externalValue.Store(true) +// }() +// +// a.Eventuallyf(func(, "error message %s", "formatted") bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// return gotValue // -// a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -1039,6 +1189,15 @@ 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. // +// Since version 1.X.X, if the condition exits unexpectedly, this is treated as +// a failure and the test fails immediately with: "Condition exited unexpectedly". +// Before version 1.X.X, unexpected exits lead to a blocked channel and a falsely +// passing [Never]. See [Eventually] for more details about unexpected exits. +// +// You can call [require.Fail] and similar requirements inside the condition +// to fail the test immediately. The blocking behavior from before version 1.X.X +// prevented this. Now it works as expected. Please adapt your tests accordingly. +// // 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 +1209,15 @@ 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. // +// Since version 1.X.X, if the condition exits unexpectedly, this is treated as +// a failure and the test fails immediately with: "Condition exited unexpectedly". +// Before version 1.X.X, unexpected exits lead to a blocked channel and a falsely +// passing [Never]. See [Eventually] for more details about unexpected exits. +// +// You can call [require.Fail] and similar requirements inside the condition +// to fail the test immediately. The blocking behavior from before version 1.X.X +// prevented this. Now it works as expected. Please adapt your tests accordingly. +// // 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 d66b95608..4c747131f 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -2001,31 +2001,82 @@ type tHelper = interface { Helper() } -// Eventually asserts that given condition will be met in waitFor time, -// periodically checking target function each tick. +// Eventually asserts that the given condition will be met in waitFor time, +// periodically checking result and completion of the target function each tick. +// If the condition is not met, the test fails with "Condition never satisfied". // -// assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +// ⚠️ A condition function may exit unexpectedly, which is a common pitfall, +// since [Eventually] runs the condition function in a separate goroutine. +// An unexpected exit happens in the following cases: +// +// 1. The condition function panics. In this case the entire test will panic +// immediately and exit. This is normal Go runtime behavior and not +// specific to the testing framework. Condition panics are currently not +// recovered by [Eventually]. +// +// 2. The condition function calls [runtime.Goexit], which exits the goroutine +// without panicking. In this case the test fails immediately with +// "Condition exited unexpectedly". This is new behavior since v1.X.X. +// +// Note that [runtime.Goexit] is called by t.FailNow() and thus by all failing +// 'require' functions. You can call [require.Fail] and similar requirements +// inside the condition, to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// Also see [EventuallyWithT] for a version that allows using assertions in the +// condition function instead of returning a simple boolean value. +// +// Eventually is often used to check conditions against values that are set by +// other goroutines. In such cases, always use thread-safe variables. +// It is also recommended to run 'go test' with the '-race' flag to detect +// race conditions in your tests and code. The following example demonstrates +// the correct usage of Eventually with a thread-safe variable, including a +// call to a 'require' function inside the condition function to fail the test +// immediately on error: +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(time.Second) +// externalValue.Store(true) +// }() +// +// assert.Eventually(t, func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// return gotValue +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } const ( - failed = iota - stop - noStop + conditionExited = iota + conditionSatisfied + conditionNotSatisfied ) resultCh := make(chan int, 1) + checkCond := func() { - result := failed + result := conditionExited + defer func() { resultCh <- result }() + if condition() { - result = stop + result = conditionSatisfied } else { - result = noStop + result = conditionNotSatisfied } } @@ -2050,16 +2101,17 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t go checkCond() // Schedule the next check. case v := <-resultCh: switch v { - case failed: - // Condition panicked or test failed and finished. - // Cannot determine correct result. - // Cannot decide if we should continue gracefully or not. - // We can stop here and now, and mark test as failed with - // the same error message as the timeout case. - return Fail(t, "Condition never satisfied", msgAndArgs...) - case stop: + case conditionExited: + // The condition function is called in a goroutine. + // If this panics the whole test should fail and exit immediately. + // If we reach here, it means the goroutine has exited unexpectedly but did not panic. + // This can happen if [runtime.Goexit] is called inside the condition function. + // This way of exiting a goroutine is reserved for special cases and should not be used + // in normal conditions. It is used by the testing package to stop tests. + return Fail(t, "Condition exited unexpectedly", msgAndArgs...) + case conditionSatisfied: return true - case noStop: + case conditionNotSatisfied: fallthrough default: tickC = ticker.C // Enable ticks to check again. @@ -2110,38 +2162,83 @@ func (c *CollectT) failed() bool { return c.errors != nil } -// 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 -// function can use the CollectT to call other assertions. -// The condition is considered "met" if no errors are raised in a tick. -// The supplied CollectT collects all errors from one tick (if there are any). -// If the condition is not met before waitFor, the collected errors of -// the last tick are copied to t. +// EventuallyWithT asserts that the given condition will be met in waitFor +// time, periodically checking the success of the condition function each tick. +// In contrast to [Eventually], it supplies a [CollectT] to the condition +// function that the condition function can use to call assertions on. +// These assertions are specific to each run of the condition function in each tick. +// +// The supplied [CollectT] collects all errors from one tick. If no errors are +// collected, the condition is considered successful ("met") and EventuallyWithT +// returns true. If there are collected errors, the condition is considered +// failed for that tick ("not met") and the next tick is scheduled until +// waitFor duration is reached. +// +// If the condition does not complete successfully before waitFor expires, the +// collected errors of the last tick are copied to t before EventuallyWithT +// fails the test with "Condition never satisfied" and returns false. +// +// If the condition exits unexpectedly and NO errors are collected, a call to +// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the +// condition function. In this case, EventuallyWithT fails the test immediately +// with "Condition exited unexpectedly" and returns false. // -// externalValue := false +// πŸ’‘ Tick Assertions vs. Parent Test Assertions +// - Use tick assertions and requirements on the supplied 'collect' and not +// on the parent 't'. Only the last tick's collected errors are copied to 't'. +// - Use parent test requirements on the parent 't' to fail the entire test +// - Do not use assertions on the parent 't', since this would affect all ticks +// and create test noise. +// +// ⚠️ See [Eventually] for more details about unexpected exits, which are a +// common pitfall when using 'require' functions inside condition functions. +// +// Since version 1.X.X, You can call [require.Fail] and similar requirements +// inside the condition to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} // go func() { -// time.Sleep(8*time.Second) -// externalValue = true +// time.Sleep(time.Second) +// externalValue.Store(true) // }() -// assert.EventuallyWithT(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") +// assert.EventuallyWithT(t, func(collect *assert.CollectT) { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // Use assertions with 'collect' and not with 't', so they are scoped to the current tick. +// assert.True(collect, gotValue, "expected 'externalValue' to become true") +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() } + // Track whether the condition exited without collecting errors. + // This is used to detect unexpected exits, such as [runtime.Goexit] + // or a t.FailNow() called on a parent 't' inside the condition + // and not on the supplied 'collect'. This is the path where also + // EventuallyWithT must exit immediately with a failure, just like [Eventually]. + var conditionExitedWithoutCollectingErrors bool var lastFinishedTickErrs []error ch := make(chan *CollectT, 1) checkCond := func() { + returned := false collect := new(CollectT) defer func() { + conditionExitedWithoutCollectingErrors = !returned && !collect.failed() ch <- collect }() condition(collect) + returned = true } timer := time.NewTimer(waitFor) @@ -2166,12 +2263,22 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time tickC = nil go checkCond() case collect := <-ch: - if !collect.failed() { + switch { + case conditionExitedWithoutCollectingErrors: + // See [Eventually] for explanation about unexpected exits. + return Fail(t, "Condition exited unexpectedly", msgAndArgs...) + case !collect.failed(): + // Condition met. return true + case collect.failed(): + fallthrough + default: + // Keep the errors from the last completed condition, + // so that they can be copied to 't' if timeout is reached. + lastFinishedTickErrs = collect.errors + // Keep checking until timeout. + 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 } } } @@ -2179,14 +2286,42 @@ 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. // +// Since version 1.X.X, if the condition exits unexpectedly, this is treated as +// a failure and the test fails immediately with: "Condition exited unexpectedly". +// Before version 1.X.X, unexpected exits lead to a blocked channel and a falsely +// passing [Never]. See [Eventually] for more details about unexpected exits. +// +// You can call [require.Fail] and similar requirements inside the condition +// to fail the test immediately. The blocking behavior from before version 1.X.X +// prevented this. Now it works as expected. Please adapt your tests accordingly. +// // 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 ( + conditionExited = iota + conditionSatisfied + conditionNotSatisfied + ) + + resultCh := make(chan int, 1) + + checkCond := func() { + result := conditionExited + + defer func() { + resultCh <- result + }() + + if condition() { + result = conditionSatisfied + } else { + result = conditionNotSatisfied + } + } timer := time.NewTimer(waitFor) defer timer.Stop() @@ -2206,11 +2341,18 @@ 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 v := <-resultCh: + switch v { + case conditionExited: + // See [Eventually] for explanation about unexpected exits. + return Fail(t, "Condition exited unexpectedly", msgAndArgs...) + case conditionSatisfied: return Fail(t, "Condition satisfied", msgAndArgs...) + case conditionNotSatisfied: + fallthrough + default: + tickC = ticker.C } - tickC = ticker.C } } } diff --git a/assert/assertions_exit_test.go b/assert/assertions_exit_test.go new file mode 100644 index 000000000..c5b7e9dcd --- /dev/null +++ b/assert/assertions_exit_test.go @@ -0,0 +1,250 @@ +package assert + +import ( + "fmt" + "os" + "runtime" + "strings" + "testing" + "time" +) + +func TestEventuallyFailsFast(t *testing.T) { + + type testCase struct { + name string + run func(t TestingT, tc testCase, completed *bool) + fn func(t TestingT) // optional + exit func() + ret bool + expErrors []string + expFail bool + } + + runFnAndExit := func(t TestingT, tc testCase, completed *bool) { + if tc.fn != nil { + tc.fn(t) + } + if tc.exit != nil { + tc.exit() + } + *completed = true + } + + evtl := func(t TestingT, tc testCase, completed *bool) { + Eventually(t, func() bool { + runFnAndExit(t, tc, completed) + return tc.ret + }, time.Hour, time.Millisecond) + } + + withT := func(t TestingT, tc testCase, completed *bool) { + EventuallyWithT(t, func(collect *CollectT) { + runFnAndExit(collect, tc, completed) + }, time.Hour, time.Millisecond) + } + + never := func(t TestingT, tc testCase, completed *bool) { + Never(t, func() bool { + runFnAndExit(t, tc, completed) + return tc.ret + }, time.Hour, time.Millisecond) + } + + doFail := func(t TestingT) { t.Errorf("failed") } + exitErr := "Condition exited unexpectedly" // fail fast err on runtime.Goexit + satisErr := "Condition satisfied" // fail fast err on condition satisfied + failedErr := "failed" // additional error from explicit failure + + cases := []testCase{ + // Fast path Eventually tests + { + name: "Satisfy", run: evtl, + fn: nil, exit: nil, ret: true, // succeed fast + expErrors: nil, expFail: false, // no errors expected + }, + { + name: "Fail", run: evtl, + fn: doFail, exit: nil, ret: true, // fail and succeed fast + expErrors: []string{failedErr}, expFail: true, // expect fail + }, + { + // Simulate [testing.T.FailNow], which calls + // [testing.T.Fail] followed by [runtime.Goexit]. + name: "FailNow", run: evtl, + fn: doFail, exit: runtime.Goexit, ret: false, // no succeed fast, but fail + expErrors: []string{exitErr, failedErr}, expFail: true, // expect both errors + }, + { + name: "Goexit", run: evtl, + fn: nil, exit: runtime.Goexit, ret: false, // no succeed fast, just exit + expErrors: []string{exitErr}, expFail: true, // expect exit error + }, + + // Fast path EventuallyWithT tests + { + name: "SatisfyWithT", run: withT, + fn: nil, exit: nil, ret: true, // succeed fast + expErrors: nil, expFail: false, // no errors expected + }, + { + name: "GoExitWithT", run: withT, + fn: nil, exit: runtime.Goexit, ret: false, // no succeed fast, just exit + expErrors: []string{exitErr}, expFail: true, // expect exit error + }, + // EventuallyWithT only fails fast when no errors are collected. + // The Fail and FailNow cases are thus equivalent and will not fail fast and are not tested here. + + // Fast path Never tests + { + name: "SatisfyNever", run: never, + fn: nil, exit: nil, ret: true, // fail fast by satisfying + expErrors: []string{satisErr}, expFail: true, // expect satisfy error only + }, + { + name: "FailNowNever", run: never, + fn: doFail, exit: runtime.Goexit, ret: false, // no satisfy, but fail + exit + expErrors: []string{exitErr, failedErr}, expFail: true, // expect both errors + }, + { + name: "GoexitNever", run: never, + fn: nil, exit: runtime.Goexit, ret: false, // no satisfy, just exit + expErrors: []string{exitErr}, expFail: true, // expect exit error + }, + { + name: "FailNever", run: never, + fn: doFail, exit: nil, ret: true, // fail then satisfy fast + expErrors: []string{failedErr, satisErr}, expFail: true, // expect fail error + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + collT := &CollectT{} + wait := make(chan struct{}) + completed := false + var panicValue interface{} + + go func() { + defer func() { + panicValue = recover() + close(wait) + }() + tc.run(collT, tc, &completed) + }() + + select { + case <-wait: + case <-time.After(time.Second): + FailNow(t, "test did not complete within timeout") + } + Nil(t, panicValue, "Eventually should not panic") + Equal(t, tc.expFail, collT.failed(), "test state does not match expected failed state") + Len(t, collT.errors, len(tc.expErrors), "number of collected errors does not match expectation") + + Found: + for _, expMsg := range tc.expErrors { + for _, err := range collT.errors { + if strings.Contains(err.Error(), expMsg) { + continue Found + } + } + t.Errorf("expected error message %q not found in collected errors", expMsg) + } + + }) + } +} + +func TestEventuallyCompletes(t *testing.T) { + t.Parallel() + mockT := &mockTestingT{} + Eventually(mockT, func() bool { + return true + }, time.Second, time.Millisecond) + False(t, mockT.Failed(), "test should not fail") + + mockT = &mockTestingT{} + EventuallyWithT(mockT, func(collect *CollectT) { + // no assertion failures + }, time.Millisecond, time.Millisecond) + False(t, mockT.Failed(), "test should not fail") + + mockT = &mockTestingT{} + Never(mockT, func() bool { + return false + }, time.Second, time.Millisecond) + False(t, mockT.Failed(), "test should not fail") +} + +func TestEventuallyHandlesUnexpectedExit(t *testing.T) { + t.Parallel() + collT := &CollectT{} + Eventually(collT, func() bool { + runtime.Goexit() + return false + }, time.Second, time.Millisecond) + True(t, collT.failed(), "test should fail") + Len(t, collT.errors, 1, "should have one error") + Contains(t, collT.errors[0].Error(), "Condition exited unexpectedly") + + collT = &CollectT{} + EventuallyWithT(collT, func(collect *CollectT) { + runtime.Goexit() + }, time.Second, time.Millisecond) + True(t, collT.failed(), "test should fail") + Len(t, collT.errors, 1, "should have one error") + Contains(t, collT.errors[0].Error(), "Condition exited unexpectedly") + + collT = &CollectT{} + Never(collT, func() bool { + runtime.Goexit() + return true + }, time.Second, time.Millisecond) + True(t, collT.failed(), "test should fail") + Len(t, collT.errors, 1, "should have one error") + Contains(t, collT.errors[0].Error(), "Condition exited unexpectedly") +} + +func TestPanicInEventuallyStaysUnrecoverable(t *testing.T) { + testPanicUnrecoverable(t, func() { + Eventually(t, func() bool { + panic("demo panic") + }, time.Minute, time.Millisecond) + }) +} + +func TestPanicInEventuallyWithTStaysUnrecoverable(t *testing.T) { + testPanicUnrecoverable(t, func() { + EventuallyWithT(t, func(collect *CollectT) { + panic("demo panic") + }, time.Minute, time.Millisecond) + }) +} + +// testPanicUnrecoverable ensures current goroutine panic behavior. +// +// Currently, [Eventually] runs the condition function in a separate goroutine. +// If that goroutine panics, the panic is not recovered, and the entire test process +// is terminated. +// +// In the future, this behavior may change so that panics in the condition are caught +// and handled more gracefully. For now we ensure such panics to stay unrecoverable. +// +// To run this test, set the environment variable TestPanic=1. +// The test is successful if it panics and fails the test process and does NOT print +// "UNREACHABLE CODE!" after the initial log messages. +func testPanicUnrecoverable(t *testing.T, f func()) { + if os.Getenv("TestPanic") == "" { + t.Skip("Skipping test, set TestPanic=1 to run") + } + t.Log("This test must fail by a panic in a goroutine.") + t.Log("If you see the text 'UNREACHABLE CODE!' after this point, this means the test exited in an unintended way") + defer func() { + // defer statements are not run when a goroutine panics, so this code is + // only reachable if the panic was somehow recovered. + fmt.Println("❌ UNREACHABLE CODE!") + fmt.Println("❌ If you see this, the test has not failed as expected.") + }() + f() +} diff --git a/require/require.go b/require/require.go index 23a3be780..99c84cb59 100644 --- a/require/require.go +++ b/require/require.go @@ -401,10 +401,58 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) { t.FailNow() } -// Eventually asserts that given condition will be met in waitFor time, -// periodically checking target function each tick. +// Eventually asserts that the given condition will be met in waitFor time, +// periodically checking result and completion of the target function each tick. +// If the condition is not met, the test fails with "Condition never satisfied". +// +// ⚠️ A condition function may exit unexpectedly, which is a common pitfall, +// since [Eventually] runs the condition function in a separate goroutine. +// An unexpected exit happens in the following cases: +// +// 1. The condition function panics. In this case the entire test will panic +// immediately and exit. This is normal Go runtime behavior and not +// specific to the testing framework. Condition panics are currently not +// recovered by [Eventually]. +// +// 2. The condition function calls [runtime.Goexit], which exits the goroutine +// without panicking. In this case the test fails immediately with +// "Condition exited unexpectedly". This is new behavior since v1.X.X. +// +// Note that [runtime.Goexit] is called by t.FailNow() and thus by all failing +// 'require' functions. You can call [require.Fail] and similar requirements +// inside the condition, to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// Also see [EventuallyWithT] for a version that allows using assertions in the +// condition function instead of returning a simple boolean value. +// +// Eventually is often used to check conditions against values that are set by +// other goroutines. In such cases, always use thread-safe variables. +// It is also recommended to run 'go test' with the '-race' flag to detect +// race conditions in your tests and code. The following example demonstrates +// the correct usage of Eventually with a thread-safe variable, including a +// call to a 'require' function inside the condition function to fail the test +// immediately on error: +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(time.Second) +// externalValue.Store(true) +// }() +// +// require.Eventually(t, func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// return gotValue // -// require.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -415,24 +463,51 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t 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 -// function can use the CollectT to call other assertions. -// The condition is considered "met" if no errors are raised in a tick. -// The supplied CollectT collects all errors from one tick (if there are any). -// If the condition is not met before waitFor, the collected errors of -// the last tick are copied to t. +// EventuallyWithT asserts that the given condition will be met in waitFor +// time, periodically checking the success of target function each tick. +// In contrast to [Eventually], it supplies a [CollectT] to the condition +// function that the condition function can use to call assertions on. +// These assertions are specific to the condition run in one tick. +// +// The supplied [CollectT] collects all errors from one tick. If no errors are +// collected, the condition is considered successful ("met") and EventuallyWithT +// returns true. If there are collected errors, the condition is considered +// failed for that tick ("not met") and the next tick is scheduled until +// waitFor duration is reached. +// +// If the condition does not complete successfully before waitFor expires, the +// collected errors of the last tick are copied to t before EventuallyWithT +// fails the test with "Condition never satisfied" and returns false. +// +// If the condition exits unexpectedly, a corresponding error: +// "Condition exited unexpectedly" is collected for that tick. +// +// ⚠️ See [Eventually] for more details about unexpected exits, which are a +// common pitfall when using 'require' functions inside condition functions. +// +// Since version 1.X.X, You can call [require.Fail] and similar requirements +// inside the condition to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. // -// externalValue := false +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} // go func() { -// time.Sleep(8*time.Second) -// externalValue = true +// time.Sleep(time.Second) +// externalValue.Store(true) // }() -// require.EventuallyWithT(t, func(c *require.CollectT) { -// // add assertions as needed; any assertion failure will fail the current tick -// require.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// require.EventuallyWithT(t, func(collect *require.CollectT) { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // Use assertions with 'collect' and not with 't', so they are scoped to the current tick. +// require.True(collect, gotValue, "expected 'externalValue' to become true") +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -443,24 +518,51 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF t.FailNow() } -// 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 -// function can use the CollectT to call other assertions. -// The condition is considered "met" if no errors are raised in a tick. -// The supplied CollectT collects all errors from one tick (if there are any). -// If the condition is not met before waitFor, the collected errors of -// the last tick are copied to t. +// EventuallyWithTf asserts that the given condition will be met in waitFor +// time, periodically checking the success of target function each tick. +// In contrast to [Eventually], it supplies a [CollectT] to the condition +// function that the condition function can use to call assertions on. +// These assertions are specific to the condition run in one tick. +// +// The supplied [CollectT] collects all errors from one tick. If no errors are +// collected, the condition is considered successful ("met") and EventuallyWithTf +// returns true. If there are collected errors, the condition is considered +// failed for that tick ("not met") and the next tick is scheduled until +// waitFor duration is reached. +// +// If the condition does not complete successfully before waitFor expires, the +// collected errors of the last tick are copied to t before EventuallyWithTf +// fails the test with "Condition never satisfied" and returns false. // -// externalValue := false +// If the condition exits unexpectedly, a corresponding error: +// "Condition exited unexpectedly" is collected for that tick. +// +// ⚠️ See [Eventually] for more details about unexpected exits, which are a +// common pitfall when using 'require' functions inside condition functions. +// +// Since version 1.X.X, You can call [require.Fail] and similar requirements +// inside the condition to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} // go func() { -// time.Sleep(8*time.Second) -// externalValue = true +// time.Sleep(time.Second) +// externalValue.Store(true) // }() -// require.EventuallyWithTf(t, func(c *require.CollectT, "error message %s", "formatted") { -// // add assertions as needed; any assertion failure will fail the current tick -// require.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// require.EventuallyWithTf(t, func(collect *require.CollectT, "error message %s", "formatted") { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // Use assertions with 'collect' and not with 't', so they are scoped to the current tick. +// require.True(collect, gotValue, "expected 'externalValue' to become true") +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -471,10 +573,58 @@ func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), wait t.FailNow() } -// Eventuallyf asserts that given condition will be met in waitFor time, -// periodically checking target function each tick. +// Eventuallyf asserts that the given condition will be met in waitFor time, +// periodically checking result and completion of the target function each tick. +// If the condition is not met, the test fails with "Condition never satisfied". +// +// ⚠️ A condition function may exit unexpectedly, which is a common pitfall, +// since [Eventually] runs the condition function in a separate goroutine. +// An unexpected exit happens in the following cases: +// +// 1. The condition function panics. In this case the entire test will panic +// immediately and exit. This is normal Go runtime behavior and not +// specific to the testing framework. Condition panics are currently not +// recovered by [Eventually]. +// +// 2. The condition function calls [runtime.Goexit], which exits the goroutine +// without panicking. In this case the test fails immediately with +// "Condition exited unexpectedly". This is new behavior since v1.X.X. +// +// Note that [runtime.Goexit] is called by t.FailNow() and thus by all failing +// 'require' functions. You can call [require.Fail] and similar requirements +// inside the condition, to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// Also see [EventuallyWithT] for a version that allows using assertions in the +// condition function instead of returning a simple boolean value. +// +// Eventuallyf is often used to check conditions against values that are set by +// other goroutines. In such cases, always use thread-safe variables. +// It is also recommended to run 'go test' with the '-race' flag to detect +// race conditions in your tests and code. The following example demonstrates +// the correct usage of Eventuallyf with a thread-safe variable, including a +// call to a 'require' function inside the condition function to fail the test +// immediately on error: +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(time.Second) +// externalValue.Store(true) +// }() +// +// require.Eventuallyf(t, func(, "error message %s", "formatted") bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// return gotValue // -// require.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -1310,6 +1460,15 @@ 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. // +// Since version 1.X.X, if the condition exits unexpectedly, this is treated as +// a failure and the test fails immediately with: "Condition exited unexpectedly". +// Before version 1.X.X, unexpected exits lead to a blocked channel and a falsely +// passing [Never]. See [Eventually] for more details about unexpected exits. +// +// You can call [require.Fail] and similar requirements inside the condition +// to fail the test immediately. The blocking behavior from before version 1.X.X +// prevented this. Now it works as expected. Please adapt your tests accordingly. +// // 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 +1483,15 @@ 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. // +// Since version 1.X.X, if the condition exits unexpectedly, this is treated as +// a failure and the test fails immediately with: "Condition exited unexpectedly". +// Before version 1.X.X, unexpected exits lead to a blocked channel and a falsely +// passing [Never]. See [Eventually] for more details about unexpected exits. +// +// You can call [require.Fail] and similar requirements inside the condition +// to fail the test immediately. The blocking behavior from before version 1.X.X +// prevented this. Now it works as expected. Please adapt your tests accordingly. +// // 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..95359dc9e 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -323,10 +323,58 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) { Errorf(a.t, err, msg, args...) } -// Eventually asserts that given condition will be met in waitFor time, -// periodically checking target function each tick. +// Eventually asserts that the given condition will be met in waitFor time, +// periodically checking result and completion of the target function each tick. +// If the condition is not met, the test fails with "Condition never satisfied". +// +// ⚠️ A condition function may exit unexpectedly, which is a common pitfall, +// since [Eventually] runs the condition function in a separate goroutine. +// An unexpected exit happens in the following cases: +// +// 1. The condition function panics. In this case the entire test will panic +// immediately and exit. This is normal Go runtime behavior and not +// specific to the testing framework. Condition panics are currently not +// recovered by [Eventually]. +// +// 2. The condition function calls [runtime.Goexit], which exits the goroutine +// without panicking. In this case the test fails immediately with +// "Condition exited unexpectedly". This is new behavior since v1.X.X. +// +// Note that [runtime.Goexit] is called by t.FailNow() and thus by all failing +// 'require' functions. You can call [require.Fail] and similar requirements +// inside the condition, to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// Also see [EventuallyWithT] for a version that allows using assertions in the +// condition function instead of returning a simple boolean value. +// +// Eventually is often used to check conditions against values that are set by +// other goroutines. In such cases, always use thread-safe variables. +// It is also recommended to run 'go test' with the '-race' flag to detect +// race conditions in your tests and code. The following example demonstrates +// the correct usage of Eventually with a thread-safe variable, including a +// call to a 'require' function inside the condition function to fail the test +// immediately on error: +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(time.Second) +// externalValue.Store(true) +// }() +// +// a.Eventually(func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// return gotValue // -// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -334,24 +382,51 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti Eventually(a.t, condition, waitFor, tick, msgAndArgs...) } -// 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 -// function can use the CollectT to call other assertions. -// The condition is considered "met" if no errors are raised in a tick. -// The supplied CollectT collects all errors from one tick (if there are any). -// If the condition is not met before waitFor, the collected errors of -// the last tick are copied to t. +// EventuallyWithT asserts that the given condition will be met in waitFor +// time, periodically checking the success of target function each tick. +// In contrast to [Eventually], it supplies a [CollectT] to the condition +// function that the condition function can use to call assertions on. +// These assertions are specific to the condition run in one tick. +// +// The supplied [CollectT] collects all errors from one tick. If no errors are +// collected, the condition is considered successful ("met") and EventuallyWithT +// returns true. If there are collected errors, the condition is considered +// failed for that tick ("not met") and the next tick is scheduled until +// waitFor duration is reached. +// +// If the condition does not complete successfully before waitFor expires, the +// collected errors of the last tick are copied to t before EventuallyWithT +// fails the test with "Condition never satisfied" and returns false. +// +// If the condition exits unexpectedly, a corresponding error: +// "Condition exited unexpectedly" is collected for that tick. +// +// ⚠️ See [Eventually] for more details about unexpected exits, which are a +// common pitfall when using 'require' functions inside condition functions. +// +// Since version 1.X.X, You can call [require.Fail] and similar requirements +// inside the condition to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. // -// externalValue := false +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} // go func() { -// time.Sleep(8*time.Second) -// externalValue = true +// time.Sleep(time.Second) +// externalValue.Store(true) // }() -// a.EventuallyWithT(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") +// a.EventuallyWithT(func(collect *assert.CollectT) { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // Use assertions with 'collect' and not with 't', so they are scoped to the current tick. +// assert.True(collect, gotValue, "expected 'externalValue' to become true") +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -359,24 +434,51 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w EventuallyWithT(a.t, condition, waitFor, tick, msgAndArgs...) } -// 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 -// function can use the CollectT to call other assertions. -// The condition is considered "met" if no errors are raised in a tick. -// The supplied CollectT collects all errors from one tick (if there are any). -// If the condition is not met before waitFor, the collected errors of -// the last tick are copied to t. +// EventuallyWithTf asserts that the given condition will be met in waitFor +// time, periodically checking the success of target function each tick. +// In contrast to [Eventually], it supplies a [CollectT] to the condition +// function that the condition function can use to call assertions on. +// These assertions are specific to the condition run in one tick. +// +// The supplied [CollectT] collects all errors from one tick. If no errors are +// collected, the condition is considered successful ("met") and EventuallyWithTf +// returns true. If there are collected errors, the condition is considered +// failed for that tick ("not met") and the next tick is scheduled until +// waitFor duration is reached. +// +// If the condition does not complete successfully before waitFor expires, the +// collected errors of the last tick are copied to t before EventuallyWithTf +// fails the test with "Condition never satisfied" and returns false. // -// externalValue := false +// If the condition exits unexpectedly, a corresponding error: +// "Condition exited unexpectedly" is collected for that tick. +// +// ⚠️ See [Eventually] for more details about unexpected exits, which are a +// common pitfall when using 'require' functions inside condition functions. +// +// Since version 1.X.X, You can call [require.Fail] and similar requirements +// inside the condition to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} // go func() { -// time.Sleep(8*time.Second) -// externalValue = true +// time.Sleep(time.Second) +// externalValue.Store(true) // }() -// a.EventuallyWithTf(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") +// a.EventuallyWithTf(func(collect *assert.CollectT, "error message %s", "formatted") { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // Use assertions with 'collect' and not with 't', so they are scoped to the current tick. +// assert.True(collect, gotValue, "expected 'externalValue' to become true") +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -384,10 +486,58 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), EventuallyWithTf(a.t, condition, waitFor, tick, msg, args...) } -// Eventuallyf asserts that given condition will be met in waitFor time, -// periodically checking target function each tick. +// Eventuallyf asserts that the given condition will be met in waitFor time, +// periodically checking result and completion of the target function each tick. +// If the condition is not met, the test fails with "Condition never satisfied". +// +// ⚠️ A condition function may exit unexpectedly, which is a common pitfall, +// since [Eventually] runs the condition function in a separate goroutine. +// An unexpected exit happens in the following cases: +// +// 1. The condition function panics. In this case the entire test will panic +// immediately and exit. This is normal Go runtime behavior and not +// specific to the testing framework. Condition panics are currently not +// recovered by [Eventually]. +// +// 2. The condition function calls [runtime.Goexit], which exits the goroutine +// without panicking. In this case the test fails immediately with +// "Condition exited unexpectedly". This is new behavior since v1.X.X. +// +// Note that [runtime.Goexit] is called by t.FailNow() and thus by all failing +// 'require' functions. You can call [require.Fail] and similar requirements +// inside the condition, to fail the test immediately. In the past this was not +// failing the test immediately but only after waitFor duration elapsed. +// This was a bug that has been fixed. Please adapt your tests accordingly. +// +// Also see [EventuallyWithT] for a version that allows using assertions in the +// condition function instead of returning a simple boolean value. +// +// Eventuallyf is often used to check conditions against values that are set by +// other goroutines. In such cases, always use thread-safe variables. +// It is also recommended to run 'go test' with the '-race' flag to detect +// race conditions in your tests and code. The following example demonstrates +// the correct usage of Eventuallyf with a thread-safe variable, including a +// call to a 'require' function inside the condition function to fail the test +// immediately on error: +// +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(time.Second) +// externalValue.Store(true) +// }() +// +// a.Eventuallyf(func(, "error message %s", "formatted") bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() +// +// // It is safe to use require functions on the parent 't' to fail the entire test immediately. +// _, err := someFunction() +// require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error +// +// return gotValue // -// a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") 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() @@ -1040,6 +1190,15 @@ 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. // +// Since version 1.X.X, if the condition exits unexpectedly, this is treated as +// a failure and the test fails immediately with: "Condition exited unexpectedly". +// Before version 1.X.X, unexpected exits lead to a blocked channel and a falsely +// passing [Never]. See [Eventually] for more details about unexpected exits. +// +// You can call [require.Fail] and similar requirements inside the condition +// to fail the test immediately. The blocking behavior from before version 1.X.X +// prevented this. Now it works as expected. Please adapt your tests accordingly. +// // 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 +1210,15 @@ 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. // +// Since version 1.X.X, if the condition exits unexpectedly, this is treated as +// a failure and the test fails immediately with: "Condition exited unexpectedly". +// Before version 1.X.X, unexpected exits lead to a blocked channel and a falsely +// passing [Never]. See [Eventually] for more details about unexpected exits. +// +// You can call [require.Fail] and similar requirements inside the condition +// to fail the test immediately. The blocking behavior from before version 1.X.X +// prevented this. Now it works as expected. Please adapt your tests accordingly. +// // 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 { From e3b99dfc115e68ca724bc227bb2085bfc83dea43 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Sun, 19 Oct 2025 03:02:19 +0200 Subject: [PATCH 07/16] add and link eventually doc --- EVENTUALLY.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 13 ++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 EVENTUALLY.md diff --git a/EVENTUALLY.md b/EVENTUALLY.md new file mode 100644 index 000000000..13ec62359 --- /dev/null +++ b/EVENTUALLY.md @@ -0,0 +1,78 @@ +# Eventually + +`assert.Eventually` waits for a user supplied condition to become `true`. It is +most often used when the code under test sets a value from another goroutine and +the test needs to poll until the desired state is reached. + +## Signature and scheduling + +```go +func Eventually( + t TestingT, + condition func() bool, + waitFor time.Duration, + tick time.Duration, + msgAndArgs ...interface{} +) bool +``` + +- `condition` is called on its own goroutine. The first evaluation happens + immediately. Subsequent evaluations are triggered every `tick` as long as the + condition keeps returning `false`. +- `waitFor` defines the maximum amount of time Eventually will spend polling. If + the deadline expires, the assertion fails with "Condition never satisfied" and + the optional `msgAndArgs` are appended to the failure output. +- The return value is `true` when the condition succeeds before the timeout and + `false` otherwise. The assertion also reports the failure through `t`. +- All state that is shared between the test and the condition must be protected + for concurrent access via mutexes, `atomic` types, and other synchronization + mechanisms. + +## Exit and panic behavior + +Since [PR #1809](https://github.com/stretchr/testify/pull/1809) `assert.Eventually` +distinguishes the different ways the condition goroutine can terminate: + +- **Condition returns `true`:** Eventually stops polling immediately and + succeeds. +- **Condition times out:** Eventually keeps polling until `waitFor` elapses and + then fails the test with "Condition never satisfied". +- **Condition panics:** The panic is *not* recovered. The Go runtime terminates + the process, prints the panic message and stack trace to standard error, and + the test run stops. This matches the normal behavior of panics in goroutines. +- **Condition calls `runtime.Goexit`:** Eventually now fails the test + immediately with "Condition exited unexpectedly". Before PR #1809 the + assertion waited until `waitFor` expired, causing tests that called + `t.FailNow()` (or any `require.*` helper that uses it) to appear to hang. The + new behavior surfaces the failure as soon as it happens. + +### EventuallyWithT specifics + +`assert.EventuallyWithT` runs the same polling loop but supplies each tick with +a fresh `*assert.CollectT`: + +- Returning from the closure without recording errors on `collect` marks the + condition as satisfied and the assertion succeeds immediately. +- Recording errors on `collect` (via `collect.Errorf`, `assert.*(collect, ...)` + helpers, or `collect.FailNow()`) marks just that tick as failed. The polling + continues, and if the timeout expires, the errors captured during the final + tick are replayed on the parent `t` before emitting "Condition never satisfied". +- If the closure exits via `runtime.Goexit` *without* first recording errors on + `collect`β€”for example by calling `require.*` on the parent `t`β€”the assertion + fails immediately with "Condition exited unexpectedly". +- Panics behave the same as in `assert.Eventually`: they are not recovered and + crash the test process. + +Use `collect` for tick-scoped assertions you want to keep retrying, and call +`require.*` on the parent `t` when you want the test to stop right away. The +same rules apply to `require.EventuallyWithT` and its helpers. + +## Usage tips + +- Pick a `tick` that balances fast feedback with the overhead of running the + condition. Extremely small ticks can create busy loops. +- Run your test suite with `go test -race` when Eventually coordinates with + other goroutines. Data races are a more common source of flakiness than the + assertion logic itself. +- If the condition needs to report rich diagnostics or multiple errors, prefer + `assert.EventuallyWithT` and record failures on the provided `CollectT` value. diff --git a/README.md b/README.md index 44d40e6c4..4ba1ca1bb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Testify - Thou Shalt Write Tests ================================ > [!NOTE] -> Testify is being maintained at v1, no breaking changes will be accepted in this repo. +> Testify is being maintained at v1, no breaking changes will be accepted in this repo. > [See discussion about v2](https://github.com/stretchr/testify/discussions/1560). [![Build Status](https://github.com/stretchr/testify/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/stretchr/testify/actions/workflows/main.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/stretchr/testify)](https://goreportcard.com/report/github.com/stretchr/testify) [![PkgGoDev](https://pkg.go.dev/badge/github.com/stretchr/testify)](https://pkg.go.dev/github.com/stretchr/testify) @@ -31,6 +31,17 @@ The `assert` package provides some helpful methods that allow you to write bette * Prints friendly, easy to read failure descriptions * Allows for very readable code * Optionally annotate each assertion with a message + * Supports polling assertions via `assert.Eventually`, `assert.EventuallyWithT`, and similar functions + + > [!Note] + > See [EVENTUALLY.md](EVENTUALLY.md) for details about timing, exit behavior, + > and panic handling or read the source code and source code comments carefully. + > + > The `Eventually` functions got some recent bug fixes and behavior changes\ + > for longstanding issues. \ + > πŸ‘‰οΈ Please **(re-)read** the [document](EVENTUALLY.md) and **code comments**! \ + > πŸ‘‰οΈ Please **adapt** your code if you were relying on the buggy behavior! + See it in action: From f9ddf2d6a495e0907048ea6b05c740311a918a47 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Sun, 19 Oct 2025 03:02:53 +0200 Subject: [PATCH 08/16] regenerate --- assert/assertion_format.go | 17 +++++++++++++---- assert/assertion_forward.go | 34 ++++++++++++++++++++++++++-------- require/require.go | 34 ++++++++++++++++++++++++++-------- require/require_forward.go | 34 ++++++++++++++++++++++++++-------- 4 files changed, 91 insertions(+), 28 deletions(-) diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 0c348467b..b031fd424 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -225,10 +225,10 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick } // EventuallyWithTf asserts that the given condition will be met in waitFor -// time, periodically checking the success of target function each tick. +// time, periodically checking the success of the condition function each tick. // In contrast to [Eventually], it supplies a [CollectT] to the condition // function that the condition function can use to call assertions on. -// These assertions are specific to the condition run in one tick. +// These assertions are specific to each run of the condition function in each tick. // // The supplied [CollectT] collects all errors from one tick. If no errors are // collected, the condition is considered successful ("met") and EventuallyWithTf @@ -240,8 +240,17 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // collected errors of the last tick are copied to t before EventuallyWithTf // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly, a corresponding error: -// "Condition exited unexpectedly" is collected for that tick. +// If the condition exits unexpectedly and NO errors are collected, a call to +// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the +// condition function. In this case, EventuallyWithTf fails the test immediately +// with "Condition exited unexpectedly" and returns false. +// +// πŸ’‘ Tick Assertions vs. Parent Test Assertions +// - Use tick assertions and requirements on the supplied 'collect' and not +// on the parent 't'. Only the last tick's collected errors are copied to 't'. +// - Use parent test requirements on the parent 't' to fail the entire test +// - Do not use assertions on the parent 't', since this would affect all ticks +// and create test noise. // // ⚠️ See [Eventually] for more details about unexpected exits, which are a // common pitfall when using 'require' functions inside condition functions. diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index 4d986a971..2ac88363f 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -382,10 +382,10 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti } // EventuallyWithT asserts that the given condition will be met in waitFor -// time, periodically checking the success of target function each tick. +// time, periodically checking the success of the condition function each tick. // In contrast to [Eventually], it supplies a [CollectT] to the condition // function that the condition function can use to call assertions on. -// These assertions are specific to the condition run in one tick. +// These assertions are specific to each run of the condition function in each tick. // // The supplied [CollectT] collects all errors from one tick. If no errors are // collected, the condition is considered successful ("met") and EventuallyWithT @@ -397,8 +397,17 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // collected errors of the last tick are copied to t before EventuallyWithT // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly, a corresponding error: -// "Condition exited unexpectedly" is collected for that tick. +// If the condition exits unexpectedly and NO errors are collected, a call to +// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the +// condition function. In this case, EventuallyWithT fails the test immediately +// with "Condition exited unexpectedly" and returns false. +// +// πŸ’‘ Tick Assertions vs. Parent Test Assertions +// - Use tick assertions and requirements on the supplied 'collect' and not +// on the parent 't'. Only the last tick's collected errors are copied to 't'. +// - Use parent test requirements on the parent 't' to fail the entire test +// - Do not use assertions on the parent 't', since this would affect all ticks +// and create test noise. // // ⚠️ See [Eventually] for more details about unexpected exits, which are a // common pitfall when using 'require' functions inside condition functions. @@ -434,10 +443,10 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor } // EventuallyWithTf asserts that the given condition will be met in waitFor -// time, periodically checking the success of target function each tick. +// time, periodically checking the success of the condition function each tick. // In contrast to [Eventually], it supplies a [CollectT] to the condition // function that the condition function can use to call assertions on. -// These assertions are specific to the condition run in one tick. +// These assertions are specific to each run of the condition function in each tick. // // The supplied [CollectT] collects all errors from one tick. If no errors are // collected, the condition is considered successful ("met") and EventuallyWithTf @@ -449,8 +458,17 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // collected errors of the last tick are copied to t before EventuallyWithTf // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly, a corresponding error: -// "Condition exited unexpectedly" is collected for that tick. +// If the condition exits unexpectedly and NO errors are collected, a call to +// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the +// condition function. In this case, EventuallyWithTf fails the test immediately +// with "Condition exited unexpectedly" and returns false. +// +// πŸ’‘ Tick Assertions vs. Parent Test Assertions +// - Use tick assertions and requirements on the supplied 'collect' and not +// on the parent 't'. Only the last tick's collected errors are copied to 't'. +// - Use parent test requirements on the parent 't' to fail the entire test +// - Do not use assertions on the parent 't', since this would affect all ticks +// and create test noise. // // ⚠️ See [Eventually] for more details about unexpected exits, which are a // common pitfall when using 'require' functions inside condition functions. diff --git a/require/require.go b/require/require.go index 99c84cb59..97ec390cb 100644 --- a/require/require.go +++ b/require/require.go @@ -464,10 +464,10 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t } // EventuallyWithT asserts that the given condition will be met in waitFor -// time, periodically checking the success of target function each tick. +// time, periodically checking the success of the condition function each tick. // In contrast to [Eventually], it supplies a [CollectT] to the condition // function that the condition function can use to call assertions on. -// These assertions are specific to the condition run in one tick. +// These assertions are specific to each run of the condition function in each tick. // // The supplied [CollectT] collects all errors from one tick. If no errors are // collected, the condition is considered successful ("met") and EventuallyWithT @@ -479,8 +479,17 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t // collected errors of the last tick are copied to t before EventuallyWithT // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly, a corresponding error: -// "Condition exited unexpectedly" is collected for that tick. +// If the condition exits unexpectedly and NO errors are collected, a call to +// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the +// condition function. In this case, EventuallyWithT fails the test immediately +// with "Condition exited unexpectedly" and returns false. +// +// πŸ’‘ Tick Assertions vs. Parent Test Assertions +// - Use tick assertions and requirements on the supplied 'collect' and not +// on the parent 't'. Only the last tick's collected errors are copied to 't'. +// - Use parent test requirements on the parent 't' to fail the entire test +// - Do not use assertions on the parent 't', since this would affect all ticks +// and create test noise. // // ⚠️ See [Eventually] for more details about unexpected exits, which are a // common pitfall when using 'require' functions inside condition functions. @@ -519,10 +528,10 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF } // EventuallyWithTf asserts that the given condition will be met in waitFor -// time, periodically checking the success of target function each tick. +// time, periodically checking the success of the condition function each tick. // In contrast to [Eventually], it supplies a [CollectT] to the condition // function that the condition function can use to call assertions on. -// These assertions are specific to the condition run in one tick. +// These assertions are specific to each run of the condition function in each tick. // // The supplied [CollectT] collects all errors from one tick. If no errors are // collected, the condition is considered successful ("met") and EventuallyWithTf @@ -534,8 +543,17 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF // collected errors of the last tick are copied to t before EventuallyWithTf // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly, a corresponding error: -// "Condition exited unexpectedly" is collected for that tick. +// If the condition exits unexpectedly and NO errors are collected, a call to +// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the +// condition function. In this case, EventuallyWithTf fails the test immediately +// with "Condition exited unexpectedly" and returns false. +// +// πŸ’‘ Tick Assertions vs. Parent Test Assertions +// - Use tick assertions and requirements on the supplied 'collect' and not +// on the parent 't'. Only the last tick's collected errors are copied to 't'. +// - Use parent test requirements on the parent 't' to fail the entire test +// - Do not use assertions on the parent 't', since this would affect all ticks +// and create test noise. // // ⚠️ See [Eventually] for more details about unexpected exits, which are a // common pitfall when using 'require' functions inside condition functions. diff --git a/require/require_forward.go b/require/require_forward.go index 95359dc9e..8a01e28e4 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -383,10 +383,10 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti } // EventuallyWithT asserts that the given condition will be met in waitFor -// time, periodically checking the success of target function each tick. +// time, periodically checking the success of the condition function each tick. // In contrast to [Eventually], it supplies a [CollectT] to the condition // function that the condition function can use to call assertions on. -// These assertions are specific to the condition run in one tick. +// These assertions are specific to each run of the condition function in each tick. // // The supplied [CollectT] collects all errors from one tick. If no errors are // collected, the condition is considered successful ("met") and EventuallyWithT @@ -398,8 +398,17 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // collected errors of the last tick are copied to t before EventuallyWithT // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly, a corresponding error: -// "Condition exited unexpectedly" is collected for that tick. +// If the condition exits unexpectedly and NO errors are collected, a call to +// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the +// condition function. In this case, EventuallyWithT fails the test immediately +// with "Condition exited unexpectedly" and returns false. +// +// πŸ’‘ Tick Assertions vs. Parent Test Assertions +// - Use tick assertions and requirements on the supplied 'collect' and not +// on the parent 't'. Only the last tick's collected errors are copied to 't'. +// - Use parent test requirements on the parent 't' to fail the entire test +// - Do not use assertions on the parent 't', since this would affect all ticks +// and create test noise. // // ⚠️ See [Eventually] for more details about unexpected exits, which are a // common pitfall when using 'require' functions inside condition functions. @@ -435,10 +444,10 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w } // EventuallyWithTf asserts that the given condition will be met in waitFor -// time, periodically checking the success of target function each tick. +// time, periodically checking the success of the condition function each tick. // In contrast to [Eventually], it supplies a [CollectT] to the condition // function that the condition function can use to call assertions on. -// These assertions are specific to the condition run in one tick. +// These assertions are specific to each run of the condition function in each tick. // // The supplied [CollectT] collects all errors from one tick. If no errors are // collected, the condition is considered successful ("met") and EventuallyWithTf @@ -450,8 +459,17 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w // collected errors of the last tick are copied to t before EventuallyWithTf // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly, a corresponding error: -// "Condition exited unexpectedly" is collected for that tick. +// If the condition exits unexpectedly and NO errors are collected, a call to +// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the +// condition function. In this case, EventuallyWithTf fails the test immediately +// with "Condition exited unexpectedly" and returns false. +// +// πŸ’‘ Tick Assertions vs. Parent Test Assertions +// - Use tick assertions and requirements on the supplied 'collect' and not +// on the parent 't'. Only the last tick's collected errors are copied to 't'. +// - Use parent test requirements on the parent 't' to fail the entire test +// - Do not use assertions on the parent 't', since this would affect all ticks +// and create test noise. // // ⚠️ See [Eventually] for more details about unexpected exits, which are a // common pitfall when using 'require' functions inside condition functions. From 1038085046416576059b9c0f3a762e1f50ca6ce4 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Mon, 20 Oct 2025 11:55:43 +0200 Subject: [PATCH 09/16] improve tests and docs --- EVENTUALLY.md | 44 ++++++++++++++++++-------- README.md | 16 +++++----- assert/assertions_exit_test.go | 56 +++++++++++++++++++++------------- 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/EVENTUALLY.md b/EVENTUALLY.md index 13ec62359..80b20acf6 100644 --- a/EVENTUALLY.md +++ b/EVENTUALLY.md @@ -19,7 +19,7 @@ func Eventually( - `condition` is called on its own goroutine. The first evaluation happens immediately. Subsequent evaluations are triggered every `tick` as long as the condition keeps returning `false`. -- `waitFor` defines the maximum amount of time Eventually will spend polling. If +- `waitFor` defines the maximum amount of time `Eventually` will spend polling. If the deadline expires, the assertion fails with "Condition never satisfied" and the optional `msgAndArgs` are appended to the failure output. - The return value is `true` when the condition succeeds before the timeout and @@ -33,20 +33,21 @@ func Eventually( Since [PR #1809](https://github.com/stretchr/testify/pull/1809) `assert.Eventually` distinguishes the different ways the condition goroutine can terminate: -- **Condition returns `true`:** Eventually stops polling immediately and +- **Condition returns `true`:** `Eventually` stops polling immediately and succeeds. -- **Condition times out:** Eventually keeps polling until `waitFor` elapses and +- **Condition times out:** `Eventually` keeps polling until `waitFor` elapses and then fails the test with "Condition never satisfied". - **Condition panics:** The panic is *not* recovered. The Go runtime terminates the process, prints the panic message and stack trace to standard error, and the test run stops. This matches the normal behavior of panics in goroutines. -- **Condition calls `runtime.Goexit`:** Eventually now fails the test - immediately with "Condition exited unexpectedly". Before PR #1809 the - assertion waited until `waitFor` expired, causing tests that called - `t.FailNow()` (or any `require.*` helper that uses it) to appear to hang. The - new behavior surfaces the failure as soon as it happens. +- **Condition calls `runtime.Goexit`:** `Eventually` now fails the test + immediately with "Condition exited unexpectedly". + Before [PR #1809](https://github.com/stretchr/testify/pull/1809) + the assertion waited until `waitFor` expired, causing tests that called + `t.FailNow()` (or `require.*` helpers that use it) to hang. + The new behavior surfaces the failure as soon as it happens. -### EventuallyWithT specifics +### `EventuallyWithT` specifics `assert.EventuallyWithT` runs the same polling loop but supplies each tick with a fresh `*assert.CollectT`: @@ -67,12 +68,31 @@ Use `collect` for tick-scoped assertions you want to keep retrying, and call `require.*` on the parent `t` when you want the test to stop right away. The same rules apply to `require.EventuallyWithT` and its helpers. +## `Never` specifics + +`assert.Never` runs the same polling loop as `Eventually` but expects the +condition to always return `false`. If the condition ever returns `true`, the +assertion fails immediately with "Condition satisfied". + +- If the condition panics, the panic is not recovered and the test process + terminates. +- If the condition calls `runtime.Goexit`, the assertion fails immediately with + "Condition exited unexpectedly". +- These behaviors match those of `assert.Eventually`. + +> [!Note] +> Since `Never` needs to run until the timeout expires to be successful, +> it cannot succeed early like `Eventually`. Prefer `Eventually` when possible +> to keep tests fast. + ## Usage tips - Pick a `tick` that balances fast feedback with the overhead of running the condition. Extremely small ticks can create busy loops. -- Run your test suite with `go test -race` when Eventually coordinates with - other goroutines. Data races are a more common source of flakiness than the - assertion logic itself. +- Run your test suite with `go test -race`, esp when `Eventually` coordinates + with other goroutines. Data races are a more common source of flakiness + than the assertion logic itself. - If the condition needs to report rich diagnostics or multiple errors, prefer `assert.EventuallyWithT` and record failures on the provided `CollectT` value. +- It is safe to call `require.*` helpers on the parent `t` from within the + condition closure to stop the test immediately. diff --git a/README.md b/README.md index 4ba1ca1bb..dd2914803 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,14 @@ The `assert` package provides some helpful methods that allow you to write bette * Optionally annotate each assertion with a message * Supports polling assertions via `assert.Eventually`, `assert.EventuallyWithT`, and similar functions - > [!Note] - > See [EVENTUALLY.md](EVENTUALLY.md) for details about timing, exit behavior, - > and panic handling or read the source code and source code comments carefully. - > - > The `Eventually` functions got some recent bug fixes and behavior changes\ - > for longstanding issues. \ - > πŸ‘‰οΈ Please **(re-)read** the [document](EVENTUALLY.md) and **code comments**! \ - > πŸ‘‰οΈ Please **adapt** your code if you were relying on the buggy behavior! +> [!Note] +> See [EVENTUALLY.md](EVENTUALLY.md) for details about timing, exit behavior, and panic handling, +> and read the source code and source code comments carefully. +> +> The `Eventually` functions behavior got some recent bug fixes and behavior changes for longstanding issues. +> +> πŸ‘‰οΈ Please **read** the [document](EVENTUALLY.md) and the **source code comments**! \ +> πŸ‘‰οΈ Please **adapt** your code if you were relying on the buggy behavior! See it in action: diff --git a/assert/assertions_exit_test.go b/assert/assertions_exit_test.go index c5b7e9dcd..2e71ecb65 100644 --- a/assert/assertions_exit_test.go +++ b/assert/assertions_exit_test.go @@ -18,7 +18,6 @@ func TestEventuallyFailsFast(t *testing.T) { exit func() ret bool expErrors []string - expFail bool } runFnAndExit := func(t TestingT, tc testCase, completed *bool) { @@ -61,36 +60,36 @@ func TestEventuallyFailsFast(t *testing.T) { { name: "Satisfy", run: evtl, fn: nil, exit: nil, ret: true, // succeed fast - expErrors: nil, expFail: false, // no errors expected + expErrors: nil, // no errors expected }, { name: "Fail", run: evtl, fn: doFail, exit: nil, ret: true, // fail and succeed fast - expErrors: []string{failedErr}, expFail: true, // expect fail + expErrors: []string{failedErr}, // expect fail }, { // Simulate [testing.T.FailNow], which calls // [testing.T.Fail] followed by [runtime.Goexit]. name: "FailNow", run: evtl, fn: doFail, exit: runtime.Goexit, ret: false, // no succeed fast, but fail - expErrors: []string{exitErr, failedErr}, expFail: true, // expect both errors + expErrors: []string{exitErr, failedErr}, // expect both errors }, { name: "Goexit", run: evtl, fn: nil, exit: runtime.Goexit, ret: false, // no succeed fast, just exit - expErrors: []string{exitErr}, expFail: true, // expect exit error + expErrors: []string{exitErr}, // expect exit error }, // Fast path EventuallyWithT tests { name: "SatisfyWithT", run: withT, fn: nil, exit: nil, ret: true, // succeed fast - expErrors: nil, expFail: false, // no errors expected + expErrors: nil, // no errors expected }, { name: "GoExitWithT", run: withT, fn: nil, exit: runtime.Goexit, ret: false, // no succeed fast, just exit - expErrors: []string{exitErr}, expFail: true, // expect exit error + expErrors: []string{exitErr}, // expect exit error }, // EventuallyWithT only fails fast when no errors are collected. // The Fail and FailNow cases are thus equivalent and will not fail fast and are not tested here. @@ -99,22 +98,22 @@ func TestEventuallyFailsFast(t *testing.T) { { name: "SatisfyNever", run: never, fn: nil, exit: nil, ret: true, // fail fast by satisfying - expErrors: []string{satisErr}, expFail: true, // expect satisfy error only + expErrors: []string{satisErr}, // expect satisfy error only }, { name: "FailNowNever", run: never, fn: doFail, exit: runtime.Goexit, ret: false, // no satisfy, but fail + exit - expErrors: []string{exitErr, failedErr}, expFail: true, // expect both errors + expErrors: []string{exitErr, failedErr}, // expect both errors }, { name: "GoexitNever", run: never, fn: nil, exit: runtime.Goexit, ret: false, // no satisfy, just exit - expErrors: []string{exitErr}, expFail: true, // expect exit error + expErrors: []string{exitErr}, // expect exit error }, { name: "FailNever", run: never, fn: doFail, exit: nil, ret: true, // fail then satisfy fast - expErrors: []string{failedErr, satisErr}, expFail: true, // expect fail error + expErrors: []string{failedErr, satisErr}, // expect fail error }, } @@ -138,8 +137,11 @@ func TestEventuallyFailsFast(t *testing.T) { case <-time.After(time.Second): FailNow(t, "test did not complete within timeout") } + + expFail := len(tc.expErrors) > 0 + Nil(t, panicValue, "Eventually should not panic") - Equal(t, tc.expFail, collT.failed(), "test state does not match expected failed state") + Equal(t, expFail, collT.failed(), "test state does not match expected failed state") Len(t, collT.errors, len(tc.expErrors), "number of collected errors does not match expectation") Found: @@ -167,7 +169,7 @@ func TestEventuallyCompletes(t *testing.T) { mockT = &mockTestingT{} EventuallyWithT(mockT, func(collect *CollectT) { // no assertion failures - }, time.Millisecond, time.Millisecond) + }, time.Second, time.Millisecond) False(t, mockT.Failed(), "test should not fail") mockT = &mockTestingT{} @@ -182,7 +184,7 @@ func TestEventuallyHandlesUnexpectedExit(t *testing.T) { collT := &CollectT{} Eventually(collT, func() bool { runtime.Goexit() - return false + panic("unreachable") }, time.Second, time.Millisecond) True(t, collT.failed(), "test should fail") Len(t, collT.errors, 1, "should have one error") @@ -191,6 +193,7 @@ func TestEventuallyHandlesUnexpectedExit(t *testing.T) { collT = &CollectT{} EventuallyWithT(collT, func(collect *CollectT) { runtime.Goexit() + panic("unreachable") }, time.Second, time.Millisecond) True(t, collT.failed(), "test should fail") Len(t, collT.errors, 1, "should have one error") @@ -199,14 +202,14 @@ func TestEventuallyHandlesUnexpectedExit(t *testing.T) { collT = &CollectT{} Never(collT, func() bool { runtime.Goexit() - return true + panic("unreachable") }, time.Second, time.Millisecond) True(t, collT.failed(), "test should fail") Len(t, collT.errors, 1, "should have one error") Contains(t, collT.errors[0].Error(), "Condition exited unexpectedly") } -func TestPanicInEventuallyStaysUnrecoverable(t *testing.T) { +func TestPanicInEventuallyNotRecovered(t *testing.T) { testPanicUnrecoverable(t, func() { Eventually(t, func() bool { panic("demo panic") @@ -214,7 +217,7 @@ func TestPanicInEventuallyStaysUnrecoverable(t *testing.T) { }) } -func TestPanicInEventuallyWithTStaysUnrecoverable(t *testing.T) { +func TestPanicInEventuallyWithTNotRecovered(t *testing.T) { testPanicUnrecoverable(t, func() { EventuallyWithT(t, func(collect *CollectT) { panic("demo panic") @@ -222,6 +225,14 @@ func TestPanicInEventuallyWithTStaysUnrecoverable(t *testing.T) { }) } +func TestPanicInNeverNotRecovered(t *testing.T) { + testPanicUnrecoverable(t, func() { + Never(t, func() bool { + panic("demo panic") + }, time.Minute, time.Millisecond) + }) +} + // testPanicUnrecoverable ensures current goroutine panic behavior. // // Currently, [Eventually] runs the condition function in a separate goroutine. @@ -229,22 +240,23 @@ func TestPanicInEventuallyWithTStaysUnrecoverable(t *testing.T) { // is terminated. // // In the future, this behavior may change so that panics in the condition are caught -// and handled more gracefully. For now we ensure such panics to stay unrecoverable. +// and handled more gracefully. For now we ensure such panics are not unrecoved. // // To run this test, set the environment variable TestPanic=1. // The test is successful if it panics and fails the test process and does NOT print // "UNREACHABLE CODE!" after the initial log messages. -func testPanicUnrecoverable(t *testing.T, f func()) { +func testPanicUnrecoverable(t *testing.T, failingDemoTest func()) { if os.Getenv("TestPanic") == "" { t.Skip("Skipping test, set TestPanic=1 to run") } - t.Log("This test must fail by a panic in a goroutine.") - t.Log("If you see the text 'UNREACHABLE CODE!' after this point, this means the test exited in an unintended way") + // Use fmt.Println instead of t.Log because t.Log output may be suppressed. + fmt.Println("⚠️ This test must fail by a panic in a goroutine.") + fmt.Println("⚠️ If you see the text 'UNREACHABLE CODE!' after this point, this means the test exited in an unintended way") defer func() { // defer statements are not run when a goroutine panics, so this code is // only reachable if the panic was somehow recovered. fmt.Println("❌ UNREACHABLE CODE!") fmt.Println("❌ If you see this, the test has not failed as expected.") }() - f() + failingDemoTest() } From f64d2b413929d593d05acfcbd57b09c11655b960 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Tue, 21 Oct 2025 23:00:07 +0200 Subject: [PATCH 10/16] update codegen to respect existing message, fix breaking condition func msgs --- _codegen/main.go | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/_codegen/main.go b/_codegen/main.go index 38a013249..fed0c9fc0 100644 --- a/_codegen/main.go +++ b/_codegen/main.go @@ -296,16 +296,51 @@ func (f *testFunc) CommentFormat() string { "["+nameF+"]", "["+nameF+"]", // ref to format func code, keep as is name+" ", nameF+" ", // mention in text -> replace name+"(", nameF+"(", // function call -> replace - name+",", nameF+",", // mention enumeration -> replace + name+",", nameF+",", // mention in enumeration -> replace name+".", nameF+".", // closure of sentence -> replace name+"\n", nameF+"\n", // end of line -> replace ) + comment = bestEffortReplacer.Replace(comment) + + // 2 Find single line assertion calls of any kind, exluding multi-line ones. + // example: // assert.Equal(t, expected, actual) <-- the call must be closed on the same line + assertFormatFuncExp := regexp.MustCompile(`assert\.` + nameF + `\(.*\)`) + // 2.1 Extract params and existing message if any. + // Note: Unless we start parsing the parameters properly, this is a best-effort solution. + // If an assertion call ends with a string parameter, we consider that the message. + // Please adjust the assertion examples accordingly if needed. + const minErrorMessageLength = 10 + paramsExp := regexp.MustCompile(`([^()]*)\((.*)\)`) + strParamExp := regexp.MustCompile(`"[^"]*"$`) + comment = assertFormatFuncExp.ReplaceAllStringFunc(comment, func(s string) string { + oBraces := strings.Count(s, "(") + cBraces := strings.Count(s, ")") + if oBraces != cBraces { + // Skip multi-line examples, where assert call is not closed on the same line. + return s + } - // 2. Regex for (replaced) function calls - callExp := regexp.MustCompile(nameF + `\(((\(\)|[^\n])+)\)`) + m := paramsExp.FindStringSubmatch(s) + prefix, params, msg := m[1], strings.Split(m[2], ", "), "error message" - comment = bestEffortReplacer.Replace(comment) - comment = callExp.ReplaceAllString(comment, nameF+`($1, "error message %s", "formatted")`) + last := strings.TrimSpace(params[len(params)-1]) + // If last param is a string, consider it the message. + // It is is too short, it is an assertion value, not a message. + if strParamExp.MatchString(last) && len(last) > minErrorMessageLength+2 { + msg = strings.Trim(msg, `"`) + ":" + params = params[:len(params)-1] + } + + // Rebuild the call with formatted message, reuse existing message if any. + params = append(params, `"`+msg+` %s", "formatted"`) + return prefix + "(" + strings.Join(params, ", ") + ")" + }) + + // 3. Replace calls to multi-line assertions end. Examles like: + // search: // }, time.Second, 10*time.Millisecond, "condition must never be true") + // replace: // }, time.Second, 10*time.Millisecond, "condition must never be true, more: %s", "formatted") + endFuncWithStringExp := regexp.MustCompile(`(//[\s]*\},.* )"([^"]+)"\)(\n|$)`) + comment = endFuncWithStringExp.ReplaceAllString(comment, `$1 "$2, more: %s", "formatted")$3`) return comment } From 068b9949ba1428a1528cb452c3bcbf14a31351d3 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Tue, 21 Oct 2025 23:02:11 +0200 Subject: [PATCH 11/16] extend CollectT, detect FailNow in CollectT, redesign MockT, add more exit tests --- assert/assertion_format.go | 38 +++++-- assert/assertion_forward.go | 72 ++++++++---- assert/assertions.go | 76 ++++++++++--- assert/http_assertions.go | 6 +- require/forward_requirements_test.go | 88 +++++++------- require/require.go | 72 ++++++++---- require/require_forward.go | 72 ++++++++---- require/requirements_exit_test.go | 164 +++++++++++++++++++++++++++ require/requirements_test.go | 148 +++++++++++++++--------- 9 files changed, 545 insertions(+), 191 deletions(-) create mode 100644 require/requirements_exit_test.go diff --git a/assert/assertion_format.go b/assert/assertion_format.go index b031fd424..6e7baca79 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -206,7 +206,7 @@ func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface // externalValue.Store(true) // }() // -// assert.Eventuallyf(t, func(, "error message %s", "formatted") bool { +// assert.Eventuallyf(t, func() bool { // // 🀝 Use thread-safe access when communicating with other goroutines! // gotValue := externalValue.Load() // @@ -216,7 +216,7 @@ func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface // // return gotValue // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s, more: %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 { h.Helper() @@ -266,7 +266,7 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // time.Sleep(time.Second) // externalValue.Store(true) // }() -// assert.EventuallyWithTf(t, func(collect *assert.CollectT, "error message %s", "formatted") { +// assert.EventuallyWithTf(t, func(collect *assert.CollectT) { // // 🀝 Use thread-safe access when communicating with other goroutines! // gotValue := externalValue.Load() // @@ -277,7 +277,7 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // _, err := someFunction() // require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s, more: %s", "formatted") 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() @@ -358,7 +358,8 @@ func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, arg // HTTPBodyContainsf asserts that a specified handler returns a // body that contains a string. // -// assert.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// expectVal := "I'm Feeling Lucky" +// assert.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, expectVal, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { @@ -371,7 +372,8 @@ func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url // HTTPBodyNotContainsf asserts that a specified handler returns a // body that does not contain a string. // -// assert.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// expectVal := "I'm Feeling Lucky" +// assert.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, expectVal, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { @@ -618,7 +620,17 @@ func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) bool // to fail the test immediately. The blocking behavior from before version 1.X.X // prevented this. Now it works as expected. Please adapt your tests accordingly. // -// assert.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(2*time.Second) +// externalValue.Store(true) +// }() +// +// assert.Neverf(t, func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// return externalValue.Load() +// }, time.Second, 10*time.Millisecond, "condition must never become true within 1s, more: %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 { h.Helper() @@ -782,8 +794,9 @@ func NotPanicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bo // NotRegexpf asserts that a specified regexp does not match a string. // -// assert.NotRegexpf(t, regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") -// assert.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +// expectVal := "not started" +// assert.NotRegexpf(t, regexp.MustCompile("^start"), expectVal, "error message %s", "formatted") +// assert.NotRegexpf(t, "^start", expectVal, "error message %s", "formatted") func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -830,7 +843,7 @@ func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) bool { // Panicsf asserts that the code inside the specified PanicTestFunc panics. // -// assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +// assert.Panicsf(t, func(){ GoCrazy() }, "error message: %s", "formatted") func Panicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -874,8 +887,9 @@ func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) bool // Regexpf asserts that a specified regexp matches a string. // -// assert.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") -// assert.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +// expectVal := "started" +// assert.Regexpf(t, regexp.MustCompile("^start"), expectVal, "error message %s", "formatted") +// assert.Regexpf(t, "^start", expectVal, "error message %s", "formatted") func Regexpf(t TestingT, rx interface{}, str interface{}, 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 2ac88363f..acd534445 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -373,7 +373,7 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool { // // return gotValue // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s") 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() @@ -434,7 +434,7 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // _, err := someFunction() // require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s") 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() @@ -484,7 +484,7 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // time.Sleep(time.Second) // externalValue.Store(true) // }() -// a.EventuallyWithTf(func(collect *assert.CollectT, "error message %s", "formatted") { +// a.EventuallyWithTf(func(collect *assert.CollectT) { // // 🀝 Use thread-safe access when communicating with other goroutines! // gotValue := externalValue.Load() // @@ -495,7 +495,7 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // _, err := someFunction() // require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s, more: %s", "formatted") 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() @@ -544,7 +544,7 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor // externalValue.Store(true) // }() // -// a.Eventuallyf(func(, "error message %s", "formatted") bool { +// a.Eventuallyf(func() bool { // // 🀝 Use thread-safe access when communicating with other goroutines! // gotValue := externalValue.Load() // @@ -554,7 +554,7 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor // // return gotValue // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s, more: %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 { h.Helper() @@ -705,7 +705,8 @@ func (a *Assertions) Greaterf(e1 interface{}, e2 interface{}, msg string, args . // HTTPBodyContains asserts that a specified handler returns a // body that contains a string. // -// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// expectVal := "I'm Feeling Lucky" +// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, expectVal) // // Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { @@ -718,7 +719,8 @@ func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, u // HTTPBodyContainsf asserts that a specified handler returns a // body that contains a string. // -// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// expectVal := "I'm Feeling Lucky" +// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, expectVal, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { @@ -731,7 +733,8 @@ func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, // HTTPBodyNotContains asserts that a specified handler returns a // body that does not contain a string. // -// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// expectVal := "I'm Feeling Lucky" +// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, expectVal) // // Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { @@ -744,7 +747,8 @@ func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string // HTTPBodyNotContainsf asserts that a specified handler returns a // body that does not contain a string. // -// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// expectVal := "I'm Feeling Lucky" +// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, expectVal, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { @@ -1216,7 +1220,17 @@ func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) b // to fail the test immediately. The blocking behavior from before version 1.X.X // prevented this. Now it works as expected. Please adapt your tests accordingly. // -// a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(2*time.Second) +// externalValue.Store(true) +// }() +// +// a.Never(func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// return externalValue.Load() +// }, time.Second, 10*time.Millisecond, "condition must never become true within 1s") 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() @@ -1236,7 +1250,17 @@ func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick ti // to fail the test immediately. The blocking behavior from before version 1.X.X // prevented this. Now it works as expected. Please adapt your tests accordingly. // -// a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(2*time.Second) +// externalValue.Store(true) +// }() +// +// a.Neverf(func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// return externalValue.Load() +// }, time.Second, 10*time.Millisecond, "condition must never become true within 1s, more: %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 { h.Helper() @@ -1554,8 +1578,9 @@ func (a *Assertions) NotPanicsf(f PanicTestFunc, msg string, args ...interface{} // NotRegexp asserts that a specified regexp does not match a string. // -// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") -// a.NotRegexp("^start", "it's not starting") +// expectVal := "not started" +// a.NotRegexp(regexp.MustCompile("^start"), expectVal) +// a.NotRegexp("^start", expectVal) func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1565,8 +1590,9 @@ func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...in // NotRegexpf asserts that a specified regexp does not match a string. // -// a.NotRegexpf(regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") -// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted") +// expectVal := "not started" +// a.NotRegexpf(regexp.MustCompile("^start"), expectVal, "error message %s", "formatted") +// a.NotRegexpf("^start", expectVal, "error message %s", "formatted") func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1650,7 +1676,7 @@ func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) bo // Panics asserts that the code inside the specified PanicTestFunc panics. // -// a.Panics(func(){ GoCrazy() }) +// a.Panics(func(){ GoCrazy() }, "GoCrazy must panic") func (a *Assertions) Panics(f PanicTestFunc, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1706,7 +1732,7 @@ func (a *Assertions) PanicsWithValuef(expected interface{}, f PanicTestFunc, msg // Panicsf asserts that the code inside the specified PanicTestFunc panics. // -// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted") +// a.Panicsf(func(){ GoCrazy() }, "error message: %s", "formatted") func (a *Assertions) Panicsf(f PanicTestFunc, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1738,8 +1764,9 @@ func (a *Assertions) Positivef(e interface{}, msg string, args ...interface{}) b // Regexp asserts that a specified regexp matches a string. // -// a.Regexp(regexp.MustCompile("start"), "it's starting") -// a.Regexp("start...$", "it's not starting") +// expectVal := "started" +// a.Regexp(regexp.MustCompile("^start"), expectVal) +// a.Regexp("^start", expectVal) func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1749,8 +1776,9 @@ func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...inter // Regexpf asserts that a specified regexp matches a string. // -// a.Regexpf(regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") -// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted") +// expectVal := "started" +// a.Regexpf(regexp.MustCompile("^start"), expectVal, "error message %s", "formatted") +// a.Regexpf("^start", expectVal, "error message %s", "formatted") func (a *Assertions) Regexpf(rx interface{}, str interface{}, 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 4c747131f..7564f8922 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -1298,7 +1298,7 @@ func didPanic(f PanicTestFunc) (didPanic bool, message interface{}, stack string // Panics asserts that the code inside the specified PanicTestFunc panics. // -// assert.Panics(t, func(){ GoCrazy() }) +// assert.Panics(t, func(){ GoCrazy() }, "GoCrazy must panic") func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -1729,8 +1729,9 @@ func matchRegexp(rx interface{}, str interface{}) bool { // Regexp asserts that a specified regexp matches a string. // -// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") -// assert.Regexp(t, "start...$", "it's not starting") +// expectVal := "started" +// assert.Regexp(t, regexp.MustCompile("^start"), expectVal) +// assert.Regexp(t, "^start", expectVal) func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -1747,8 +1748,9 @@ func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface // NotRegexp asserts that a specified regexp does not match a string. // -// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") -// assert.NotRegexp(t, "^start", "it's not starting") +// expectVal := "not started" +// assert.NotRegexp(t, regexp.MustCompile("^start"), expectVal) +// assert.NotRegexp(t, "^start", expectVal) func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -2052,7 +2054,7 @@ type tHelper = interface { // // return gotValue // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s") func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -2126,6 +2128,9 @@ 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 + + // finished is true if FailNow was called. + finished bool } // Helper is like [testing.T.Helper] but does nothing. @@ -2139,9 +2144,32 @@ func (c *CollectT) Errorf(format string, args ...interface{}) { // FailNow stops execution by calling runtime.Goexit. func (c *CollectT) FailNow() { c.fail() + c.finished = true runtime.Goexit() } +// Fail marks the function as failed without recording an error. +// It does not stop execution. +func (c *CollectT) Fail() { + c.fail() +} + +// Failed returns true if any errors were collected or FailNow was called. +// This also implements [TestingT.Failed]. +func (t *CollectT) Failed() bool { + return t.failed() +} + +// Errors returns the collected errors. +// It returns nil only if no errors were collected and FailNow was not called. +// If FailNow was called without any prior Errorf calls, it returns an empty slice (non-nil). +// +// Errors can be used to inspect all collected errors after running assertions with CollectT. +// Also see [CollectT.Failed] to quickly check whether any errors were collected or FailNow was called. +func (t *CollectT) Errors() []error { + return t.errors +} + // Deprecated: That was a method for internal usage that should not have been published. Now just panics. func (*CollectT) Reset() { panic("Reset() is deprecated") @@ -2153,15 +2181,21 @@ func (*CollectT) Copy(TestingT) { } func (c *CollectT) fail() { - if !c.failed() { + if c.errors == nil { c.errors = []error{} // Make it non-nil to mark a failure. } } +// failed returns true if any errors were collected or FailNow was called. func (c *CollectT) failed() bool { return c.errors != nil } +// calledFailNow returns true if the goroutine has exited via FailNow. +func (c *CollectT) calledFailNow() bool { + return c.finished +} + // EventuallyWithT asserts that the given condition will be met in waitFor // time, periodically checking the success of the condition function each tick. // In contrast to [Eventually], it supplies a [CollectT] to the condition @@ -2215,7 +2249,7 @@ func (c *CollectT) failed() bool { // _, err := someFunction() // require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s") 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() @@ -2226,19 +2260,19 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time // or a t.FailNow() called on a parent 't' inside the condition // and not on the supplied 'collect'. This is the path where also // EventuallyWithT must exit immediately with a failure, just like [Eventually]. - var conditionExitedWithoutCollectingErrors bool + var goroutineExitedWithoutCallingFailNow bool var lastFinishedTickErrs []error ch := make(chan *CollectT, 1) checkCond := func() { - returned := false + goroutineExited := true // Assume the goroutine will exit. collect := new(CollectT) defer func() { - conditionExitedWithoutCollectingErrors = !returned && !collect.failed() + goroutineExitedWithoutCallingFailNow = goroutineExited && !collect.calledFailNow() ch <- collect }() condition(collect) - returned = true + goroutineExited = false // The goroutine did not exit via [runtime.Goexit] } timer := time.NewTimer(waitFor) @@ -2264,8 +2298,12 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time go checkCond() case collect := <-ch: switch { - case conditionExitedWithoutCollectingErrors: + case goroutineExitedWithoutCallingFailNow: // See [Eventually] for explanation about unexpected exits. + // Copy last tick errors to 't' before failing. + for _, err := range collect.errors { + t.Errorf("%v", err) + } return Fail(t, "Condition exited unexpectedly", msgAndArgs...) case !collect.failed(): // Condition met. @@ -2295,7 +2333,17 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time // to fail the test immediately. The blocking behavior from before version 1.X.X // prevented this. Now it works as expected. Please adapt your tests accordingly. // -// assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(2*time.Second) +// externalValue.Store(true) +// }() +// +// assert.Never(t, func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// return externalValue.Load() +// }, time.Second, 10*time.Millisecond, "condition must never become true within 1s") func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() diff --git a/assert/http_assertions.go b/assert/http_assertions.go index 5a6bb75f2..71ec46baf 100644 --- a/assert/http_assertions.go +++ b/assert/http_assertions.go @@ -127,7 +127,8 @@ func HTTPBody(handler http.HandlerFunc, method, url string, values url.Values) s // HTTPBodyContains asserts that a specified handler returns a // body that contains a string. // -// assert.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// expectVal := "I'm Feeling Lucky" +// assert.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, expectVal) // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { @@ -147,7 +148,8 @@ func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method, url string, // HTTPBodyNotContains asserts that a specified handler returns a // body that does not contain a string. // -// assert.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// expectVal := "I'm Feeling Lucky" +// assert.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, expectVal) // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { diff --git a/require/forward_requirements_test.go b/require/forward_requirements_test.go index 617bfb2c3..443fe87f9 100644 --- a/require/forward_requirements_test.go +++ b/require/forward_requirements_test.go @@ -16,7 +16,7 @@ func TestImplementsWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -30,7 +30,7 @@ func TestIsNotTypeWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.IsNotType(new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -44,7 +44,7 @@ func TestIsTypeWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.IsType(new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -58,7 +58,7 @@ func TestEqualWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.Equal(1, 2) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -72,7 +72,7 @@ func TestNotEqualWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.NotEqual(2, 2) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -91,7 +91,7 @@ func TestExactlyWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.Exactly(a, c) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -105,7 +105,7 @@ func TestNotNilWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.NotNil(nil) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -119,7 +119,7 @@ func TestNilWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.Nil(new(AssertionTesterConformingObject)) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -133,7 +133,7 @@ func TestTrueWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.True(false) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -147,7 +147,7 @@ func TestFalseWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.False(true) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -161,7 +161,7 @@ func TestContainsWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.Contains("Hello World", "Salut") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -175,7 +175,7 @@ func TestNotContainsWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.NotContains("Hello World", "Hello") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -191,7 +191,7 @@ func TestPanicsWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.Panics(func() {}) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -207,7 +207,7 @@ func TestNotPanicsWrapper(t *testing.T) { mockRequire.NotPanics(func() { panic("Panic!") }) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -221,7 +221,7 @@ func TestNoErrorWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.NoError(errors.New("some error")) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -235,7 +235,7 @@ func TestErrorWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.Error(nil) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -249,7 +249,7 @@ func TestErrorContainsWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.ErrorContains(errors.New("some error: another error"), "different error") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -263,7 +263,7 @@ func TestEqualErrorWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.EqualError(errors.New("some error"), "Not some error") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -277,7 +277,7 @@ func TestEmptyWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.Empty("x") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -291,7 +291,7 @@ func TestNotEmptyWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.NotEmpty("") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -308,7 +308,7 @@ func TestWithinDurationWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.WithinDuration(a, b, 5*time.Second) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -322,7 +322,7 @@ func TestInDeltaWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.InDelta(1, 2, 0.5) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -336,7 +336,7 @@ func TestZeroWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.Zero(1) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -350,7 +350,7 @@ func TestNotZeroWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) mockRequire.NotZero(0) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -362,7 +362,7 @@ func TestJSONEqWrapper_EqualSONString(t *testing.T) { mockRequire := New(mockT) mockRequire.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -374,7 +374,7 @@ func TestJSONEqWrapper_EquivalentButNotEqual(t *testing.T) { mockRequire := New(mockT) mockRequire.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -387,7 +387,7 @@ func TestJSONEqWrapper_HashOfArraysAndHashes(t *testing.T) { mockRequire.JSONEq("{\r\n\t\"numeric\": 1.5,\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]],\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\"\r\n}", "{\r\n\t\"numeric\": 1.5,\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\",\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]]\r\n}") - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -399,7 +399,7 @@ func TestJSONEqWrapper_Array(t *testing.T) { mockRequire := New(mockT) mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -411,7 +411,7 @@ func TestJSONEqWrapper_HashAndArrayNotEquivalent(t *testing.T) { mockRequire := New(mockT) mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -423,7 +423,7 @@ func TestJSONEqWrapper_HashesNotEquivalent(t *testing.T) { mockRequire := New(mockT) mockRequire.JSONEq(`{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -435,7 +435,7 @@ func TestJSONEqWrapper_ActualIsNotJSON(t *testing.T) { mockRequire := New(mockT) mockRequire.JSONEq(`{"foo": "bar"}`, "Not JSON") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -447,7 +447,7 @@ func TestJSONEqWrapper_ExpectedIsNotJSON(t *testing.T) { mockRequire := New(mockT) mockRequire.JSONEq("Not JSON", `{"foo": "bar", "hello": "world"}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -459,7 +459,7 @@ func TestJSONEqWrapper_ExpectedAndActualNotJSON(t *testing.T) { mockRequire := New(mockT) mockRequire.JSONEq("Not JSON", "Not JSON") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -471,7 +471,7 @@ func TestJSONEqWrapper_ArraysOfDifferentOrder(t *testing.T) { mockRequire := New(mockT) mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -483,7 +483,7 @@ func TestYAMLEqWrapper_EqualYAMLString(t *testing.T) { mockRequire := New(mockT) mockRequire.YAMLEq(`{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -495,7 +495,7 @@ func TestYAMLEqWrapper_EquivalentButNotEqual(t *testing.T) { mockRequire := New(mockT) mockRequire.YAMLEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -533,7 +533,7 @@ array: ` mockRequire.YAMLEq(expected, actual) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -545,7 +545,7 @@ func TestYAMLEqWrapper_Array(t *testing.T) { mockRequire := New(mockT) mockRequire.YAMLEq(`["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -557,7 +557,7 @@ func TestYAMLEqWrapper_HashAndArrayNotEquivalent(t *testing.T) { mockRequire := New(mockT) mockRequire.YAMLEq(`["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -569,7 +569,7 @@ func TestYAMLEqWrapper_HashesNotEquivalent(t *testing.T) { mockRequire := New(mockT) mockRequire.YAMLEq(`{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -581,7 +581,7 @@ func TestYAMLEqWrapper_ActualIsSimpleString(t *testing.T) { mockRequire := New(mockT) mockRequire.YAMLEq(`{"foo": "bar"}`, "Simple String") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -593,7 +593,7 @@ func TestYAMLEqWrapper_ExpectedIsSimpleString(t *testing.T) { mockRequire := New(mockT) mockRequire.YAMLEq("Simple String", `{"foo": "bar", "hello": "world"}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -605,7 +605,7 @@ func TestYAMLEqWrapper_ExpectedAndActualSimpleString(t *testing.T) { mockRequire := New(mockT) mockRequire.YAMLEq("Simple String", "Simple String") - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -617,7 +617,7 @@ func TestYAMLEqWrapper_ArraysOfDifferentOrder(t *testing.T) { mockRequire := New(mockT) mockRequire.YAMLEq(`["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } diff --git a/require/require.go b/require/require.go index 97ec390cb..d36dfd706 100644 --- a/require/require.go +++ b/require/require.go @@ -452,7 +452,7 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) { // // return gotValue // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s") func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -516,7 +516,7 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t // _, err := someFunction() // require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s") 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() @@ -569,7 +569,7 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF // time.Sleep(time.Second) // externalValue.Store(true) // }() -// require.EventuallyWithTf(t, func(collect *require.CollectT, "error message %s", "formatted") { +// require.EventuallyWithTf(t, func(collect *require.CollectT) { // // 🀝 Use thread-safe access when communicating with other goroutines! // gotValue := externalValue.Load() // @@ -580,7 +580,7 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF // _, err := someFunction() // require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s, more: %s", "formatted") 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() @@ -632,7 +632,7 @@ func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), wait // externalValue.Store(true) // }() // -// require.Eventuallyf(t, func(, "error message %s", "formatted") bool { +// require.Eventuallyf(t, func() bool { // // 🀝 Use thread-safe access when communicating with other goroutines! // gotValue := externalValue.Load() // @@ -642,7 +642,7 @@ func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), wait // // return gotValue // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s, more: %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 { h.Helper() @@ -838,7 +838,8 @@ func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...in // HTTPBodyContains asserts that a specified handler returns a // body that contains a string. // -// require.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// expectVal := "I'm Feeling Lucky" +// require.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, expectVal) // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { @@ -854,7 +855,8 @@ func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url s // HTTPBodyContainsf asserts that a specified handler returns a // body that contains a string. // -// require.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// expectVal := "I'm Feeling Lucky" +// require.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, expectVal, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { @@ -870,7 +872,8 @@ func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url // HTTPBodyNotContains asserts that a specified handler returns a // body that does not contain a string. // -// require.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// expectVal := "I'm Feeling Lucky" +// require.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, expectVal) // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { @@ -886,7 +889,8 @@ func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, ur // HTTPBodyNotContainsf asserts that a specified handler returns a // body that does not contain a string. // -// require.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// expectVal := "I'm Feeling Lucky" +// require.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, expectVal, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { @@ -1487,7 +1491,17 @@ func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) { // to fail the test immediately. The blocking behavior from before version 1.X.X // prevented this. Now it works as expected. Please adapt your tests accordingly. // -// require.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(2*time.Second) +// externalValue.Store(true) +// }() +// +// require.Never(t, func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// return externalValue.Load() +// }, time.Second, 10*time.Millisecond, "condition must never become true within 1s") func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1510,7 +1524,17 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D // to fail the test immediately. The blocking behavior from before version 1.X.X // prevented this. Now it works as expected. Please adapt your tests accordingly. // -// require.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(2*time.Second) +// externalValue.Store(true) +// }() +// +// require.Neverf(t, func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// return externalValue.Load() +// }, time.Second, 10*time.Millisecond, "condition must never become true within 1s, more: %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 { h.Helper() @@ -1915,8 +1939,9 @@ func NotPanicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interfac // NotRegexp asserts that a specified regexp does not match a string. // -// require.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") -// require.NotRegexp(t, "^start", "it's not starting") +// expectVal := "not started" +// require.NotRegexp(t, regexp.MustCompile("^start"), expectVal) +// require.NotRegexp(t, "^start", expectVal) func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1929,8 +1954,9 @@ func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interf // NotRegexpf asserts that a specified regexp does not match a string. // -// require.NotRegexpf(t, regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") -// require.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +// expectVal := "not started" +// require.NotRegexpf(t, regexp.MustCompile("^start"), expectVal, "error message %s", "formatted") +// require.NotRegexpf(t, "^start", expectVal, "error message %s", "formatted") func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -2035,7 +2061,7 @@ func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) { // Panics asserts that the code inside the specified PanicTestFunc panics. // -// require.Panics(t, func(){ GoCrazy() }) +// require.Panics(t, func(){ GoCrazy() }, "GoCrazy must panic") func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -2106,7 +2132,7 @@ func PanicsWithValuef(t TestingT, expected interface{}, f assert.PanicTestFunc, // Panicsf asserts that the code inside the specified PanicTestFunc panics. // -// require.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +// require.Panicsf(t, func(){ GoCrazy() }, "error message: %s", "formatted") func Panicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -2147,8 +2173,9 @@ func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) { // Regexp asserts that a specified regexp matches a string. // -// require.Regexp(t, regexp.MustCompile("start"), "it's starting") -// require.Regexp(t, "start...$", "it's not starting") +// expectVal := "started" +// require.Regexp(t, regexp.MustCompile("^start"), expectVal) +// require.Regexp(t, "^start", expectVal) func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -2161,8 +2188,9 @@ func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface // Regexpf asserts that a specified regexp matches a string. // -// require.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") -// require.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +// expectVal := "started" +// require.Regexpf(t, regexp.MustCompile("^start"), expectVal, "error message %s", "formatted") +// require.Regexpf(t, "^start", expectVal, "error message %s", "formatted") func Regexpf(t TestingT, rx interface{}, str interface{}, 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 8a01e28e4..4db92065e 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -374,7 +374,7 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) { // // return gotValue // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s") func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -435,7 +435,7 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // _, err := someFunction() // require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s") 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() @@ -485,7 +485,7 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w // time.Sleep(time.Second) // externalValue.Store(true) // }() -// a.EventuallyWithTf(func(collect *assert.CollectT, "error message %s", "formatted") { +// a.EventuallyWithTf(func(collect *assert.CollectT) { // // 🀝 Use thread-safe access when communicating with other goroutines! // gotValue := externalValue.Load() // @@ -496,7 +496,7 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w // _, err := someFunction() // require.NoError(t, err, "external function must not fail") // πŸ›‘ exit early on error // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s, more: %s", "formatted") 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() @@ -545,7 +545,7 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), // externalValue.Store(true) // }() // -// a.Eventuallyf(func(, "error message %s", "formatted") bool { +// a.Eventuallyf(func() bool { // // 🀝 Use thread-safe access when communicating with other goroutines! // gotValue := externalValue.Load() // @@ -555,7 +555,7 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), // // return gotValue // -// }, 2*time.Second, 10*time.Millisecond, "externalValue never became true") +// }, 2*time.Second, 10*time.Millisecond, "externalValue must become true within 2s, more: %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 { h.Helper() @@ -706,7 +706,8 @@ func (a *Assertions) Greaterf(e1 interface{}, e2 interface{}, msg string, args . // HTTPBodyContains asserts that a specified handler returns a // body that contains a string. // -// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// expectVal := "I'm Feeling Lucky" +// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, expectVal) // // Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { @@ -719,7 +720,8 @@ func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, u // HTTPBodyContainsf asserts that a specified handler returns a // body that contains a string. // -// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// expectVal := "I'm Feeling Lucky" +// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, expectVal, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { @@ -732,7 +734,8 @@ func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, // HTTPBodyNotContains asserts that a specified handler returns a // body that does not contain a string. // -// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// expectVal := "I'm Feeling Lucky" +// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, expectVal) // // Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { @@ -745,7 +748,8 @@ func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string // HTTPBodyNotContainsf asserts that a specified handler returns a // body that does not contain a string. // -// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// expectVal := "I'm Feeling Lucky" +// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, expectVal, "error message %s", "formatted") // // Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { @@ -1217,7 +1221,17 @@ func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) { // to fail the test immediately. The blocking behavior from before version 1.X.X // prevented this. Now it works as expected. Please adapt your tests accordingly. // -// a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(2*time.Second) +// externalValue.Store(true) +// }() +// +// a.Never(func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// return externalValue.Load() +// }, time.Second, 10*time.Millisecond, "condition must never become true within 1s") func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1237,7 +1251,17 @@ func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick ti // to fail the test immediately. The blocking behavior from before version 1.X.X // prevented this. Now it works as expected. Please adapt your tests accordingly. // -// a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// // 🀝 Always use thread-safe variables for concurrent access! +// externalValue := atomic.Bool{} +// go func() { +// time.Sleep(2*time.Second) +// externalValue.Store(true) +// }() +// +// a.Neverf(func() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// return externalValue.Load() +// }, time.Second, 10*time.Millisecond, "condition must never become true within 1s, more: %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 { h.Helper() @@ -1555,8 +1579,9 @@ func (a *Assertions) NotPanicsf(f assert.PanicTestFunc, msg string, args ...inte // NotRegexp asserts that a specified regexp does not match a string. // -// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") -// a.NotRegexp("^start", "it's not starting") +// expectVal := "not started" +// a.NotRegexp(regexp.MustCompile("^start"), expectVal) +// a.NotRegexp("^start", expectVal) func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1566,8 +1591,9 @@ func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...in // NotRegexpf asserts that a specified regexp does not match a string. // -// a.NotRegexpf(regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") -// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted") +// expectVal := "not started" +// a.NotRegexpf(regexp.MustCompile("^start"), expectVal, "error message %s", "formatted") +// a.NotRegexpf("^start", expectVal, "error message %s", "formatted") func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1651,7 +1677,7 @@ func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) { // Panics asserts that the code inside the specified PanicTestFunc panics. // -// a.Panics(func(){ GoCrazy() }) +// a.Panics(func(){ GoCrazy() }, "GoCrazy must panic") func (a *Assertions) Panics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1707,7 +1733,7 @@ func (a *Assertions) PanicsWithValuef(expected interface{}, f assert.PanicTestFu // Panicsf asserts that the code inside the specified PanicTestFunc panics. // -// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted") +// a.Panicsf(func(){ GoCrazy() }, "error message: %s", "formatted") func (a *Assertions) Panicsf(f assert.PanicTestFunc, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1739,8 +1765,9 @@ func (a *Assertions) Positivef(e interface{}, msg string, args ...interface{}) { // Regexp asserts that a specified regexp matches a string. // -// a.Regexp(regexp.MustCompile("start"), "it's starting") -// a.Regexp("start...$", "it's not starting") +// expectVal := "started" +// a.Regexp(regexp.MustCompile("^start"), expectVal) +// a.Regexp("^start", expectVal) func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1750,8 +1777,9 @@ func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...inter // Regexpf asserts that a specified regexp matches a string. // -// a.Regexpf(regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") -// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted") +// expectVal := "started" +// a.Regexpf(regexp.MustCompile("^start"), expectVal, "error message %s", "formatted") +// a.Regexpf("^start", expectVal, "error message %s", "formatted") func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() diff --git a/require/requirements_exit_test.go b/require/requirements_exit_test.go new file mode 100644 index 000000000..979b95e62 --- /dev/null +++ b/require/requirements_exit_test.go @@ -0,0 +1,164 @@ +package require + +import ( + "runtime" + "testing" + "time" + + assert "github.com/stretchr/testify/assert" +) + +func TestEventuallyGoexit(t *testing.T) { + t.Parallel() + + condition := func() bool { + runtime.Goexit() // require.Fail(t) will also call Goexit internally + panic("unreachable") + } + + t.Run("WithoutMessage", func(t *testing.T) { + outerT := new(MockT) // does not call runtime.Goexit immediately + Eventually(outerT, condition, 100*time.Millisecond, 20*time.Millisecond) + True(t, outerT.Failed(), "Check must fail") + Len(t, outerT.Errors(), 1, "There must be one error recorded") + err1 := outerT.Errors()[0] + Contains(t, err1.Error(), "Condition exited unexpectedly", "Error message must mention unexpected exit") + }) + + t.Run("WithMessage", func(t *testing.T) { + outerT := new(MockT) // does not call runtime.Goexit immediately + Eventually(outerT, condition, 100*time.Millisecond, 20*time.Millisecond, "error: %s", "details") + True(t, outerT.Failed(), "Check must fail") + Len(t, outerT.Errors(), 1, "There must be one error recorded") + err1 := outerT.Errors()[0] + Contains(t, err1.Error(), "Condition exited unexpectedly", "Error message must mention unexpected exit") + Contains(t, err1.Error(), "error: details", "Error message must contain formatted message") + }) +} + +func TestEventuallyWithTGoexit(t *testing.T) { + t.Parallel() + + condition := func(collect *assert.CollectT) { + runtime.Goexit() // require.Fail(t) will also call Goexit internally + panic("unreachable") + } + + t.Run("WithoutMessage", func(t *testing.T) { + mockT := new(MockT) // does not call runtime.Goexit immediately + EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond) + True(t, mockT.Failed(), "Check must fail") + Len(t, mockT.Errors(), 1, "There must be one error recorded") + Contains(t, mockT.Errors()[0].Error(), "Condition exited unexpectedly", "Error message must mention unexpected exit") + }) + + t.Run("WithMessage", func(t *testing.T) { + mockT := new(MockT) // does not call runtime.Goexit immediately + EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond, "error: %s", "details") + True(t, mockT.Failed(), "Check must fail") + Len(t, mockT.Errors(), 1, "There must be one error recorded") + + err1 := mockT.Errors()[0] + Contains(t, err1.Error(), "Condition exited unexpectedly", "Error message must mention unexpected exit") + Contains(t, err1.Error(), "error: details", "Error message must contain formatted message") + }) +} + +func TestEventuallyWithTFail(t *testing.T) { + t.Parallel() + + outerT := new(MockT) + condition := func(collect *assert.CollectT) { + // tick assertion failure + assert.Fail(collect, "tick error") + + // stop the entire test immediately (outer assertion) + outerT.FailNow() // MockT does not call Goexit internally + runtime.Goexit() // so we need to call it here to simulate the behavior + panic("unreachable") + } + + EventuallyWithT(outerT, condition, 100*time.Millisecond, 20*time.Millisecond) + True(t, outerT.Failed(), "Check must fail") + Len(t, outerT.Errors(), 2, "There must be two errors recorded") + err1, err2 := outerT.Errors()[0], outerT.Errors()[1] + Contains(t, err1.Error(), "tick error", "First error must be tick error") + Contains(t, err2.Error(), "Condition exited unexpectedly", "Second error must mention unexpected exit") +} + +func TestTestingTFailNow(t *testing.T) { + t.Parallel() + + tt := new(testing.T) + done := make(chan struct{}) + // Run in a separate goroutine to capture the Goexit behavior. + // This avoid test panics from the unssupported Goexit call in the main test goroutine. + // Note that this will trigger linter warnings about goroutines in tests. (SA2002) + go func(tt *testing.T) { + defer close(done) + defer func() { + r := recover() // [runtime.Goexit] does not trigger a panic + // If we see a panic here, the condition function misbehaved + Nil(t, r, "Condition function must not panic: %v", r) + }() + tt.Errorf("test error") + tt.FailNow() + panic("unreachable") + }(tt) + <-done + True(t, tt.Failed(), "testing.T must be marked as failed") +} + +func TestEventuallyTestingTFailNow(t *testing.T) { + tt := new(testing.T) + + count := 0 + done := make(chan struct{}) + + // Run Eventually in a separate goroutine to capture the Goexit behavior. + // This avoid test panics from the unssupported Goexit call in the main test goroutine. + // Note that this will trigger linter warnings about goroutines in tests. (SA2002) + go func(tt *testing.T) { + defer close(done) + defer func() { + r := recover() // [runtime.Goexit] does not trigger a panic + // If we see a panic here, the condition function misbehaved + Nil(t, r, "Condition function must not panic: %v", r) + }() + condition := func() bool { + // tick assertion failure + count++ + tt.Error("tick error") + tt.FailNow() + panic("unreachable") + } + Eventually(tt, condition, 100*time.Millisecond, 20*time.Millisecond) + }(tt) + <-done + + True(t, tt.Failed(), "Check must fail") + Equal(t, 1, count, "Condition function must have been called once") +} + +func TestEventuallyFailNow(t *testing.T) { + t.Parallel() + + outerT := new(MockT) + condition := func() bool { + // tick assertion failure + assert.Fail(outerT, "tick error") + + // stop the entire test immediately (outer assertion) + outerT.FailNow() // MockT does not call Goexit internally + runtime.Goexit() // so we need to call it here to simulate the behavior + panic("unreachable") + } + + Eventually(outerT, condition, 100*time.Millisecond, 20*time.Millisecond) + True(t, outerT.Failed(), "Check must fail") + True(t, outerT.calledFailNow(), "FailNow must have been called") + Len(t, outerT.Errors(), 2, "There must be two errors recorded") + err1, err2 := outerT.Errors()[0], outerT.Errors()[1] + Contains(t, err1.Error(), "tick error", "First error must be tick error") + Contains(t, err2.Error(), "Condition exited unexpectedly", "Second error must mention unexpected exit") +} diff --git a/require/requirements_test.go b/require/requirements_test.go index 7cb63a554..59dee9ed0 100644 --- a/require/requirements_test.go +++ b/require/requirements_test.go @@ -25,19 +25,30 @@ func (a *AssertionTesterConformingObject) TestMethod() { type AssertionTesterNonConformingObject struct { } +// MockT is a mock implementation of testing.T that embeds assert.CollectT. +// It differs from assert.CollectT by not stopping execution when FailNow is called. type MockT struct { - Failed bool -} + // CollectT is embedded to provide assertion methods and error collection. + assert.CollectT -// Helper is like [testing.T.Helper] but does nothing. -func (MockT) Helper() {} + // Indicates whether the test has finished and would have stopped execution in a real testing.T. + // This overrides the CollectT's finished state to allow checking it after FailNow is called. + // The name 'finished' was adopted from testing.T's internal state field. + finished bool +} -func (t *MockT) FailNow() { - t.Failed = true +// calledFailNow returns whether FailNow was called +// Note that MockT does not actually stop execution when FailNow is called, +// because that would prevent test analysis after the call. +func (t *MockT) calledFailNow() bool { + return t.finished } -func (t *MockT) Errorf(format string, args ...interface{}) { - _, _ = format, args +// FailNow marks the function as failed without stopping execution. +// This overrides the CollectT's FailNow to allow checking the finished state after the call. +func (t *MockT) FailNow() { + t.CollectT.Fail() + t.finished = true } func TestImplements(t *testing.T) { @@ -47,7 +58,7 @@ func TestImplements(t *testing.T) { mockT := new(MockT) Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -59,11 +70,42 @@ func TestIsType(t *testing.T) { mockT := new(MockT) IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } +func TestMockTFailNow(t *testing.T) { + t.Parallel() + + m := new(MockT) + m.Errorf("test error") + + m.FailNow() + Len(t, m.Errors(), 1, "MockT should have one recorded error") + True(t, m.Failed(), "MockT should be marked as failed after Errorf is called") + True(t, m.calledFailNow(), "MockT should indicate that FailNow was called") + + // In real testing.T, execution would stop after FailNow is called. + // However, in MockT, execution continues to allow test analysis. + // So we can still call Errorf again. In the future, we might reject Errorf after FailNow. + m.Errorf("error after fail") + Len(t, m.Errors(), 2, "MockT should have two recorded errors") +} + +func TestMockTFailNowWithoutError(t *testing.T) { + t.Parallel() + m := new(MockT) + + // Also check if we can call FailNow multiple times without prior Errorf calls. + for i := 0; i < 3; i++ { + m.FailNow() + True(t, m.Failed(), "MockT should be marked as failed after FailNow is called") + Len(t, m.Errors(), 0, "MockT should have no recorded errors") + True(t, m.calledFailNow(), "MockT should indicate that FailNow was called") + } +} + func TestEqual(t *testing.T) { t.Parallel() @@ -71,7 +113,7 @@ func TestEqual(t *testing.T) { mockT := new(MockT) Equal(mockT, 1, 2) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } @@ -83,7 +125,7 @@ func TestNotEqual(t *testing.T) { NotEqual(t, 1, 2) mockT := new(MockT) NotEqual(mockT, 2, 2) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -99,7 +141,7 @@ func TestExactly(t *testing.T) { mockT := new(MockT) Exactly(mockT, a, c) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -111,7 +153,7 @@ func TestNotNil(t *testing.T) { mockT := new(MockT) NotNil(mockT, nil) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -123,7 +165,7 @@ func TestNil(t *testing.T) { mockT := new(MockT) Nil(mockT, new(AssertionTesterConformingObject)) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -135,7 +177,7 @@ func TestTrue(t *testing.T) { mockT := new(MockT) True(mockT, false) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -147,7 +189,7 @@ func TestFalse(t *testing.T) { mockT := new(MockT) False(mockT, true) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -159,7 +201,7 @@ func TestContains(t *testing.T) { mockT := new(MockT) Contains(mockT, "Hello World", "Salut") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -171,7 +213,7 @@ func TestNotContains(t *testing.T) { mockT := new(MockT) NotContains(mockT, "Hello World", "Hello") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -185,7 +227,7 @@ func TestPanics(t *testing.T) { mockT := new(MockT) Panics(mockT, func() {}) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -199,7 +241,7 @@ func TestNotPanics(t *testing.T) { NotPanics(mockT, func() { panic("Panic!") }) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -211,7 +253,7 @@ func TestNoError(t *testing.T) { mockT := new(MockT) NoError(mockT, errors.New("some error")) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -223,7 +265,7 @@ func TestError(t *testing.T) { mockT := new(MockT) Error(mockT, nil) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -235,7 +277,7 @@ func TestErrorContains(t *testing.T) { mockT := new(MockT) ErrorContains(mockT, errors.New("some error"), "different error") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -247,7 +289,7 @@ func TestEqualError(t *testing.T) { mockT := new(MockT) EqualError(mockT, errors.New("some error"), "Not some error") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -259,7 +301,7 @@ func TestEmpty(t *testing.T) { mockT := new(MockT) Empty(mockT, "x") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -271,7 +313,7 @@ func TestNotEmpty(t *testing.T) { mockT := new(MockT) NotEmpty(mockT, "") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -286,7 +328,7 @@ func TestWithinDuration(t *testing.T) { mockT := new(MockT) WithinDuration(mockT, a, b, 5*time.Second) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -298,7 +340,7 @@ func TestInDelta(t *testing.T) { mockT := new(MockT) InDelta(mockT, 1, 2, 0.5) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -310,7 +352,7 @@ func TestZero(t *testing.T) { mockT := new(MockT) Zero(mockT, "x") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -322,7 +364,7 @@ func TestNotZero(t *testing.T) { mockT := new(MockT) NotZero(mockT, "") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -332,7 +374,7 @@ func TestJSONEq_EqualSONString(t *testing.T) { mockT := new(MockT) JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -342,7 +384,7 @@ func TestJSONEq_EquivalentButNotEqual(t *testing.T) { mockT := new(MockT) JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -353,7 +395,7 @@ func TestJSONEq_HashOfArraysAndHashes(t *testing.T) { mockT := new(MockT) JSONEq(mockT, "{\r\n\t\"numeric\": 1.5,\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]],\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\"\r\n}", "{\r\n\t\"numeric\": 1.5,\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\",\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]]\r\n}") - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -363,7 +405,7 @@ func TestJSONEq_Array(t *testing.T) { mockT := new(MockT) JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -373,7 +415,7 @@ func TestJSONEq_HashAndArrayNotEquivalent(t *testing.T) { mockT := new(MockT) JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -383,7 +425,7 @@ func TestJSONEq_HashesNotEquivalent(t *testing.T) { mockT := new(MockT) JSONEq(mockT, `{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -393,7 +435,7 @@ func TestJSONEq_ActualIsNotJSON(t *testing.T) { mockT := new(MockT) JSONEq(mockT, `{"foo": "bar"}`, "Not JSON") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -403,7 +445,7 @@ func TestJSONEq_ExpectedIsNotJSON(t *testing.T) { mockT := new(MockT) JSONEq(mockT, "Not JSON", `{"foo": "bar", "hello": "world"}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -413,7 +455,7 @@ func TestJSONEq_ExpectedAndActualNotJSON(t *testing.T) { mockT := new(MockT) JSONEq(mockT, "Not JSON", "Not JSON") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -423,7 +465,7 @@ func TestJSONEq_ArraysOfDifferentOrder(t *testing.T) { mockT := new(MockT) JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -433,7 +475,7 @@ func TestYAMLEq_EqualYAMLString(t *testing.T) { mockT := new(MockT) YAMLEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -443,7 +485,7 @@ func TestYAMLEq_EquivalentButNotEqual(t *testing.T) { mockT := new(MockT) YAMLEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -478,7 +520,7 @@ array: - ["nested", "array", 5.5] ` YAMLEq(mockT, expected, actual) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -488,7 +530,7 @@ func TestYAMLEq_Array(t *testing.T) { mockT := new(MockT) YAMLEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -498,7 +540,7 @@ func TestYAMLEq_HashAndArrayNotEquivalent(t *testing.T) { mockT := new(MockT) YAMLEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -508,7 +550,7 @@ func TestYAMLEq_HashesNotEquivalent(t *testing.T) { mockT := new(MockT) YAMLEq(mockT, `{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -518,7 +560,7 @@ func TestYAMLEq_ActualIsSimpleString(t *testing.T) { mockT := new(MockT) YAMLEq(mockT, `{"foo": "bar"}`, "Simple String") - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -528,7 +570,7 @@ func TestYAMLEq_ExpectedIsSimpleString(t *testing.T) { mockT := new(MockT) YAMLEq(mockT, "Simple String", `{"foo": "bar", "hello": "world"}`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -538,7 +580,7 @@ func TestYAMLEq_ExpectedAndActualSimpleString(t *testing.T) { mockT := new(MockT) YAMLEq(mockT, "Simple String", "Simple String") - if mockT.Failed { + if mockT.Failed() { t.Error("Check should pass") } } @@ -548,7 +590,7 @@ func TestYAMLEq_ArraysOfDifferentOrder(t *testing.T) { mockT := new(MockT) YAMLEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) - if !mockT.Failed { + if !mockT.Failed() { t.Error("Check should fail") } } @@ -768,7 +810,7 @@ func TestEventuallyWithTFalse(t *testing.T) { } EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond) - True(t, mockT.Failed, "Check should fail") + True(t, mockT.Failed(), "Check should fail") } func TestEventuallyWithTTrue(t *testing.T) { @@ -785,6 +827,6 @@ func TestEventuallyWithTTrue(t *testing.T) { } EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond) - False(t, mockT.Failed, "Check should pass") + False(t, mockT.Failed(), "Check should pass") Equal(t, 2, counter, "Condition is expected to be called 2 times") } From cc58d206c20aec9e81b36bce6e645b8f74b65165 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Tue, 21 Oct 2025 23:04:06 +0200 Subject: [PATCH 12/16] ignore local AI helper docs --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6e1bb22a2..0556df3ff 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ _testmain.go .DS_Store +# AI helpers +*-discussion.md + # Output of "go test -c" /assert/assert.test /require/require.test From 0d00fb5a15fcd2c906d6c4d64b61d6a2a1630a79 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Tue, 21 Oct 2025 23:26:03 +0200 Subject: [PATCH 13/16] adjust comment to explain need for calling FailNow on collect --- assert/assertions.go | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/assert/assertions.go b/assert/assertions.go index 7564f8922..2948af19e 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -2202,25 +2202,34 @@ func (c *CollectT) calledFailNow() bool { // function that the condition function can use to call assertions on. // These assertions are specific to each run of the condition function in each tick. // -// The supplied [CollectT] collects all errors from one tick. If no errors are -// collected, the condition is considered successful ("met") and EventuallyWithT -// returns true. If there are collected errors, the condition is considered -// failed for that tick ("not met") and the next tick is scheduled until -// waitFor duration is reached. +// The condition is considered successful ("met") if: +// +// 1. No errors are collected. +// +// 2. And 'collect' was not marked as failed via Fail or FailNow. +// +// 3. And the parent 't' did not fail fast via FailNow. +// +// EventuallyWithT returns true as soon as the condition is met within the +// waitFor duration. +// +// If the condition is "not met" and the parent 't' did not fail fast, +// EventuallyWithT schedules the next tick. This continues until either the +// condition is met or the waitFor duration elapses. // // If the condition does not complete successfully before waitFor expires, the -// collected errors of the last tick are copied to t before EventuallyWithT +// collected errors of the last tick are copied to 't' before EventuallyWithT // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly and NO errors are collected, a call to -// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the -// condition function. In this case, EventuallyWithT fails the test immediately -// with "Condition exited unexpectedly" and returns false. +// If the condition exits unexpectedly, i.e., not returning normally or by +// calling FailNow on the supplied 'collect', the test fails immediately with +// "Condition exited unexpectedly" and EventuallyWithT returns false. // // πŸ’‘ Tick Assertions vs. Parent Test Assertions // - Use tick assertions and requirements on the supplied 'collect' and not -// on the parent 't'. Only the last tick's collected errors are copied to 't'. -// - Use parent test requirements on the parent 't' to fail the entire test +// on the parent 't'. +// - The last tick errors are always copied to 't' in case of failure. +// - On the parent 't' only use requirements for failing the entire test immediately. // - Do not use assertions on the parent 't', since this would affect all ticks // and create test noise. // From 4cb9260817a0a3af620fcd4870543103dcd44f2f Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Tue, 21 Oct 2025 23:39:40 +0200 Subject: [PATCH 14/16] simplify wording a bit --- EVENTUALLY.md | 121 ++++++++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 59 deletions(-) diff --git a/EVENTUALLY.md b/EVENTUALLY.md index 80b20acf6..6dfb7ea5c 100644 --- a/EVENTUALLY.md +++ b/EVENTUALLY.md @@ -1,8 +1,16 @@ # Eventually -`assert.Eventually` waits for a user supplied condition to become `true`. It is -most often used when the code under test sets a value from another goroutine and -the test needs to poll until the desired state is reached. +`assert.Eventually` waits for a user-supplied condition to become `true`. It is +most often used when code under test sets a value from another goroutine, so the +test needs to poll until the desired state is reached. This guide also covers +`assert.EventuallyWithT` and `assert.Never`. + +## Variants at a glance + +- `Eventually` polls until the condition returns `true` or the timeout fires. +- `EventuallyWithT` retries with a fresh `*assert.CollectT` each tick. It keeps + only the last tick's errors. +- `Never` makes sure the condition stays `false` for the whole timeout. ## Signature and scheduling @@ -16,63 +24,60 @@ func Eventually( ) bool ``` -- `condition` is called on its own goroutine. The first evaluation happens - immediately. Subsequent evaluations are triggered every `tick` as long as the - condition keeps returning `false`. -- `waitFor` defines the maximum amount of time `Eventually` will spend polling. If - the deadline expires, the assertion fails with "Condition never satisfied" and - the optional `msgAndArgs` are appended to the failure output. -- The return value is `true` when the condition succeeds before the timeout and - `false` otherwise. The assertion also reports the failure through `t`. -- All state that is shared between the test and the condition must be protected - for concurrent access via mutexes, `atomic` types, and other synchronization - mechanisms. +- `condition` runs on its own goroutine. The first check happens immediately. + Later checks run every `tick` while it keeps returning `false`. +- `waitFor` sets the maximum polling time. When the deadline passes, the + assertion fails with "Condition never satisfied" and appends any + `msgAndArgs` to the output. +- The return value is `true` when the condition succeeds before the timeout. + It is `false` otherwise. The assertion also reports failures through `t`. + +> [!Note] +> You must protect shared state between the test and the condition with +> mutexes, `atomic` types, or other synchronization tools. ## Exit and panic behavior -Since [PR #1809](https://github.com/stretchr/testify/pull/1809) `assert.Eventually` -distinguishes the different ways the condition goroutine can terminate: - -- **Condition returns `true`:** `Eventually` stops polling immediately and - succeeds. -- **Condition times out:** `Eventually` keeps polling until `waitFor` elapses and - then fails the test with "Condition never satisfied". -- **Condition panics:** The panic is *not* recovered. The Go runtime terminates - the process, prints the panic message and stack trace to standard error, and - the test run stops. This matches the normal behavior of panics in goroutines. -- **Condition calls `runtime.Goexit`:** `Eventually` now fails the test - immediately with "Condition exited unexpectedly". - Before [PR #1809](https://github.com/stretchr/testify/pull/1809) - the assertion waited until `waitFor` expired, causing tests that called - `t.FailNow()` (or `require.*` helpers that use it) to hang. - The new behavior surfaces the failure as soon as it happens. +Since [PR #1809](https://github.com/stretchr/testify/pull/1809), +`assert.Eventually` handles each way the condition goroutine can finish: + +- **Condition returns `true`:** Polling stops at once and the assertion passes. +- **Condition times out:** Polling keeps running until `waitFor` expires and the + test fails with "Condition never satisfied". +- **Condition panics:** The panic is not recovered. The Go runtime prints the + panic and stack trace, then stops the test run. This is the normal goroutine + panic path. +- **Condition calls `runtime.Goexit`:** The assertion now fails immediately with + "Condition exited unexpectedly". Earlier versions waited for `waitFor` and + could hang after `t.FailNow()` or `require.*`. ### `EventuallyWithT` specifics -`assert.EventuallyWithT` runs the same polling loop but supplies each tick with -a fresh `*assert.CollectT`: +`assert.EventuallyWithT` runs the same polling loop but gives each tick a new +`*assert.CollectT` named `collect`: -- Returning from the closure without recording errors on `collect` marks the - condition as satisfied and the assertion succeeds immediately. +- Returning from the closure without errors on `collect` marks the condition as + satisfied and the assertion succeeds right away. - Recording errors on `collect` (via `collect.Errorf`, `assert.*(collect, ...)` - helpers, or `collect.FailNow()`) marks just that tick as failed. The polling - continues, and if the timeout expires, the errors captured during the final - tick are replayed on the parent `t` before emitting "Condition never satisfied". -- If the closure exits via `runtime.Goexit` *without* first recording errors on - `collect`β€”for example by calling `require.*` on the parent `t`β€”the assertion + helpers, or `collect.FailNow()`) fails only that tick. Polling keeps going, + and if the timeout hits, the last tick's errors replay on the parent `t` + before "Condition never satisfied". +- Call `collect.FailNow()` to exit the tick quickly and move to the next poll. +- If the closure exits via `runtime.Goexit` without first recording errors on + `collect`β€”for example, by calling `require.*` on the parent `t`β€”the assertion fails immediately with "Condition exited unexpectedly". -- Panics behave the same as in `assert.Eventually`: they are not recovered and - crash the test process. +- Panics behave the same as in `assert.Eventually`. They are not recovered and + stop the test process. -Use `collect` for tick-scoped assertions you want to keep retrying, and call -`require.*` on the parent `t` when you want the test to stop right away. The -same rules apply to `require.EventuallyWithT` and its helpers. +Use `collect` for assertions you want to retry on each tick. Call `require.*` +on the parent `t` when you want the test to stop immediately. The same rules +apply to `require.EventuallyWithT` and its helpers. ## `Never` specifics -`assert.Never` runs the same polling loop as `Eventually` but expects the -condition to always return `false`. If the condition ever returns `true`, the -assertion fails immediately with "Condition satisfied". +`assert.Never` uses the same polling loop as `Eventually` but expects the +condition to stay `false`. If the condition ever returns `true`, the assertion +fails immediately with "Condition satisfied". - If the condition panics, the panic is not recovered and the test process terminates. @@ -81,18 +86,16 @@ assertion fails immediately with "Condition satisfied". - These behaviors match those of `assert.Eventually`. > [!Note] -> Since `Never` needs to run until the timeout expires to be successful, -> it cannot succeed early like `Eventually`. Prefer `Eventually` when possible -> to keep tests fast. +> `Never` only succeeds when it lasts the full timeout, so it cannot finish +> early. Prefer using `Eventually` to keep tests fast. ## Usage tips -- Pick a `tick` that balances fast feedback with the overhead of running the - condition. Extremely small ticks can create busy loops. -- Run your test suite with `go test -race`, esp when `Eventually` coordinates - with other goroutines. Data races are a more common source of flakiness - than the assertion logic itself. -- If the condition needs to report rich diagnostics or multiple errors, prefer - `assert.EventuallyWithT` and record failures on the provided `CollectT` value. -- It is safe to call `require.*` helpers on the parent `t` from within the - condition closure to stop the test immediately. +- Pick a `tick` that balances quick feedback with the work the condition does. + Very small ticks can turn into a busy loop. +- Run `go test -race` when `Eventually` works with other goroutines. Data races + cause more flakiness than the assertion itself. +- Use `assert.EventuallyWithT` when you need richer diagnostics or multiple + errors. Record the failures on the provided `CollectT` value. +- Call `require.*` on the parent `t` inside the condition when you need to stop + the test immediately. From 802e03306d6d8aa63df2363bcc38fb6db67cf7b9 Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Tue, 21 Oct 2025 23:39:40 +0200 Subject: [PATCH 15/16] simplify wording a bit, update generated code --- assert/assertion_format.go | 33 ++++++++++++------- assert/assertion_forward.go | 66 +++++++++++++++++++++++-------------- require/require.go | 66 +++++++++++++++++++++++-------------- require/require_forward.go | 66 +++++++++++++++++++++++-------------- 4 files changed, 147 insertions(+), 84 deletions(-) diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 6e7baca79..c7aa48d16 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -230,25 +230,34 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // function that the condition function can use to call assertions on. // These assertions are specific to each run of the condition function in each tick. // -// The supplied [CollectT] collects all errors from one tick. If no errors are -// collected, the condition is considered successful ("met") and EventuallyWithTf -// returns true. If there are collected errors, the condition is considered -// failed for that tick ("not met") and the next tick is scheduled until -// waitFor duration is reached. +// The condition is considered successful ("met") if: +// +// 1. No errors are collected. +// +// 2. And 'collect' was not marked as failed via Fail or FailNow. +// +// 3. And the parent 't' did not fail fast via FailNow. +// +// EventuallyWithTf returns true as soon as the condition is met within the +// waitFor duration. +// +// If the condition is "not met" and the parent 't' did not fail fast, +// EventuallyWithTf schedules the next tick. This continues until either the +// condition is met or the waitFor duration elapses. // // If the condition does not complete successfully before waitFor expires, the -// collected errors of the last tick are copied to t before EventuallyWithTf +// collected errors of the last tick are copied to 't' before EventuallyWithTf // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly and NO errors are collected, a call to -// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the -// condition function. In this case, EventuallyWithTf fails the test immediately -// with "Condition exited unexpectedly" and returns false. +// If the condition exits unexpectedly, i.e., not returning normally or by +// calling FailNow on the supplied 'collect', the test fails immediately with +// "Condition exited unexpectedly" and EventuallyWithTf returns false. // // πŸ’‘ Tick Assertions vs. Parent Test Assertions // - Use tick assertions and requirements on the supplied 'collect' and not -// on the parent 't'. Only the last tick's collected errors are copied to 't'. -// - Use parent test requirements on the parent 't' to fail the entire test +// on the parent 't'. +// - The last tick errors are always copied to 't' in case of failure. +// - On the parent 't' only use requirements for failing the entire test immediately. // - Do not use assertions on the parent 't', since this would affect all ticks // and create test noise. // diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index acd534445..4e9e6c8c1 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -387,25 +387,34 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // function that the condition function can use to call assertions on. // These assertions are specific to each run of the condition function in each tick. // -// The supplied [CollectT] collects all errors from one tick. If no errors are -// collected, the condition is considered successful ("met") and EventuallyWithT -// returns true. If there are collected errors, the condition is considered -// failed for that tick ("not met") and the next tick is scheduled until -// waitFor duration is reached. +// The condition is considered successful ("met") if: +// +// 1. No errors are collected. +// +// 2. And 'collect' was not marked as failed via Fail or FailNow. +// +// 3. And the parent 't' did not fail fast via FailNow. +// +// EventuallyWithT returns true as soon as the condition is met within the +// waitFor duration. +// +// If the condition is "not met" and the parent 't' did not fail fast, +// EventuallyWithT schedules the next tick. This continues until either the +// condition is met or the waitFor duration elapses. // // If the condition does not complete successfully before waitFor expires, the -// collected errors of the last tick are copied to t before EventuallyWithT +// collected errors of the last tick are copied to 't' before EventuallyWithT // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly and NO errors are collected, a call to -// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the -// condition function. In this case, EventuallyWithT fails the test immediately -// with "Condition exited unexpectedly" and returns false. +// If the condition exits unexpectedly, i.e., not returning normally or by +// calling FailNow on the supplied 'collect', the test fails immediately with +// "Condition exited unexpectedly" and EventuallyWithT returns false. // // πŸ’‘ Tick Assertions vs. Parent Test Assertions // - Use tick assertions and requirements on the supplied 'collect' and not -// on the parent 't'. Only the last tick's collected errors are copied to 't'. -// - Use parent test requirements on the parent 't' to fail the entire test +// on the parent 't'. +// - The last tick errors are always copied to 't' in case of failure. +// - On the parent 't' only use requirements for failing the entire test immediately. // - Do not use assertions on the parent 't', since this would affect all ticks // and create test noise. // @@ -448,25 +457,34 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // function that the condition function can use to call assertions on. // These assertions are specific to each run of the condition function in each tick. // -// The supplied [CollectT] collects all errors from one tick. If no errors are -// collected, the condition is considered successful ("met") and EventuallyWithTf -// returns true. If there are collected errors, the condition is considered -// failed for that tick ("not met") and the next tick is scheduled until -// waitFor duration is reached. +// The condition is considered successful ("met") if: +// +// 1. No errors are collected. +// +// 2. And 'collect' was not marked as failed via Fail or FailNow. +// +// 3. And the parent 't' did not fail fast via FailNow. +// +// EventuallyWithTf returns true as soon as the condition is met within the +// waitFor duration. +// +// If the condition is "not met" and the parent 't' did not fail fast, +// EventuallyWithTf schedules the next tick. This continues until either the +// condition is met or the waitFor duration elapses. // // If the condition does not complete successfully before waitFor expires, the -// collected errors of the last tick are copied to t before EventuallyWithTf +// collected errors of the last tick are copied to 't' before EventuallyWithTf // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly and NO errors are collected, a call to -// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the -// condition function. In this case, EventuallyWithTf fails the test immediately -// with "Condition exited unexpectedly" and returns false. +// If the condition exits unexpectedly, i.e., not returning normally or by +// calling FailNow on the supplied 'collect', the test fails immediately with +// "Condition exited unexpectedly" and EventuallyWithTf returns false. // // πŸ’‘ Tick Assertions vs. Parent Test Assertions // - Use tick assertions and requirements on the supplied 'collect' and not -// on the parent 't'. Only the last tick's collected errors are copied to 't'. -// - Use parent test requirements on the parent 't' to fail the entire test +// on the parent 't'. +// - The last tick errors are always copied to 't' in case of failure. +// - On the parent 't' only use requirements for failing the entire test immediately. // - Do not use assertions on the parent 't', since this would affect all ticks // and create test noise. // diff --git a/require/require.go b/require/require.go index d36dfd706..2336d3ba6 100644 --- a/require/require.go +++ b/require/require.go @@ -469,25 +469,34 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t // function that the condition function can use to call assertions on. // These assertions are specific to each run of the condition function in each tick. // -// The supplied [CollectT] collects all errors from one tick. If no errors are -// collected, the condition is considered successful ("met") and EventuallyWithT -// returns true. If there are collected errors, the condition is considered -// failed for that tick ("not met") and the next tick is scheduled until -// waitFor duration is reached. +// The condition is considered successful ("met") if: +// +// 1. No errors are collected. +// +// 2. And 'collect' was not marked as failed via Fail or FailNow. +// +// 3. And the parent 't' did not fail fast via FailNow. +// +// EventuallyWithT returns true as soon as the condition is met within the +// waitFor duration. +// +// If the condition is "not met" and the parent 't' did not fail fast, +// EventuallyWithT schedules the next tick. This continues until either the +// condition is met or the waitFor duration elapses. // // If the condition does not complete successfully before waitFor expires, the -// collected errors of the last tick are copied to t before EventuallyWithT +// collected errors of the last tick are copied to 't' before EventuallyWithT // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly and NO errors are collected, a call to -// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the -// condition function. In this case, EventuallyWithT fails the test immediately -// with "Condition exited unexpectedly" and returns false. +// If the condition exits unexpectedly, i.e., not returning normally or by +// calling FailNow on the supplied 'collect', the test fails immediately with +// "Condition exited unexpectedly" and EventuallyWithT returns false. // // πŸ’‘ Tick Assertions vs. Parent Test Assertions // - Use tick assertions and requirements on the supplied 'collect' and not -// on the parent 't'. Only the last tick's collected errors are copied to 't'. -// - Use parent test requirements on the parent 't' to fail the entire test +// on the parent 't'. +// - The last tick errors are always copied to 't' in case of failure. +// - On the parent 't' only use requirements for failing the entire test immediately. // - Do not use assertions on the parent 't', since this would affect all ticks // and create test noise. // @@ -533,25 +542,34 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF // function that the condition function can use to call assertions on. // These assertions are specific to each run of the condition function in each tick. // -// The supplied [CollectT] collects all errors from one tick. If no errors are -// collected, the condition is considered successful ("met") and EventuallyWithTf -// returns true. If there are collected errors, the condition is considered -// failed for that tick ("not met") and the next tick is scheduled until -// waitFor duration is reached. +// The condition is considered successful ("met") if: +// +// 1. No errors are collected. +// +// 2. And 'collect' was not marked as failed via Fail or FailNow. +// +// 3. And the parent 't' did not fail fast via FailNow. +// +// EventuallyWithTf returns true as soon as the condition is met within the +// waitFor duration. +// +// If the condition is "not met" and the parent 't' did not fail fast, +// EventuallyWithTf schedules the next tick. This continues until either the +// condition is met or the waitFor duration elapses. // // If the condition does not complete successfully before waitFor expires, the -// collected errors of the last tick are copied to t before EventuallyWithTf +// collected errors of the last tick are copied to 't' before EventuallyWithTf // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly and NO errors are collected, a call to -// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the -// condition function. In this case, EventuallyWithTf fails the test immediately -// with "Condition exited unexpectedly" and returns false. +// If the condition exits unexpectedly, i.e., not returning normally or by +// calling FailNow on the supplied 'collect', the test fails immediately with +// "Condition exited unexpectedly" and EventuallyWithTf returns false. // // πŸ’‘ Tick Assertions vs. Parent Test Assertions // - Use tick assertions and requirements on the supplied 'collect' and not -// on the parent 't'. Only the last tick's collected errors are copied to 't'. -// - Use parent test requirements on the parent 't' to fail the entire test +// on the parent 't'. +// - The last tick errors are always copied to 't' in case of failure. +// - On the parent 't' only use requirements for failing the entire test immediately. // - Do not use assertions on the parent 't', since this would affect all ticks // and create test noise. // diff --git a/require/require_forward.go b/require/require_forward.go index 4db92065e..fbd32100d 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -388,25 +388,34 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti // function that the condition function can use to call assertions on. // These assertions are specific to each run of the condition function in each tick. // -// The supplied [CollectT] collects all errors from one tick. If no errors are -// collected, the condition is considered successful ("met") and EventuallyWithT -// returns true. If there are collected errors, the condition is considered -// failed for that tick ("not met") and the next tick is scheduled until -// waitFor duration is reached. +// The condition is considered successful ("met") if: +// +// 1. No errors are collected. +// +// 2. And 'collect' was not marked as failed via Fail or FailNow. +// +// 3. And the parent 't' did not fail fast via FailNow. +// +// EventuallyWithT returns true as soon as the condition is met within the +// waitFor duration. +// +// If the condition is "not met" and the parent 't' did not fail fast, +// EventuallyWithT schedules the next tick. This continues until either the +// condition is met or the waitFor duration elapses. // // If the condition does not complete successfully before waitFor expires, the -// collected errors of the last tick are copied to t before EventuallyWithT +// collected errors of the last tick are copied to 't' before EventuallyWithT // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly and NO errors are collected, a call to -// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the -// condition function. In this case, EventuallyWithT fails the test immediately -// with "Condition exited unexpectedly" and returns false. +// If the condition exits unexpectedly, i.e., not returning normally or by +// calling FailNow on the supplied 'collect', the test fails immediately with +// "Condition exited unexpectedly" and EventuallyWithT returns false. // // πŸ’‘ Tick Assertions vs. Parent Test Assertions // - Use tick assertions and requirements on the supplied 'collect' and not -// on the parent 't'. Only the last tick's collected errors are copied to 't'. -// - Use parent test requirements on the parent 't' to fail the entire test +// on the parent 't'. +// - The last tick errors are always copied to 't' in case of failure. +// - On the parent 't' only use requirements for failing the entire test immediately. // - Do not use assertions on the parent 't', since this would affect all ticks // and create test noise. // @@ -449,25 +458,34 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w // function that the condition function can use to call assertions on. // These assertions are specific to each run of the condition function in each tick. // -// The supplied [CollectT] collects all errors from one tick. If no errors are -// collected, the condition is considered successful ("met") and EventuallyWithTf -// returns true. If there are collected errors, the condition is considered -// failed for that tick ("not met") and the next tick is scheduled until -// waitFor duration is reached. +// The condition is considered successful ("met") if: +// +// 1. No errors are collected. +// +// 2. And 'collect' was not marked as failed via Fail or FailNow. +// +// 3. And the parent 't' did not fail fast via FailNow. +// +// EventuallyWithTf returns true as soon as the condition is met within the +// waitFor duration. +// +// If the condition is "not met" and the parent 't' did not fail fast, +// EventuallyWithTf schedules the next tick. This continues until either the +// condition is met or the waitFor duration elapses. // // If the condition does not complete successfully before waitFor expires, the -// collected errors of the last tick are copied to t before EventuallyWithTf +// collected errors of the last tick are copied to 't' before EventuallyWithTf // fails the test with "Condition never satisfied" and returns false. // -// If the condition exits unexpectedly and NO errors are collected, a call to -// [runtime.Goexit] or a t.FailNow() on the PARENT 't' has happened inside the -// condition function. In this case, EventuallyWithTf fails the test immediately -// with "Condition exited unexpectedly" and returns false. +// If the condition exits unexpectedly, i.e., not returning normally or by +// calling FailNow on the supplied 'collect', the test fails immediately with +// "Condition exited unexpectedly" and EventuallyWithTf returns false. // // πŸ’‘ Tick Assertions vs. Parent Test Assertions // - Use tick assertions and requirements on the supplied 'collect' and not -// on the parent 't'. Only the last tick's collected errors are copied to 't'. -// - Use parent test requirements on the parent 't' to fail the entire test +// on the parent 't'. +// - The last tick errors are always copied to 't' in case of failure. +// - On the parent 't' only use requirements for failing the entire test immediately. // - Do not use assertions on the parent 't', since this would affect all ticks // and create test noise. // From a12b920d0d88559c97bf088a3ab3f97cad4b4cef Mon Sep 17 00:00:00 2001 From: Uwe Jugel Date: Tue, 21 Oct 2025 23:59:07 +0200 Subject: [PATCH 16/16] move unvetted test to extra file --- .ci.govet.sh | 12 +++++- require/requirements_exit_test.go | 54 ------------------------ require/requirements_testing_test.go | 63 ++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 55 deletions(-) create mode 100644 require/requirements_testing_test.go diff --git a/.ci.govet.sh b/.ci.govet.sh index 9bdf4519b..ac39d37a6 100755 --- a/.ci.govet.sh +++ b/.ci.govet.sh @@ -2,4 +2,14 @@ set -e -go vet ./... +# Run go vet on all packages. To exclude specific tests that are known to +# trigger vet warnings, use the 'novet' build tag. This is used in the +# following tests: +# +# require/requirements_testing_test.go: +# +# The 'testing' tests test testify behavior 😜 against a real testing.T, +# running tests in goroutines to capture Goexit behavior. +# Such usage triggers would normally trigger vet warnings (SA2002). + +go vet -tags novet ./... diff --git a/require/requirements_exit_test.go b/require/requirements_exit_test.go index 979b95e62..241b81b3c 100644 --- a/require/requirements_exit_test.go +++ b/require/requirements_exit_test.go @@ -86,60 +86,6 @@ func TestEventuallyWithTFail(t *testing.T) { Contains(t, err2.Error(), "Condition exited unexpectedly", "Second error must mention unexpected exit") } -func TestTestingTFailNow(t *testing.T) { - t.Parallel() - - tt := new(testing.T) - done := make(chan struct{}) - // Run in a separate goroutine to capture the Goexit behavior. - // This avoid test panics from the unssupported Goexit call in the main test goroutine. - // Note that this will trigger linter warnings about goroutines in tests. (SA2002) - go func(tt *testing.T) { - defer close(done) - defer func() { - r := recover() // [runtime.Goexit] does not trigger a panic - // If we see a panic here, the condition function misbehaved - Nil(t, r, "Condition function must not panic: %v", r) - }() - tt.Errorf("test error") - tt.FailNow() - panic("unreachable") - }(tt) - <-done - True(t, tt.Failed(), "testing.T must be marked as failed") -} - -func TestEventuallyTestingTFailNow(t *testing.T) { - tt := new(testing.T) - - count := 0 - done := make(chan struct{}) - - // Run Eventually in a separate goroutine to capture the Goexit behavior. - // This avoid test panics from the unssupported Goexit call in the main test goroutine. - // Note that this will trigger linter warnings about goroutines in tests. (SA2002) - go func(tt *testing.T) { - defer close(done) - defer func() { - r := recover() // [runtime.Goexit] does not trigger a panic - // If we see a panic here, the condition function misbehaved - Nil(t, r, "Condition function must not panic: %v", r) - }() - condition := func() bool { - // tick assertion failure - count++ - tt.Error("tick error") - tt.FailNow() - panic("unreachable") - } - Eventually(tt, condition, 100*time.Millisecond, 20*time.Millisecond) - }(tt) - <-done - - True(t, tt.Failed(), "Check must fail") - Equal(t, 1, count, "Condition function must have been called once") -} - func TestEventuallyFailNow(t *testing.T) { t.Parallel() diff --git a/require/requirements_testing_test.go b/require/requirements_testing_test.go new file mode 100644 index 000000000..9877492c4 --- /dev/null +++ b/require/requirements_testing_test.go @@ -0,0 +1,63 @@ +//go:build !novet +// +build !novet + +package require + +import ( + "testing" + "time" +) + +func TestTestingTFailNow(t *testing.T) { + t.Parallel() + + tt := new(testing.T) + done := make(chan struct{}) + // Run in a separate goroutine to capture the Goexit behavior. + // This avoid test panics from the unssupported Goexit call in the main test goroutine. + // Note that this will trigger linter warnings about goroutines in tests. (SA2002) + go func(tt *testing.T) { + defer close(done) + defer func() { + r := recover() // [runtime.Goexit] does not trigger a panic + // If we see a panic here, the condition function misbehaved + Nil(t, r, "Condition function must not panic: %v", r) + }() + tt.Errorf("test error") + tt.FailNow() + panic("unreachable") + }(tt) + <-done + True(t, tt.Failed(), "testing.T must be marked as failed") +} + +func TestEventuallyTestingTFailNow(t *testing.T) { + tt := new(testing.T) + + count := 0 + done := make(chan struct{}) + + // Run Eventually in a separate goroutine to capture the Goexit behavior. + // This avoid test panics from the unssupported Goexit call in the main test goroutine. + // Note that this will trigger linter warnings about goroutines in tests. (SA2002) + go func(tt *testing.T) { + defer close(done) + defer func() { + r := recover() // [runtime.Goexit] does not trigger a panic + // If we see a panic here, the condition function misbehaved + Nil(t, r, "Condition function must not panic: %v", r) + }() + condition := func() bool { + // tick assertion failure + count++ + tt.Error("tick error") + tt.FailNow() + panic("unreachable") + } + Eventually(tt, condition, 100*time.Millisecond, 20*time.Millisecond) + }(tt) + <-done + + True(t, tt.Failed(), "Check must fail") + Equal(t, 1, count, "Condition function must have been called once") +}