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/.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 diff --git a/EVENTUALLY.md b/EVENTUALLY.md new file mode 100644 index 000000000..6dfb7ea5c --- /dev/null +++ b/EVENTUALLY.md @@ -0,0 +1,101 @@ +# Eventually + +`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 + +```go +func Eventually( + t TestingT, + condition func() bool, + waitFor time.Duration, + tick time.Duration, + msgAndArgs ...interface{} +) bool +``` + +- `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` 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 gives each tick a new +`*assert.CollectT` named `collect`: + +- 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()`) 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 + stop the test process. + +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` 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. +- If the condition calls `runtime.Goexit`, the assertion fails immediately with + "Condition exited unexpectedly". +- These behaviors match those of `assert.Eventually`. + +> [!Note] +> `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 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. diff --git a/README.md b/README.md index 44d40e6c4..dd2914803 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, +> 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/_codegen/main.go b/_codegen/main.go index 574985181..fed0c9fc0 100644 --- a/_codegen/main.go +++ b/_codegen/main.go @@ -285,11 +285,64 @@ 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 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 + } + + m := paramsExp.FindStringSubmatch(s) + prefix, params, msg := m[1], strings.Split(m[2], ", "), "error message" + + 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 } func (f *testFunc) CommentWithoutT(receiver string) string { diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 2d089991a..c7aa48d16 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() bool { +// // 🀝 Use thread-safe access when communicating with other goroutines! +// gotValue := externalValue.Load() // -// assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// // 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 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() @@ -176,24 +224,69 @@ 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 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 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 +// fails the test with "Condition never satisfied" and returns false. // -// externalValue := 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'. +// - 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. +// +// ⚠️ 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) { +// // 🀝 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 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() @@ -274,7 +367,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 { @@ -287,7 +381,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 { @@ -525,7 +620,26 @@ 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. // -// assert.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// 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. +// +// // 🀝 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() @@ -689,8 +803,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() @@ -737,7 +852,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() @@ -781,8 +896,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 d8300af73..4e9e6c8c1 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 // -// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +// return gotValue +// +// }, 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() @@ -333,24 +381,69 @@ 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 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 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 +// fails the test with "Condition never satisfied" 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'. +// - 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. // -// 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) // }() -// 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 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() @@ -358,24 +451,69 @@ 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 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 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 +// fails the test with "Condition never satisfied" 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'. +// - 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. // -// 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) // }() -// 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) { +// // 🀝 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 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() @@ -383,10 +521,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() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// a.Eventuallyf(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 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() @@ -537,7 +723,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 { @@ -550,7 +737,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 { @@ -563,7 +751,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 { @@ -576,7 +765,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 { @@ -1039,7 +1229,26 @@ 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. // -// a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +// 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. +// +// // 🀝 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() @@ -1050,7 +1259,26 @@ 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. // -// a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// 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. +// +// // 🀝 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() @@ -1368,8 +1596,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() @@ -1379,8 +1608,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() @@ -1464,7 +1694,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() @@ -1520,7 +1750,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() @@ -1552,8 +1782,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() @@ -1563,8 +1794,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 a27e70546..2948af19e 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() @@ -2001,17 +2003,84 @@ 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 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() } - 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() @@ -2019,6 +2088,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 +2099,25 @@ 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 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 conditionNotSatisfied: + fallthrough + default: + tickC = ticker.C // Enable ticks to check again. } - tickC = ticker.C } } } @@ -2046,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. @@ -2059,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") @@ -2073,47 +2181,107 @@ 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 } -// 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. +// 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 +// 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 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. // -// externalValue := false +// 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, 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'. +// - 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. +// +// ⚠️ 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 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() } + // 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 goroutineExitedWithoutCallingFailNow bool var lastFinishedTickErrs []error ch := make(chan *CollectT, 1) checkCond := func() { + goroutineExited := true // Assume the goroutine will exit. collect := new(CollectT) defer func() { + goroutineExitedWithoutCallingFailNow = goroutineExited && !collect.calledFailNow() ch <- collect }() condition(collect) + goroutineExited = false // The goroutine did not exit via [runtime.Goexit] } timer := time.NewTimer(waitFor) @@ -2138,12 +2306,26 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time tickC = nil go checkCond() case collect := <-ch: - if !collect.failed() { + switch { + 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. 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 } } } @@ -2151,14 +2333,52 @@ 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. // -// assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +// 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. +// +// // 🀝 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() } - 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() @@ -2178,11 +2398,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..2e71ecb65 --- /dev/null +++ b/assert/assertions_exit_test.go @@ -0,0 +1,262 @@ +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 + } + + 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, // no errors expected + }, + { + name: "Fail", run: evtl, + fn: doFail, exit: nil, ret: true, // fail and succeed fast + 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}, // expect both errors + }, + { + name: "Goexit", run: evtl, + fn: nil, exit: runtime.Goexit, ret: false, // no succeed fast, just exit + expErrors: []string{exitErr}, // expect exit error + }, + + // Fast path EventuallyWithT tests + { + name: "SatisfyWithT", run: withT, + fn: nil, exit: nil, ret: true, // succeed fast + expErrors: nil, // no errors expected + }, + { + name: "GoExitWithT", run: withT, + fn: nil, exit: runtime.Goexit, ret: false, // no succeed fast, just exit + 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. + + // Fast path Never tests + { + name: "SatisfyNever", run: never, + fn: nil, exit: nil, ret: true, // fail fast by satisfying + 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}, // expect both errors + }, + { + name: "GoexitNever", run: never, + fn: nil, exit: runtime.Goexit, ret: false, // no satisfy, just exit + expErrors: []string{exitErr}, // expect exit error + }, + { + name: "FailNever", run: never, + fn: doFail, exit: nil, ret: true, // fail then satisfy fast + expErrors: []string{failedErr, satisErr}, // 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") + } + + expFail := len(tc.expErrors) > 0 + + Nil(t, panicValue, "Eventually should not panic") + 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: + 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.Second, 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() + 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") + + 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") + Contains(t, collT.errors[0].Error(), "Condition exited unexpectedly") + + collT = &CollectT{} + Never(collT, func() bool { + runtime.Goexit() + 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 TestPanicInEventuallyNotRecovered(t *testing.T) { + testPanicUnrecoverable(t, func() { + Eventually(t, func() bool { + panic("demo panic") + }, time.Minute, time.Millisecond) + }) +} + +func TestPanicInEventuallyWithTNotRecovered(t *testing.T) { + testPanicUnrecoverable(t, func() { + EventuallyWithT(t, func(collect *CollectT) { + panic("demo panic") + }, time.Minute, time.Millisecond) + }) +} + +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. +// 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 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, failingDemoTest func()) { + if os.Getenv("TestPanic") == "" { + t.Skip("Skipping test, set TestPanic=1 to run") + } + // 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.") + }() + failingDemoTest() +} 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/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) { 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 23a3be780..2336d3ba6 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 // -// require.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +// return gotValue +// +// }, 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() @@ -415,24 +463,69 @@ 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 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 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 +// fails the test with "Condition never satisfied" 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'. +// - 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. // -// 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) // }() -// 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 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() @@ -443,24 +536,69 @@ 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 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 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 +// fails the test with "Condition never satisfied" 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'. +// - 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. // -// 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) // }() -// 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) { +// // 🀝 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 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() @@ -471,10 +609,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() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// require.Eventuallyf(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 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() @@ -670,7 +856,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{}) { @@ -686,7 +873,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{}) { @@ -702,7 +890,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{}) { @@ -718,7 +907,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{}) { @@ -1310,7 +1500,26 @@ 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. // -// require.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +// 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. +// +// // 🀝 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() @@ -1324,7 +1533,26 @@ 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. // -// require.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// 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. +// +// // 🀝 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() @@ -1729,8 +1957,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() @@ -1743,8 +1972,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() @@ -1849,7 +2079,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() @@ -1920,7 +2150,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() @@ -1961,8 +2191,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() @@ -1975,8 +2206,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 38d985a55..fbd32100d 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 // -// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +// return gotValue +// +// }, 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() @@ -334,24 +382,69 @@ 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 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 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 +// fails the test with "Condition never satisfied" 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'. +// - 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. // -// 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) // }() -// 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 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() @@ -359,24 +452,69 @@ 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 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 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 +// fails the test with "Condition never satisfied" 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'. +// - 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. // -// 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) // }() -// 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) { +// // 🀝 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 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() @@ -384,10 +522,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() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// a.Eventuallyf(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 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() @@ -538,7 +724,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{}) { @@ -551,7 +738,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{}) { @@ -564,7 +752,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{}) { @@ -577,7 +766,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{}) { @@ -1040,7 +1230,26 @@ 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. // -// a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +// 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. +// +// // 🀝 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() @@ -1051,7 +1260,26 @@ 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. // -// a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +// 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. +// +// // 🀝 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() @@ -1369,8 +1597,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() @@ -1380,8 +1609,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() @@ -1465,7 +1695,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() @@ -1521,7 +1751,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() @@ -1553,8 +1783,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() @@ -1564,8 +1795,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..241b81b3c --- /dev/null +++ b/require/requirements_exit_test.go @@ -0,0 +1,110 @@ +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 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") } 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") +}