Skip to content

Commit 16f6c61

Browse files
authored
feat(condition): added opt-in support for synctest in async assertions (#100)
doc: updated docs for synctest support, with additional ad'hoc examples for synctest doc: fixed rendering of testable examples Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent bfe5158 commit 16f6c61

24 files changed

Lines changed: 2067 additions & 809 deletions
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package assert_test
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"sync/atomic"
11+
"testing"
12+
"time"
13+
14+
"github.com/go-openapi/testify/v2/assert"
15+
)
16+
17+
// ExampleWithSynctest_asyncReady demonstrates opting into [testing/synctest]
18+
// bubble polling via [assert.WithSynctest]. Time operations inside the bubble
19+
// use a fake clock — a 1-hour timeout with a 1-minute tick completes in
20+
// microseconds of real wall-clock time while remaining deterministic.
21+
//
22+
// Prefer this wrapper when the condition is pure compute or uses [time.Sleep]
23+
// internally. See [assert.WithSynctest] for the constraints (no real I/O, no
24+
// external goroutines driving state change).
25+
func ExampleEventually_withSyncTest() {
26+
t := new(testing.T) // normally provided by test
27+
28+
// A counter that converges on the 5th poll — no external time pressure.
29+
var attempts atomic.Int32
30+
cond := func() bool {
31+
return attempts.Add(1) == 5
32+
}
33+
34+
// 1-hour/1-minute: under fake time this is instantaneous and
35+
// deterministic — exactly 5 calls to the condition.
36+
result := assert.Eventually(t, assert.WithSynctest(cond), 1*time.Hour, 1*time.Minute)
37+
38+
fmt.Printf("ready: %t, attempts: %d", result, attempts.Load())
39+
40+
// Output: ready: true, attempts: 5
41+
}
42+
43+
// ExampleWithSynctestContext_healthCheck demonstrates the context/error
44+
// variant of the synctest opt-in. [assert.WithSynctestContext] wraps a
45+
// [func(context.Context) error] condition for fake-time polling.
46+
func ExampleEventually_withContext() {
47+
t := new(testing.T) // normally provided by test
48+
49+
var attempts atomic.Int32
50+
healthCheck := func(_ context.Context) error {
51+
if attempts.Add(1) < 3 {
52+
return errors.New("service not ready")
53+
}
54+
55+
return nil
56+
}
57+
58+
result := assert.Eventually(t, assert.WithSynctestContext(healthCheck), 1*time.Hour, 1*time.Minute)
59+
60+
fmt.Printf("healthy: %t, attempts: %d", result, attempts.Load())
61+
62+
// Output: healthy: true, attempts: 3
63+
}
64+
65+
// ExampleWithSynctest_never demonstrates [assert.Never] with the synctest
66+
// opt-in. The condition is polled over the fake-time window without costing
67+
// real wall-clock time.
68+
func ExampleNever_withSyncTest() {
69+
t := new(testing.T) // normally provided by test
70+
71+
// A flag that should remain false across the whole observation period.
72+
var flipped atomic.Bool
73+
result := assert.Never(t, assert.WithSynctest(flipped.Load), 1*time.Hour, 1*time.Minute)
74+
75+
fmt.Printf("never flipped: %t", result)
76+
77+
// Output: never flipped: true
78+
}
79+
80+
// ExampleWithSynctest_consistently demonstrates [assert.Consistently] with
81+
// the synctest opt-in — asserting an invariant holds across the entire
82+
// observation window under deterministic fake time.
83+
func ExampleConsistently_withSynctest() {
84+
t := new(testing.T) // normally provided by test
85+
86+
// An invariant that must hold throughout the observation period.
87+
var counter atomic.Int32
88+
counter.Store(5)
89+
invariant := func() bool { return counter.Load() < 10 }
90+
91+
result := assert.Consistently(t, assert.WithSynctest(invariant), 1*time.Hour, 1*time.Minute)
92+
93+
fmt.Printf("invariant held: %t", result)
94+
95+
// Output: invariant held: true
96+
}
97+
98+
// ExampleWithSynctestCollect_convergence demonstrates [assert.EventuallyWith]
99+
// with [assert.WithSynctestCollect] — a [CollectT]-based condition polled
100+
// inside a synctest bubble. Useful when the condition uses several require /
101+
// assert calls and you want deterministic retry behavior.
102+
func ExampleEventuallyWith_withSynctest() {
103+
t := new(testing.T) // normally provided by test
104+
105+
var attempts atomic.Int32
106+
cond := func(c *assert.CollectT) {
107+
n := attempts.Add(1)
108+
assert.Equal(c, int32(3), n, "not yet converged")
109+
}
110+
111+
result := assert.EventuallyWith(t, assert.WithSynctestCollect(cond), 1*time.Hour, 1*time.Minute)
112+
113+
fmt.Printf("converged: %t, attempts: %d", result, attempts.Load())
114+
115+
// Output: converged: true, attempts: 3
116+
}

assert/assert_assertions.go

Lines changed: 35 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assert/assert_format.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assert/assert_forward.go

Lines changed: 0 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assert/assert_forward_test.go

Lines changed: 0 additions & 58 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assert/assert_types.go

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)