diff --git a/assert/assert_assertions.go b/assert/assert_assertions.go index ff5120051..b7de4ac76 100644 --- a/assert/assert_assertions.go +++ b/assert/assert_assertions.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert @@ -14,7 +14,7 @@ import ( "github.com/go-openapi/testify/v2/internal/assertions" ) -// Condition uses a Comparison to assert a complex condition. +// Condition uses a [Comparison] to assert a complex condition. // // # Usage // @@ -299,12 +299,28 @@ func ErrorIs(t T, err error, target error, msgAndArgs ...any) bool { return assertions.ErrorIs(t, err, target, msgAndArgs...) } -// 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 the target function on each tick. +// +// [Eventually] waits until the condition returns true, for at most waitFor, +// or until the parent context of the test is cancelled. +// +// If the condition takes longer than waitFor to complete, [Eventually] fails +// but waits for the current condition execution to finish before returning. +// +// For long-running conditions to be interrupted early, check [testing.T.Context] +// which is cancelled on test failure. // // # Usage // -// assertions.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +// assertions.Eventually(t, func() bool { return true }, time.Second, 10*time.Millisecond) +// +// # Concurrency +// +// The condition function is never executed in parallel: only one goroutine executes it. +// It may write to variables outside its scope without triggering race conditions. +// +// A blocking condition will cause [Eventually] to hang until it returns. // // # Examples // @@ -319,14 +335,19 @@ func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Dur return assertions.Eventually(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. +// EventuallyWithT asserts that the given condition will be met in waitFor time, +// periodically checking the target function at each tick. +// +// In contrast to [Eventually], the condition function is supplied with a [CollectT] +// to accumulate errors from calling 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. +// The supplied [CollectT] collects all errors from one tick. +// +// If the condition is not met before waitFor, the collected errors from the +// last tick are copied to t. +// +// Calling [CollectT.FailNow] cancels the condition immediately and fails the assertion. // // # Usage // @@ -335,11 +356,17 @@ func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Dur // time.Sleep(8*time.Second) // externalValue = true // }() +// // assertions.EventuallyWithT(t, func(c *assertions.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assertions.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") // +// # Concurrency +// +// The condition function is never executed in parallel: only one goroutine executes it. +// It may write to variables outside its scope without triggering race conditions. +// // # Examples // // success: func(c *CollectT) { True(c,true) }, 100*time.Millisecond, 20*time.Millisecond @@ -1021,12 +1048,24 @@ func Negative(t T, e any, msgAndArgs ...any) bool { return assertions.Negative(t, e, msgAndArgs...) } -// Never asserts that the given condition doesn't satisfy in waitFor time, -// periodically checking the target function each tick. +// Never asserts that the given condition is never satisfied within waitFor time, +// periodically checking the target function at each tick. +// +// [Never] is the opposite of [Eventually]. It succeeds if the waitFor timeout +// is reached without the condition ever returning true. +// +// If the parent context is cancelled before the timeout, [Never] fails. // // # Usage // -// assertions.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +// assertions.Never(t, func() bool { return false }, time.Second, 10*time.Millisecond) +// +// # Concurrency +// +// The condition function is never executed in parallel: only one goroutine executes it. +// It may write to variables outside its scope without triggering race conditions. +// +// A blocking condition will cause [Never] to hang until it returns. // // # Examples // diff --git a/assert/assert_assertions_test.go b/assert/assert_assertions_test.go index 68cc33de2..a67d757e6 100644 --- a/assert/assert_assertions_test.go +++ b/assert/assert_assertions_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert diff --git a/assert/assert_examples_test.go b/assert/assert_examples_test.go index 82cb71f11..b2ee0c8e8 100644 --- a/assert/assert_examples_test.go +++ b/assert/assert_examples_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert_test diff --git a/assert/assert_format.go b/assert/assert_format.go index b6b901ed8..a64852e87 100644 --- a/assert/assert_format.go +++ b/assert/assert_format.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert diff --git a/assert/assert_format_test.go b/assert/assert_format_test.go index e5e318a87..17e3d4f7f 100644 --- a/assert/assert_format_test.go +++ b/assert/assert_format_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert diff --git a/assert/assert_forward.go b/assert/assert_forward.go index 3861c5cbb..1a6f39228 100644 --- a/assert/assert_forward.go +++ b/assert/assert_forward.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert diff --git a/assert/assert_forward_test.go b/assert/assert_forward_test.go index 839495b1e..bdf9950ee 100644 --- a/assert/assert_forward_test.go +++ b/assert/assert_forward_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert diff --git a/assert/assert_helpers.go b/assert/assert_helpers.go index ea9205e9e..c1792ebd9 100644 --- a/assert/assert_helpers.go +++ b/assert/assert_helpers.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert diff --git a/assert/assert_helpers_test.go b/assert/assert_helpers_test.go index c86490fe1..3c7385fb7 100644 --- a/assert/assert_helpers_test.go +++ b/assert/assert_helpers_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert diff --git a/assert/assert_types.go b/assert/assert_types.go index 0bb0068e9..625423771 100644 --- a/assert/assert_types.go +++ b/assert/assert_types.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package assert @@ -25,6 +25,9 @@ type ( BoolAssertionFunc = assertions.BoolAssertionFunc // CollectT implements the [T] interface and collects all errors. + // + // [CollectT] is specifically intended to be used with [EventuallyWithT] and + // should not be used outside of that context. CollectT = assertions.CollectT // Comparison is a custom function that returns true on success and false on failure. diff --git a/docs/doc-site/api/_index.md b/docs/doc-site/api/_index.md index 367e8cd2b..d806fda12 100644 --- a/docs/doc-site/api/_index.md +++ b/docs/doc-site/api/_index.md @@ -6,7 +6,7 @@ description: | Find the assertion function you need for your data. weight: 1 -modified: 2026-01-02 +modified: 2026-01-11 --- **Go testing assertions for the rest of us** @@ -52,7 +52,7 @@ The `testify` API is organized in 18 domains shown below. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -64,7 +64,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/boolean.md b/docs/doc-site/api/boolean.md index 2d0abeec5..ac630d6d7 100644 --- a/docs/doc-site/api/boolean.md +++ b/docs/doc-site/api/boolean.md @@ -1,7 +1,7 @@ --- title: "Boolean" description: "Asserting Boolean Values" -modified: 2026-01-02 +modified: 2026-01-11 weight: 1 domains: - "boolean" @@ -121,7 +121,7 @@ True asserts that the specified value is true. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -131,7 +131,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/collection.md b/docs/doc-site/api/collection.md index 356c2b7ef..6eef2b795 100644 --- a/docs/doc-site/api/collection.md +++ b/docs/doc-site/api/collection.md @@ -1,7 +1,7 @@ --- title: "Collection" description: "Asserting Slices And Maps" -modified: 2026-01-02 +modified: 2026-01-11 weight: 2 domains: - "collection" @@ -407,7 +407,7 @@ only the map key is evaluated. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -417,7 +417,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/common.md b/docs/doc-site/api/common.md index 8156537da..3bd243d4c 100644 --- a/docs/doc-site/api/common.md +++ b/docs/doc-site/api/common.md @@ -1,7 +1,7 @@ --- title: "Common" description: "Other Uncategorized Helpers" -modified: 2026-01-02 +modified: 2026-01-11 weight: 18 domains: - "common" @@ -145,7 +145,7 @@ values are equal. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -155,7 +155,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/comparison.md b/docs/doc-site/api/comparison.md index 2217f9646..bf88a6209 100644 --- a/docs/doc-site/api/comparison.md +++ b/docs/doc-site/api/comparison.md @@ -1,7 +1,7 @@ --- title: "Comparison" description: "Comparing Ordered Values" -modified: 2026-01-02 +modified: 2026-01-11 weight: 3 domains: - "comparison" @@ -329,7 +329,7 @@ Positive asserts that the specified element is strictly positive. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -339,7 +339,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/condition.md b/docs/doc-site/api/condition.md index 7c8ef82b4..4457c3714 100644 --- a/docs/doc-site/api/condition.md +++ b/docs/doc-site/api/condition.md @@ -1,7 +1,7 @@ --- title: "Condition" description: "Expressing Assertions Using Conditions" -modified: 2026-01-02 +modified: 2026-01-11 weight: 4 domains: - "condition" @@ -29,7 +29,7 @@ This domain exposes 4 functionalities. ### Condition -Condition uses a Comparison to assert a complex condition. +Condition uses a [Comparison] to assert a complex condition. {{% expand title="Examples" %}} {{< tabs >}} @@ -70,20 +70,36 @@ Condition uses a Comparison to assert a complex condition. |--|--| | [`assertions.Condition(t T, comp Comparison, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Condition) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Condition](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L22) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Condition](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L26) {{% /tab %}} {{< /tabs >}} ### Eventually -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 the target function on each tick. + +[Eventually] waits until the condition returns true, for at most waitFor, +or until the parent context of the test is cancelled. + +If the condition takes longer than waitFor to complete, [Eventually] fails +but waits for the current condition execution to finish before returning. + +For long-running conditions to be interrupted early, check [testing.T.Context](https://pkg.go.dev/testing#T.Context) +which is cancelled on test failure. {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Usage" %}} ```go - assertions.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) + assertions.Eventually(t, func() bool { return true }, time.Second, 10*time.Millisecond) +``` +{{< /tab >}} +{{% tab title="Concurrency" %}} +```go +The condition function is never executed in parallel: only one goroutine executes it. +It may write to variables outside its scope without triggering race conditions. +A blocking condition will cause [Eventually] to hang until it returns. ``` {{< /tab >}} {{% tab title="Examples" %}} @@ -118,20 +134,25 @@ periodically checking target function each tick. |--|--| | [`assertions.Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Eventually) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Eventually](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L45) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Eventually](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L67) {{% /tab %}} {{< /tabs >}} ### EventuallyWithT -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. +EventuallyWithT asserts that the given condition will be met in waitFor time, +periodically checking the target function at each tick. + +In contrast to [Eventually], the condition function is supplied with a [CollectT] +to accumulate errors from calling 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. +The supplied [CollectT] collects all errors from one tick. + +If the condition is not met before waitFor, the collected errors from the +last tick are copied to t. + +Calling [CollectT.FailNow](https://pkg.go.dev/CollectT#FailNow) cancels the condition immediately and fails the assertion. {{% expand title="Examples" %}} {{< tabs >}} @@ -148,6 +169,12 @@ the last tick are copied to t. }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") ``` {{< /tab >}} +{{% tab title="Concurrency" %}} +```go +The condition function is never executed in parallel: only one goroutine executes it. +It may write to variables outside its scope without triggering race conditions. +``` +{{< /tab >}} {{% tab title="Examples" %}} ```go success: func(c *CollectT) { True(c,true) }, 100*time.Millisecond, 20*time.Millisecond @@ -186,14 +213,26 @@ the last tick are copied to t. ### Never -Never asserts that the given condition doesn't satisfy in waitFor time, -periodically checking the target function each tick. +Never asserts that the given condition is never satisfied within waitFor time, +periodically checking the target function at each tick. + +[Never] is the opposite of [Eventually]. It succeeds if the waitFor timeout +is reached without the condition ever returning true. + +If the parent context is cancelled before the timeout, [Never] fails. {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Usage" %}} ```go - assertions.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) + assertions.Never(t, func() bool { return false }, time.Second, 10*time.Millisecond) +``` +{{< /tab >}} +{{% tab title="Concurrency" %}} +```go +The condition function is never executed in parallel: only one goroutine executes it. +It may write to variables outside its scope without triggering race conditions. +A blocking condition will cause [Never] to hang until it returns. ``` {{< /tab >}} {{% tab title="Examples" %}} @@ -228,7 +267,7 @@ periodically checking the target function each tick. |--|--| | [`assertions.Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Never) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Never](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L204) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Never](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L99) {{% /tab %}} {{< /tabs >}} @@ -236,7 +275,7 @@ periodically checking the target function each tick. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -246,7 +285,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/equality.md b/docs/doc-site/api/equality.md index 0f76c0fea..e27ebc10d 100644 --- a/docs/doc-site/api/equality.md +++ b/docs/doc-site/api/equality.md @@ -1,7 +1,7 @@ --- title: "Equality" description: "Asserting Two Things Are Equal" -modified: 2026-01-02 +modified: 2026-01-11 weight: 5 domains: - "equality" @@ -644,7 +644,7 @@ determined based on the equality of both type and value. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -654,7 +654,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/error.md b/docs/doc-site/api/error.md index a9b8f8342..51f9a1fe9 100644 --- a/docs/doc-site/api/error.md +++ b/docs/doc-site/api/error.md @@ -1,7 +1,7 @@ --- title: "Error" description: "Asserting Errors" -modified: 2026-01-02 +modified: 2026-01-11 weight: 6 domains: - "error" @@ -430,7 +430,7 @@ This is a wrapper for [errors.Is](https://pkg.go.dev/errors#Is). --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -440,7 +440,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/file.md b/docs/doc-site/api/file.md index 4e311c9e9..f024e3789 100644 --- a/docs/doc-site/api/file.md +++ b/docs/doc-site/api/file.md @@ -1,7 +1,7 @@ --- title: "File" description: "Asserting OS Files" -modified: 2026-01-02 +modified: 2026-01-11 weight: 7 domains: - "file" @@ -323,7 +323,7 @@ if the path points to an existing _file_ only. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -333,7 +333,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/http.md b/docs/doc-site/api/http.md index 2a7605cbe..b0a9ff4ca 100644 --- a/docs/doc-site/api/http.md +++ b/docs/doc-site/api/http.md @@ -1,7 +1,7 @@ --- title: "Http" description: "Asserting HTTP Response And Body" -modified: 2026-01-02 +modified: 2026-01-11 weight: 8 domains: - "http" @@ -361,7 +361,7 @@ It returns the empty string if building a new request fails. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -371,7 +371,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/json.md b/docs/doc-site/api/json.md index b1d4ba3ee..edb9af0bc 100644 --- a/docs/doc-site/api/json.md +++ b/docs/doc-site/api/json.md @@ -1,7 +1,7 @@ --- title: "Json" description: "Asserting JSON Documents" -modified: 2026-01-02 +modified: 2026-01-11 weight: 9 domains: - "json" @@ -121,7 +121,7 @@ JSONEqBytes asserts that two JSON byte slices are equivalent. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -131,7 +131,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/number.md b/docs/doc-site/api/number.md index b0b4f71be..89240172c 100644 --- a/docs/doc-site/api/number.md +++ b/docs/doc-site/api/number.md @@ -1,7 +1,7 @@ --- title: "Number" description: "Asserting Numbers" -modified: 2026-01-02 +modified: 2026-01-11 weight: 10 domains: - "number" @@ -268,7 +268,7 @@ InEpsilonSlice is the same as InEpsilon, except it compares each value from two --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -278,7 +278,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/ordering.md b/docs/doc-site/api/ordering.md index b33fba1b9..51e1800b8 100644 --- a/docs/doc-site/api/ordering.md +++ b/docs/doc-site/api/ordering.md @@ -1,7 +1,7 @@ --- title: "Ordering" description: "Asserting How Collections Are Ordered" -modified: 2026-01-02 +modified: 2026-01-11 weight: 11 domains: - "ordering" @@ -227,7 +227,7 @@ IsNonIncreasing asserts that the collection is not increasing. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -237,7 +237,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/panic.md b/docs/doc-site/api/panic.md index ab51a4752..979d89d41 100644 --- a/docs/doc-site/api/panic.md +++ b/docs/doc-site/api/panic.md @@ -1,7 +1,7 @@ --- title: "Panic" description: "Asserting A Panic Behavior" -modified: 2026-01-02 +modified: 2026-01-11 weight: 12 domains: - "panic" @@ -222,7 +222,7 @@ the recovered panic value equals the expected panic value. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -232,7 +232,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/string.md b/docs/doc-site/api/string.md index e7c41a4d8..20d95f8d9 100644 --- a/docs/doc-site/api/string.md +++ b/docs/doc-site/api/string.md @@ -1,7 +1,7 @@ --- title: "String" description: "Asserting Strings" -modified: 2026-01-02 +modified: 2026-01-11 weight: 13 domains: - "string" @@ -123,7 +123,7 @@ Regexp asserts that a specified regexp matches a string. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -133,7 +133,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/testing.md b/docs/doc-site/api/testing.md index 6e2b491de..c22d3df75 100644 --- a/docs/doc-site/api/testing.md +++ b/docs/doc-site/api/testing.md @@ -1,7 +1,7 @@ --- title: "Testing" description: "Mimicks Methods From The Testing Standard Library" -modified: 2026-01-02 +modified: 2026-01-11 weight: 14 domains: - "testing" @@ -119,7 +119,7 @@ FailNow fails test. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -129,7 +129,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/time.md b/docs/doc-site/api/time.md index 7aa6c20c8..4b398bf03 100644 --- a/docs/doc-site/api/time.md +++ b/docs/doc-site/api/time.md @@ -1,7 +1,7 @@ --- title: "Time" description: "Asserting Times And Durations" -modified: 2026-01-02 +modified: 2026-01-11 weight: 15 domains: - "time" @@ -121,7 +121,7 @@ WithinRange asserts that a time is within a time range (inclusive). --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -131,7 +131,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/type.md b/docs/doc-site/api/type.md index 82705771f..2f05b97ae 100644 --- a/docs/doc-site/api/type.md +++ b/docs/doc-site/api/type.md @@ -1,7 +1,7 @@ --- title: "Type" description: "Asserting Types Rather Than Values" -modified: 2026-01-02 +modified: 2026-01-11 weight: 16 domains: - "type" @@ -317,7 +317,7 @@ Zero asserts that i is the zero value for its type. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -327,7 +327,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/docs/doc-site/api/yaml.md b/docs/doc-site/api/yaml.md index 3003c95cf..dc64b84c0 100644 --- a/docs/doc-site/api/yaml.md +++ b/docs/doc-site/api/yaml.md @@ -1,7 +1,7 @@ --- title: "Yaml" description: "Asserting Yaml Documents" -modified: 2026-01-02 +modified: 2026-01-11 weight: 17 domains: - "yaml" @@ -82,7 +82,7 @@ YAMLEq asserts that the first documents in the two YAML strings are equivalent. --- -Generated with github.com/go-openapi/testify/v2/codegen +Generated with github.com/go-openapi/testify/codegen/v2 [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 [godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 @@ -92,7 +92,7 @@ SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers SPDX-License-Identifier: Apache-2.0 -Document generated by github.com/go-openapi/testify/v2/codegen DO NOT EDIT. +Document generated by github.com/go-openapi/testify/codegen/v2 DO NOT EDIT. -Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] --> diff --git a/internal/assertions/compare_test.go b/internal/assertions/compare_test.go index f26bac831..aadc61b37 100644 --- a/internal/assertions/compare_test.go +++ b/internal/assertions/compare_test.go @@ -5,7 +5,6 @@ package assertions import ( "bytes" - "fmt" "iter" "runtime" "slices" @@ -194,24 +193,6 @@ func TestCompareMsgAndArgsForwarding(t *testing.T) { } } -type outputT struct { - buf *bytes.Buffer - helpers map[string]struct{} -} - -// Implements T. -func (t *outputT) Errorf(format string, args ...any) { - s := fmt.Sprintf(format, args...) - t.buf.WriteString(s) -} - -func (t *outputT) Helper() { - if t.helpers == nil { - t.helpers = make(map[string]struct{}) - } - t.helpers[callerName(1)] = struct{}{} -} - // callerName gives the function name (qualified with a package path) // for the caller after skip frames (where 0 means the current function). func callerName(skip int) string { diff --git a/internal/assertions/condition.go b/internal/assertions/condition.go index fef478a76..b6d040254 100644 --- a/internal/assertions/condition.go +++ b/internal/assertions/condition.go @@ -4,12 +4,16 @@ package assertions import ( + "context" + "errors" "fmt" "runtime" + "sync" + "sync/atomic" "time" ) -// Condition uses a Comparison to assert a complex condition. +// Condition uses a [Comparison] to assert a complex condition. // // # Usage // @@ -24,19 +28,37 @@ func Condition(t T, comp Comparison, msgAndArgs ...any) bool { if h, ok := t.(H); ok { h.Helper() } + result := comp() if !result { - Fail(t, "Condition failed!", msgAndArgs...) + Fail(t, "condition failed", msgAndArgs...) } + return result } -// 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 the target function on each tick. +// +// [Eventually] waits until the condition returns true, for at most waitFor, +// or until the parent context of the test is cancelled. +// +// If the condition takes longer than waitFor to complete, [Eventually] fails +// but waits for the current condition execution to finish before returning. +// +// For long-running conditions to be interrupted early, check [testing.T.Context] +// which is cancelled on test failure. // // # Usage // -// assertions.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +// assertions.Eventually(t, func() bool { return true }, time.Second, 10*time.Millisecond) +// +// # Concurrency +// +// The condition function is never executed in parallel: only one goroutine executes it. +// It may write to variables outside its scope without triggering race conditions. +// +// A blocking condition will cause [Eventually] to hang until it returns. // // # Examples // @@ -48,82 +70,54 @@ func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Dur h.Helper() } - ch := make(chan bool, 1) - checkCond := func() { ch <- condition() } - - timer := time.NewTimer(waitFor) - defer timer.Stop() - - ticker := time.NewTicker(tick) - defer ticker.Stop() - - var tickC <-chan time.Time - - // Check the condition once first on the initial call. - go checkCond() - - for { - select { - case <-timer.C: - return Fail(t, "Condition never satisfied", msgAndArgs...) - case <-tickC: - tickC = nil - go checkCond() - case v := <-ch: - if v { - return true - } - tickC = ticker.C - } - } + return eventually(t, condition, waitFor, tick, msgAndArgs...) } -// CollectT implements the [T] interface and collects all errors. -type CollectT struct { +// Never asserts that the given condition is never satisfied within waitFor time, +// periodically checking the target function at each tick. +// +// [Never] is the opposite of [Eventually]. It succeeds if the waitFor timeout +// is reached without the condition ever returning true. +// +// If the parent context is cancelled before the timeout, [Never] fails. +// +// # Usage +// +// assertions.Never(t, func() bool { return false }, time.Second, 10*time.Millisecond) +// +// # Concurrency +// +// The condition function is never executed in parallel: only one goroutine executes it. +// It may write to variables outside its scope without triggering race conditions. +// +// A blocking condition will cause [Never] to hang until it returns. +// +// # Examples +// +// success: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond +// failure: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond +func Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { // Domain: condition - // - // Maintainer: - // 1. we should verify if the use of runtime.GoExit is correct in this context. - // 2. deprecated methods removed. - - // A slice of errors. Non-nil slice denotes a failure. - // If it's non-nil but len(c.errors) == 0, this is also a failure - // obtained by direct c.FailNow() call. - errors []error -} - -// Helper is like [testing.T.Helper] but does nothing. -func (CollectT) Helper() {} - -// Errorf collects the error. -func (c *CollectT) Errorf(format string, args ...any) { - c.errors = append(c.errors, fmt.Errorf(format, args...)) -} - -// FailNow stops execution by calling runtime.Goexit. -func (c *CollectT) FailNow() { - c.fail() - runtime.Goexit() -} - -func (c *CollectT) fail() { - if !c.failed() { - c.errors = []error{} // Make it non-nil to mark a failure. + if h, ok := t.(H); ok { + h.Helper() } -} -func (c *CollectT) failed() bool { - return c.errors != nil + return never(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. +// EventuallyWithT asserts that the given condition will be met in waitFor time, +// periodically checking the target function at each tick. +// +// In contrast to [Eventually], the condition function is supplied with a [CollectT] +// to accumulate errors from calling 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. +// The supplied [CollectT] collects all errors from one tick. +// +// If the condition is not met before waitFor, the collected errors from the +// last tick are copied to t. +// +// Calling [CollectT.FailNow] cancels the condition immediately and fails the assertion. // // # Usage // @@ -132,11 +126,17 @@ func (c *CollectT) failed() bool { // time.Sleep(8*time.Second) // externalValue = true // }() +// // assertions.EventuallyWithT(t, func(c *assertions.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assertions.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") // +// # Concurrency +// +// The condition function is never executed in parallel: only one goroutine executes it. +// It may write to variables outside its scope without triggering race conditions. +// // # Examples // // success: func(c *CollectT) { True(c,true) }, 100*time.Millisecond, 20*time.Millisecond @@ -147,92 +147,322 @@ func EventuallyWithT(t T, condition func(collect *CollectT), waitFor time.Durati h.Helper() } - var lastFinishedTickErrs []error - ch := make(chan *CollectT, 1) + return eventuallyWithT(t, condition, waitFor, tick, msgAndArgs...) +} - checkCond := func() { - collect := new(CollectT) - defer func() { - ch <- collect - }() - condition(collect) +func eventually(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { + if h, ok := t.(H); ok { + h.Helper() } - timer := time.NewTimer(waitFor) - defer timer.Stop() + return pollCondition(t, + condition, waitFor, tick, + pollOptions{ + mode: pollUntilTrue, + failMessage: "condition never satisfied", + }, + msgAndArgs..., + ) +} - ticker := time.NewTicker(tick) - defer ticker.Stop() +func never(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } - var tickC <-chan time.Time + return pollCondition(t, + condition, waitFor, tick, + pollOptions{ + mode: pollUntilTimeout, + failMessage: "condition satisfied", + }, + msgAndArgs..., + ) +} - // Check the condition once first on the initial call. - go checkCond() +func eventuallyWithT(t T, collectCondition func(collector *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } - for { - select { - case <-timer.C: - for _, err := range lastFinishedTickErrs { - t.Errorf("%v", err) - } - return Fail(t, "Condition never satisfied", msgAndArgs...) - case <-tickC: - tickC = nil - go checkCond() - case collect := <-ch: - if !collect.failed() { - return true - } - // 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 + var lastCollectedErrors []error + var cancelFunc func() // will be set by pollCondition via onSetup + + condition := func() bool { + collector := new(CollectT).withCancelFunc(cancelFunc) + collectCondition(collector) + if collector.failed() { + lastCollectedErrors = collector.collected() + return false + } + + return true + } + + copyCollected := func(tt T) { + for _, err := range lastCollectedErrors { + tt.Errorf("%v", err) } } + + return pollCondition(t, + condition, waitFor, tick, + pollOptions{ + mode: pollUntilTrue, + failMessage: "condition never satisfied", + onFailure: copyCollected, + onSetup: func(cancel func()) { cancelFunc = cancel }, + }, + msgAndArgs..., + ) } -// Never asserts that the given condition doesn't satisfy in waitFor time, -// periodically checking the target function each tick. -// -// # Usage -// -// assertions.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) -// -// # Examples +// pollMode determines how the condition polling should behave. +type pollMode int + +const ( + // pollUntilTrue succeeds when condition returns true (for Eventually). + pollUntilTrue pollMode = iota + // pollUntilTimeout succeeds when timeout is reached without condition being true (for Never). + pollUntilTimeout +) + +// pollOptions configures the condition polling behavior. +type pollOptions struct { + mode pollMode + failMessage string // error message added at the end of the stack + onFailure func(t T) // called on failure (e.g., to copy collected errors) + onSetup func(cancel func()) // called after context setup to expose cancel function +} + +// pollCondition is the common implementation for eventually, never, and eventuallyWithT. +// It polls a condition function at regular intervals until success or timeout. // -// success: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond -// failure: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond -func Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { - // Domain: condition +//nolint:gocognit,gocyclo,cyclop // A refactoring is planned for this complex function. +func pollCondition(t T, condition func() bool, waitFor, tick time.Duration, opts pollOptions, msgAndArgs ...any) bool { if h, ok := t.(H); ok { h.Helper() } - ch := make(chan bool, 1) - checkCond := func() { ch <- condition() } + var parentCtx context.Context + if withContext, ok := t.(contextualizer); ok { + parentCtx = withContext.Context() + } + if parentCtx == nil { + parentCtx = context.Background() + } + + // For pollUntilTimeout (Never), we detach from parent cancellation + // so that timeout reaching is a success, not a failure. + var ctx context.Context + var cancel context.CancelFunc + if opts.mode == pollUntilTimeout { + ctx, cancel = context.WithTimeout(context.WithoutCancel(parentCtx), waitFor) + } else { + ctx, cancel = context.WithTimeout(parentCtx, waitFor) + } + defer cancel() - timer := time.NewTimer(waitFor) - defer timer.Stop() + // Allow caller to capture the cancel function (for eventuallyWithT's CollectT) + if opts.onSetup != nil { + opts.onSetup(cancel) + } + + var reported atomic.Bool + failFunc := func(reason string) { + if reported.CompareAndSwap(false, true) { + if reason != "" { + t.Errorf("%s", reason) + } + Fail(t, opts.failMessage, msgAndArgs...) + } + } + + conditionChan := make(chan func() bool, 1) + doneChan := make(chan struct{}) ticker := time.NewTicker(tick) defer ticker.Stop() - var tickC <-chan time.Time - // Check the condition once first on the initial call. - go checkCond() - - for { - select { - case <-timer.C: - return true - case <-tickC: - tickC = nil - go checkCond() - case v := <-ch: - if v { - return Fail(t, "Condition satisfied", msgAndArgs...) + conditionChan <- condition + + var wg sync.WaitGroup + + // Goroutine 1: Poll for the condition at every tick + wg.Add(1) + go func() { + defer wg.Done() + + for { + if opts.mode == pollUntilTimeout { + // For Never: check parent context separately + select { + case <-parentCtx.Done(): + failFunc(parentCtx.Err().Error()) + return + case <-ctx.Done(): + return // timeout reached = success for Never + case <-doneChan: + return + case <-ticker.C: + select { + case <-parentCtx.Done(): + failFunc(parentCtx.Err().Error()) + return + case <-ctx.Done(): + return + case <-doneChan: + return + case conditionChan <- condition: + } + } + } else { + // For Eventually: parent cancellation flows through ctx + select { + case <-ctx.Done(): + failFunc(ctx.Err().Error()) + return + case <-doneChan: + return + case <-ticker.C: + select { + case <-ctx.Done(): + failFunc(ctx.Err().Error()) + return + case <-doneChan: + return + case conditionChan <- condition: + } + } + } + } + }() + + // Goroutine 2: Execute the condition and check results + wg.Add(1) + go func() { + defer wg.Done() + + for { + if opts.mode == pollUntilTimeout { + select { + case <-parentCtx.Done(): + failFunc(parentCtx.Err().Error()) + return + case <-ctx.Done(): + return // timeout = success + case fn := <-conditionChan: + if fn() { + close(doneChan) // condition true = failure for Never + return + } + } + } else { + select { + case <-ctx.Done(): + failFunc(ctx.Err().Error()) + return + case fn := <-conditionChan: + if fn() { + close(doneChan) // condition true = success + return + } + } } - tickC = ticker.C } + }() + + wg.Wait() + + // Determine success based on mode + select { + case <-doneChan: + if opts.mode == pollUntilTimeout { + // For Never: doneChan closed means condition became true + // But if timeout was reached first (ctx.Err != nil), it's still a success + if ctx.Err() != nil { + return true + } + // Condition became true before timeout = failure + failFunc("") + return false + } + // For Eventually: doneChan closed means condition became true + if ctx.Err() != nil { + // Timeout occurred before or during success + if opts.onFailure != nil { + opts.onFailure(t) + } + return false + } + return true + default: + // doneChan not closed + if opts.mode == pollUntilTimeout { + // For Never: timeout reached without condition being true = success + // We should return a success, unless the parent context has failed. + return parentCtx.Err() == nil + } + + // opts.mode = pollUntilTrue + // For Eventually: should not reach here (failFunc already called) + if opts.onFailure != nil { + opts.onFailure(t) + } + + return false } } + +// CollectT implements the [T] interface and collects all errors. +// +// [CollectT] is specifically intended to be used with [EventuallyWithT] and +// should not be used outside of that context. +type CollectT struct { + // Domain: condition + // + // Maintainer: + // 1. FailNow() no longer just exits the go routine, but cancels the context of the caller instead before exiting. + // 2. We no longer establish the distinction between c.error nil or empty. Non-empty is an error, full stop. + // 2. Deprecated methods have been removed. + + // A slice of errors. Non-empty slice denotes a failure. + // A c.FailNow() will thee lose accumulated errors + errors []error + + // cancelContext cancels the parent context on FailNow() + cancelContext func() +} + +// Helper is like [testing.T.Helper] but does nothing. +func (*CollectT) Helper() {} + +// Errorf collects the error. +func (c *CollectT) Errorf(format string, args ...any) { + c.errors = append(c.errors, fmt.Errorf(format, args...)) +} + +// FailNow records a failure and cancels the parent [EventuallyWithT] context, +// before exiting the current go routine with [runtime.Goexit]. +// +// This causes the assertion to fail immediately without waiting for a timeout. +func (c *CollectT) FailNow() { + c.cancelContext() + c.errors = append(c.errors, errors.New("failed now")) // so c.failed() is true (currently lost as not owned by another go routine) + runtime.Goexit() +} + +func (c *CollectT) failed() bool { + return len(c.errors) != 0 +} + +func (c *CollectT) collected() []error { + return c.errors +} + +func (c *CollectT) withCancelFunc(cancel func()) *CollectT { + c.cancelContext = cancel + + return c +} diff --git a/internal/assertions/condition_test.go b/internal/assertions/condition_test.go index 8c4982925..159b9bfd4 100644 --- a/internal/assertions/condition_test.go +++ b/internal/assertions/condition_test.go @@ -4,22 +4,39 @@ package assertions import ( - "fmt" + "context" + "sort" + "strings" + "sync" "testing" "time" ) +const ( + testTimeout = 100 * time.Millisecond + testTick = 20 * time.Millisecond +) + func TestCondition(t *testing.T) { t.Parallel() - mock := new(testing.T) - if !Condition(mock, func() bool { return true }, "Truth") { - t.Error("Condition should return true") - } + t.Run("condition should be true", func(t *testing.T) { + t.Parallel() + + mock := new(testing.T) + if !Condition(mock, func() bool { return true }, "Truth") { + t.Error("condition should return true") + } + }) + + t.Run("condition should be false", func(t *testing.T) { + t.Parallel() - if Condition(mock, func() bool { return false }, "Lie") { - t.Error("Condition should return false") - } + mock := new(testing.T) + if Condition(mock, func() bool { return false }, "Lie") { + t.Error("condition should return false") + } + }) } func TestConditionEventually(t *testing.T) { @@ -27,18 +44,17 @@ func TestConditionEventually(t *testing.T) { t.Run("condition should Eventually be false", func(t *testing.T) { t.Parallel() - mock := new(testing.T) + mock := new(errorsCapturingT) condition := func() bool { return false } - False(t, Eventually(mock, condition, 100*time.Millisecond, 20*time.Millisecond)) + False(t, Eventually(mock, condition, testTimeout, testTick)) }) t.Run("condition should Eventually be true", func(t *testing.T) { t.Parallel() - mock := new(testing.T) state := 0 condition := func() bool { @@ -48,181 +64,340 @@ func TestConditionEventually(t *testing.T) { return state == 2 } - True(t, Eventually(mock, condition, 100*time.Millisecond, 20*time.Millisecond)) + True(t, Eventually(t, condition, testTimeout, testTick)) }) } -func TestConditionEventuallyWithTFalse(t *testing.T) { +// Check that a long running condition doesn't block Eventually. +// +// See issue 805 (and its long tail of following issues). +func TestConditionEventuallyTimeout(t *testing.T) { t.Parallel() - mock := new(errorsCapturingT) - condition := func(collect *CollectT) { - Fail(collect, "condition fixed failure") - } + t.Run("should fail on timeout", func(t *testing.T) { + t.Parallel() - False(t, EventuallyWithT(mock, condition, 100*time.Millisecond, 20*time.Millisecond)) - Len(t, mock.errors, 2) -} + mock := new(errorsCapturingT) + // A condition function that returns after the Eventually timeout + condition := func() bool { + time.Sleep(5 * time.Millisecond) + return true + } -func TestConditionEventuallyWithTTrue(t *testing.T) { - t.Parallel() - mock := new(errorsCapturingT) + False(t, Eventually(mock, condition, time.Millisecond, time.Microsecond)) + }) + + t.Run("should fail on parent test failed", func(t *testing.T) { + t.Parallel() - counter := 0 - condition := func(collect *CollectT) { - counter++ - True(collect, counter == 2) - } + parentCtx, failParent := context.WithCancel(context.WithoutCancel(t.Context())) + mock := new(errorsCapturingT).WithContext(parentCtx) - True(t, EventuallyWithT(mock, condition, 100*time.Millisecond, 20*time.Millisecond)) - Len(t, mock.errors, 0) - Equal(t, 2, counter, "Condition is expected to be called 2 times") + condition := func() bool { + time.Sleep(testTick) + failParent() // this cancels the parent context (e.g. mocks failing the parent test) + time.Sleep(2 * testTick) + + return true + } + + False(t, Eventually(mock, condition, testTimeout, testTick)) + + t.Run("reported errors should include the context cancellation", func(t *testing.T) { + // assert how this failure is reported + Len(t, mock.errors, 2, "expected to have 2 error messages: 1 for the context canceled, 1 for the never met condition") + + var hasContextCancelled, hasFailedCondition bool + for _, err := range mock.errors { + msg := err.Error() + switch { + case strings.Contains(msg, "context canceled"): + hasContextCancelled = true + case strings.Contains(msg, "never satisfied"): + hasFailedCondition = true + } + } + True(t, hasContextCancelled, "expected a context cancelled error") + True(t, hasFailedCondition, "expected a condition never satisfied error") + }) + }) } -func TestConditionEventuallyWithT_ConcurrencySafe(t *testing.T) { +func TestConditionEventuallySucceedQuickly(t *testing.T) { t.Parallel() - mock := new(errorsCapturingT) - condition := func(collect *CollectT) { - Fail(collect, "condition fixed failure") - } + t.Run("should succeed before the first tick", func(t *testing.T) { + mock := new(errorsCapturingT) + condition := func() bool { return true } - // To trigger race conditions, we run EventuallyWithT with a nanosecond tick. - False(t, EventuallyWithT(mock, condition, 100*time.Millisecond, time.Nanosecond)) - Len(t, mock.errors, 2) + // By making the tick longer than the total duration, we expect that this test would fail if + // we didn't check the condition before the first tick elapses. + True(t, Eventually(mock, condition, testTimeout, 1*time.Second)) + }) } -func TestConditionEventuallyWithT_ReturnsTheLatestFinishedConditionErrors(t *testing.T) { +func TestConditionEventuallyNoLeak(t *testing.T) { t.Parallel() - mock := new(errorsCapturingT) - // We'll use a channel to control whether a condition should sleep or not. - mustSleep := make(chan bool, 2) - mustSleep <- false - mustSleep <- true - close(mustSleep) + t.Run("should output messages in a determined order", func(t *testing.T) { + t.Parallel() - condition := func(collect *CollectT) { - if <-mustSleep { - // Sleep to ensure that the second condition runs longer than timeout. - time.Sleep(time.Second) - return + /* Original output (replaced by integers) from https://github.com/stretchr/testify/issues/1611 + condition_test.go:150: 2026-01-11 12:11:49.34854116 +0100 CET m=+0.000641595 Condition: inEventually = true + condition_test.go:152: 2026-01-11 12:11:49.84944055 +0100 CET m=+0.501540975 Condition: inEventually = true + condition_test.go:147: 2026-01-11 12:11:49.849484723 +0100 CET m=+0.501585149 Condition: end. + condition_test.go:160: 2026-01-11 12:11:49.849500022 +0100 CET m=+0.501600447 Eventually done + condition_test.go:163: 2026-01-11 12:11:49.849508218 +0100 CET m=+0.501608643 End of TestConditionEventuallyNoLeak/should_output_messages_in_a_determined_order + */ + mock := new(errorsCapturingT) + done := make(chan struct{}, 1) + recordedActions := make([]int, 0, 5) + var mx sync.Mutex + record := func(action int) { + mx.Lock() + defer mx.Unlock() + + recordedActions = append(recordedActions, action) } - // The first condition will fail. We expect to get this error as a result. - Fail(collect, "condition fixed failure") - } + inEventually := true + Eventually(mock, + func() bool { + defer func() { + record(2) + done <- struct{}{} + }() + if inEventually { + record(0) + } + time.Sleep(5 * testTimeout) + if inEventually { + record(1) + } + return true + }, + testTimeout, + testTick, + ) + + inEventually = false + record(3) + + <-done + record(4) + record(5) + + const expectedActions = 6 + Len(t, recordedActions, expectedActions, "expected 6 actions to be recorded during this execution", "got:", len(recordedActions)) + True(t, sort.IntsAreSorted(recordedActions), "expected recorded actions to be ordered") + }) - False(t, EventuallyWithT(mock, condition, 100*time.Millisecond, 20*time.Millisecond)) - Len(t, mock.errors, 2) + t.Run("should not leak a go routine for condition execution", func(t *testing.T) { + t.Parallel() + + mock := new(errorsCapturingT) + done := make(chan bool, 1) + + inEventually := true + Eventually(mock, + func() bool { + defer func() { + done <- inEventually + }() + time.Sleep(5 * testTimeout) + + return true + }, + testTimeout, + testTick, + ) + + inEventually = false + result := <-done + True(t, result, "Condition should end while Eventually still runs.") + }) } -func TestConditionEventuallyWithTFailNow(t *testing.T) { +func TestConditionEventuallyWithT(t *testing.T) { t.Parallel() - mock := new(CollectT) - condition := func(collect *CollectT) { - collect.FailNow() - } + t.Run("should complete with false", func(t *testing.T) { + t.Parallel() - False(t, EventuallyWithT(mock, condition, 100*time.Millisecond, 20*time.Millisecond)) - Len(t, mock.errors, 1) -} + mock := new(errorsCapturingT) + counter := 0 + condition := func(collect *CollectT) { + counter++ + Fail(collect, "condition fixed failure") + Fail(collect, "another condition fixed failure") + } -// Check that a long running condition doesn't block Eventually. -// See issue 805 (and its long tail of following issues). -func TestConditionEventuallyTimeout(t *testing.T) { - t.Parallel() - mock := new(testing.T) + False(t, EventuallyWithT(mock, condition, testTimeout, testTick)) - NotPanics(t, func() { - done, done2 := make(chan struct{}), make(chan struct{}) + const expectedErrors = 4 + Len(t, mock.errors, expectedErrors, "expected 2 errors from the condition, and 2 additional errors from Eventually") - // A condition function that returns after the Eventually timeout - condition := func() bool { - // Wait until Eventually times out and terminates - <-done - close(done2) - return true + expectedCalls := int(testTimeout / testTick) + if counter != expectedCalls && counter != expectedCalls+1 { // it may be 5 or 6 depending on how the test schedules + t.Errorf("expected %d calls to the condition, but got %d", expectedCalls, counter) } + }) - False(t, Eventually(mock, condition, time.Millisecond, time.Microsecond)) + t.Run("should complete with true", func(t *testing.T) { + t.Parallel() + + mock := new(errorsCapturingT) + counter := 0 + condition := func(collect *CollectT) { + counter++ + True(collect, counter == 2) + } - close(done) - <-done2 + True(t, EventuallyWithT(mock, condition, testTimeout, testTick)) + Len(t, mock.errors, 0) + const expectedCalls = 2 + Equal(t, expectedCalls, counter, "Condition is expected to have been called 2 times") }) -} -func TestConditionEventuallySucceedQuickly(t *testing.T) { - t.Parallel() - mock := new(testing.T) + t.Run("should complete with fail, on a nanosecond tick", func(t *testing.T) { + t.Parallel() - condition := func() bool { return true } + mock := new(errorsCapturingT) + condition := func(collect *CollectT) { + Fail(collect, "condition fixed failure") + } - // By making the tick longer than the total duration, we expect that this test would fail if - // we didn't check the condition before the first tick elapses. - True(t, Eventually(mock, condition, 100*time.Millisecond, time.Second)) -} + // To trigger race conditions, we run EventuallyWithT with a nanosecond tick. + False(t, EventuallyWithT(mock, condition, testTimeout, time.Nanosecond)) + const expectedErrors = 3 + Len(t, mock.errors, expectedErrors, "expected 1 errors from the condition, and 2 additional errors from Eventually") + }) -func TestConditionEventuallyWithTSucceedQuickly(t *testing.T) { - t.Parallel() - mock := new(testing.T) + t.Run("should complete with fail, with latest failed condition", func(t *testing.T) { + t.Parallel() - condition := func(*CollectT) {} + mock := new(errorsCapturingT) + // We'll use a channel to control whether a condition should sleep or not. + mustSleep := make(chan bool, 2) + mustSleep <- false + mustSleep <- true + close(mustSleep) + + condition := func(collect *CollectT) { + if <-mustSleep { + // Sleep to ensure that the second condition runs longer than timeout. + time.Sleep(time.Second) + return + } + + // The first condition will fail. We expect to get this error as a result. + Fail(collect, "condition fixed failure") + } - // By making the tick longer than the total duration, we expect that this test would fail if - // we didn't check the condition before the first tick elapses. - True(t, EventuallyWithT(mock, condition, 100*time.Millisecond, time.Second)) -} + False(t, EventuallyWithT(mock, condition, testTimeout, testTick)) + const expectedErrors = 3 + Len(t, mock.errors, expectedErrors, "expected 1 errors from the condition, and 2 additional errors from Eventually") + }) -func TestConditionNeverFalse(t *testing.T) { - t.Parallel() + t.Run("should complete with success, with the ticker never used", func(t *testing.T) { + t.Parallel() - condition := func() bool { - return false - } + mock := new(errorsCapturingT) + condition := func(*CollectT) {} - True(t, Never(t, condition, 100*time.Millisecond, 20*time.Millisecond)) -} + // By making the tick longer than the total duration, we expect that this test would fail if + // we didn't check the condition before the first tick elapses. + True(t, EventuallyWithT(mock, condition, testTimeout, time.Second)) + }) -// TestNeverTrue checks Never with a condition that returns true on second call. -func TestConditionNeverTrue(t *testing.T) { - t.Parallel() - mock := new(testing.T) + t.Run("should fail with a call to collect.FailNow", func(t *testing.T) { + t.Parallel() - // A list of values returned by condition. - // Channel protects against concurrent access. - returns := make(chan bool, 2) - returns <- false - returns <- true - defer close(returns) + mock := new(errorsCapturingT) + counter := 0 - // Will return true on second call. - condition := func() bool { - return <-returns - } + // The call to FailNow cancels the execution context of EventuallyWithT. + // so we don't have to wait for the timeout. + condition := func(collect *CollectT) { + counter++ + collect.FailNow() + } - False(t, Never(mock, condition, 100*time.Millisecond, 20*time.Millisecond)) + False(t, EventuallyWithT(mock, condition, 30*time.Minute, testTick)) + const expectedErrors = 2 + Len(t, mock.errors, expectedErrors) // we have 0 accumulated error + 2 errors from EventuallyWithT (includes the timeout) + if counter != 1 { + t.Errorf("expected the condition function to have been called only once, but got: %d", counter) + } + }) } -func TestConditionNeverFailQuickly(t *testing.T) { +func TestConditionNever(t *testing.T) { t.Parallel() - mock := new(testing.T) - // By making the tick longer than the total duration, we expect that this test would fail if - // we didn't check the condition before the first tick elapses. - condition := func() bool { return true } - False(t, Never(mock, condition, 100*time.Millisecond, time.Second)) -} + t.Run("should never be true", func(t *testing.T) { + t.Parallel() -// errorsCapturingT is a mock implementation of TestingT that captures errors reported with Errorf. -type errorsCapturingT struct { - errors []error -} + mock := new(errorsCapturingT) + condition := func() bool { + return false + } -// Helper is like [testing.T.Helper] but does nothing. -func (errorsCapturingT) Helper() {} + True(t, Never(mock, condition, testTimeout, testTick)) + }) + + t.Run("should never be true, on timeout", func(t *testing.T) { + t.Parallel() + + mock := new(errorsCapturingT) + condition := func() bool { + time.Sleep(2 * testTick) + // eventually returns true, after timeout + return true + } -func (t *errorsCapturingT) Errorf(format string, args ...any) { - t.errors = append(t.errors, fmt.Errorf(format, args...)) + True(t, Never(mock, condition, testTick, 1*time.Millisecond)) + }) + + t.Run("should never be true fails", func(t *testing.T) { + // checks Never with a condition that returns true on second call. + t.Parallel() + + mock := new(errorsCapturingT) + // A list of values returned by condition. + // Channel protects against concurrent access. + returns := make(chan bool, 2) + returns <- false + returns <- true + defer close(returns) + + // Will return true on second call. + condition := func() bool { + return <-returns + } + + False(t, Never(mock, condition, testTimeout, testTick)) + }) + + t.Run("should never be true fails, with ticker never triggered", func(t *testing.T) { + t.Parallel() + + mock := new(errorsCapturingT) + // By making the tick longer than the total duration, we expect that this test would fail if + // we didn't check the condition before the first tick elapses. + condition := func() bool { return true } + False(t, Never(mock, condition, testTimeout, time.Second)) + }) + + t.Run("should never be true fails, with parent test failing", func(t *testing.T) { + t.Parallel() + + parentCtx, failParent := context.WithCancel(context.WithoutCancel(t.Context())) + mock := new(errorsCapturingT).WithContext(parentCtx) + condition := func() bool { + failParent() // cancels the parent context, which results in Never to fail + return false + } + False(t, Never(mock, condition, testTimeout, time.Second)) + }) } diff --git a/internal/assertions/equal_test.go b/internal/assertions/equal_test.go index 9765180f4..d5a6ebae9 100644 --- a/internal/assertions/equal_test.go +++ b/internal/assertions/equal_test.go @@ -380,6 +380,9 @@ func panicCases() iter.Seq[panicCase] { type structWithUnexportedMapWithArrayKey struct { m any } + type s struct { + f map[[1]byte]int + } return slices.Values([]panicCase{ { @@ -444,6 +447,17 @@ func panicCases() iter.Seq[panicCase] { }, expectEqual: false, }, + { + name: "panic behavior on map with array key", + value1: s{ + f: map[[1]byte]int{ + {0x1}: 0, + {0x2}: 0, + }, + }, + value2: s{}, + expectEqual: false, + }, }) } diff --git a/internal/assertions/ifaces.go b/internal/assertions/ifaces.go index 22d4847b3..eab6737d0 100644 --- a/internal/assertions/ifaces.go +++ b/internal/assertions/ifaces.go @@ -3,6 +3,8 @@ package assertions +import "context" + // T is an interface wrapper around [testing.T]. type T interface { Errorf(format string, args ...any) @@ -21,3 +23,7 @@ type failNower interface { type namer interface { Name() string } + +type contextualizer interface { + Context() context.Context +} diff --git a/internal/assertions/mock_test.go b/internal/assertions/mock_test.go index efbb3e91c..5112bd52a 100644 --- a/internal/assertions/mock_test.go +++ b/internal/assertions/mock_test.go @@ -5,6 +5,7 @@ package assertions import ( "bytes" + "context" "fmt" "regexp" "runtime" @@ -12,6 +13,17 @@ import ( "testing" ) +var ( + _ T = &mockT{} + _ T = &mockFailNowT{} + _ failNower = &mockFailNowT{} + _ T = &captureT{} + _ T = &bufferT{} + _ T = &dummyT{} + _ T = &errorsCapturingT{} + _ T = &outputT{} +) + type mockT struct { errorFmt string args []any @@ -49,6 +61,59 @@ func (m *mockFailNowT) FailNow() { m.failed = true } +type dummyT struct{} + +func (dummyT) Errorf(string, ...any) {} + +func (dummyT) Context() context.Context { + return context.Background() +} + +// errorsCapturingT is a mock implementation of TestingT that captures errors reported with Errorf. +type errorsCapturingT struct { + errors []error + ctx context.Context //nolint:containedctx // this is ok to support context injection tests +} + +// Helper is like [testing.T.Helper] but does nothing. +func (errorsCapturingT) Helper() {} + +func (t errorsCapturingT) Context() context.Context { + if t.ctx == nil { + return context.Background() + } + + return t.ctx +} + +func (t *errorsCapturingT) WithContext(ctx context.Context) *errorsCapturingT { + t.ctx = ctx + + return t +} + +func (t *errorsCapturingT) Errorf(format string, args ...any) { + t.errors = append(t.errors, fmt.Errorf(format, args...)) +} + +type outputT struct { + buf *bytes.Buffer + helpers map[string]struct{} +} + +// Implements T. +func (t *outputT) Errorf(format string, args ...any) { + s := fmt.Sprintf(format, args...) + t.buf.WriteString(s) +} + +func (t *outputT) Helper() { + if t.helpers == nil { + t.helpers = make(map[string]struct{}) + } + t.helpers[callerName(1)] = struct{}{} +} + type captureT struct { failed bool msg string diff --git a/require/require_assertions.go b/require/require_assertions.go index e509c8078..2dcdeee9d 100644 --- a/require/require_assertions.go +++ b/require/require_assertions.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require @@ -14,7 +14,7 @@ import ( "github.com/go-openapi/testify/v2/internal/assertions" ) -// Condition uses a Comparison to assert a complex condition. +// Condition uses a [Comparison] to assert a complex condition. // // # Usage // @@ -351,12 +351,28 @@ func ErrorIs(t T, err error, target error, msgAndArgs ...any) { 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 the target function on each tick. +// +// [Eventually] waits until the condition returns true, for at most waitFor, +// or until the parent context of the test is cancelled. +// +// If the condition takes longer than waitFor to complete, [Eventually] fails +// but waits for the current condition execution to finish before returning. +// +// For long-running conditions to be interrupted early, check [testing.T.Context] +// which is cancelled on test failure. // // # Usage // -// assertions.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +// assertions.Eventually(t, func() bool { return true }, time.Second, 10*time.Millisecond) +// +// # Concurrency +// +// The condition function is never executed in parallel: only one goroutine executes it. +// It may write to variables outside its scope without triggering race conditions. +// +// A blocking condition will cause [Eventually] to hang until it returns. // // # Examples // @@ -375,14 +391,19 @@ func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Dur 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. +// EventuallyWithT asserts that the given condition will be met in waitFor time, +// periodically checking the target function at each tick. +// +// In contrast to [Eventually], the condition function is supplied with a [CollectT] +// to accumulate errors from calling 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. +// The supplied [CollectT] collects all errors from one tick. +// +// If the condition is not met before waitFor, the collected errors from the +// last tick are copied to t. +// +// Calling [CollectT.FailNow] cancels the condition immediately and fails the assertion. // // # Usage // @@ -391,11 +412,17 @@ func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Dur // time.Sleep(8*time.Second) // externalValue = true // }() +// // assertions.EventuallyWithT(t, func(c *assertions.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assertions.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") // +// # Concurrency +// +// The condition function is never executed in parallel: only one goroutine executes it. +// It may write to variables outside its scope without triggering race conditions. +// // # Examples // // success: func(c *CollectT) { True(c,true) }, 100*time.Millisecond, 20*time.Millisecond @@ -1209,12 +1236,24 @@ func Negative(t T, e any, msgAndArgs ...any) { t.FailNow() } -// Never asserts that the given condition doesn't satisfy in waitFor time, -// periodically checking the target function each tick. +// Never asserts that the given condition is never satisfied within waitFor time, +// periodically checking the target function at each tick. +// +// [Never] is the opposite of [Eventually]. It succeeds if the waitFor timeout +// is reached without the condition ever returning true. +// +// If the parent context is cancelled before the timeout, [Never] fails. // // # Usage // -// assertions.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +// assertions.Never(t, func() bool { return false }, time.Second, 10*time.Millisecond) +// +// # Concurrency +// +// The condition function is never executed in parallel: only one goroutine executes it. +// It may write to variables outside its scope without triggering race conditions. +// +// A blocking condition will cause [Never] to hang until it returns. // // # Examples // diff --git a/require/require_assertions_test.go b/require/require_assertions_test.go index 5e6f53a5e..48d725061 100644 --- a/require/require_assertions_test.go +++ b/require/require_assertions_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require diff --git a/require/require_examples_test.go b/require/require_examples_test.go index bc1a05118..18eeb4555 100644 --- a/require/require_examples_test.go +++ b/require/require_examples_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require_test diff --git a/require/require_format.go b/require/require_format.go index 41fdeac1f..b704fa554 100644 --- a/require/require_format.go +++ b/require/require_format.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require diff --git a/require/require_format_test.go b/require/require_format_test.go index a4da38239..a8c1b7528 100644 --- a/require/require_format_test.go +++ b/require/require_format_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require diff --git a/require/require_forward.go b/require/require_forward.go index 941b876e7..416048630 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require diff --git a/require/require_forward_test.go b/require/require_forward_test.go index e43372a0c..76a3d146a 100644 --- a/require/require_forward_test.go +++ b/require/require_forward_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require diff --git a/require/require_helpers.go b/require/require_helpers.go index 3bc26dfed..4a49efac6 100644 --- a/require/require_helpers.go +++ b/require/require_helpers.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require diff --git a/require/require_helpers_test.go b/require/require_helpers_test.go index b507f90c3..f3fd8b9a5 100644 --- a/require/require_helpers_test.go +++ b/require/require_helpers_test.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require diff --git a/require/require_types.go b/require/require_types.go index 80e19f708..a15626596 100644 --- a/require/require_types.go +++ b/require/require_types.go @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. -// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] +// Code generated with github.com/go-openapi/testify/codegen/v2; DO NOT EDIT. +// Generated on 2026-01-11 (version e6b0793) using codegen version v2.1.9-0.20260111152118-e6b0793ba519+dirty [sha: e6b0793ba519fb22dc1887392e1465649a5a95ff] package require @@ -25,6 +25,9 @@ type ( BoolAssertionFunc func(T, bool, ...any) // CollectT implements the [T] interface and collects all errors. + // + // [CollectT] is specifically intended to be used with [EventuallyWithT] and + // should not be used outside of that context. CollectT = assertions.CollectT // Comparison is a custom function that returns true on success and false on failure.