Atlas › Test

TestAwaitTrueFalseRetry

Exact test identity: go.temporal.io/server/common/testing/parallelsuite/TestRun_AcceptsSuite/await_true/TestAwaitTrueFalseRetry

Package
go.temporal.io/server/common/testing/parallelsuite
Suite / test hierarchy
TestRun_AcceptsSuite/await_true/TestAwaitTrueFalseRetry
Test
TestAwaitTrueFalseRetry
Introduced at
TestAwaitTrueFalseRetry Frontier kind: Test frontier
Covered ranges
82
Covered lines
286
Covered files
10

Covered source

Expand a file to inspect source; the > gutter marks covered lines.

go.temporal.io/server/common/testing/parallelsuite/suite.go 93 covered LOC · 26 ranges

Open complete file

74 //
75 //nolint:revive // ctx is last so callers can pass nil to mean "no override"; SA1012 forbids passing nil as the first ctx arg.
76 > func (s *Suite[T]) copySuite(t *testing.T, parallel bool, assertT require.TestingT, ctx context.Context) testingSuite { suite.go
77 > cp := reflect.New(reflect.TypeFor[T]().Elem()).Interface().(T)
78 > cp.initSuite(t, parallel, assertT, ctx)
79 > return cp
80 > }
81
82 //nolint:revive // see copySuite above.
83 > func (s *Suite[T]) initSuite(t *testing.T, parallel bool, assertT require.TestingT, ctx context.Context) { suite.go
84 > g := &s.guardT
85 > g.name = t.Name()
86 > g.T = t
87 > g.hasSubtests.Store(false)
88 > s.runParallel = parallel
89 > s.ctx = ctx
90 > s.ctxOnce = sync.Once{}
91 > if s.runParallel {
92 > t.Parallel() //nolint:testifylint // parallelsuite intentionally supports parallel tests
93 > }
94 > if assertT == nil {
95 > assertT = g
96 > }
97 > s.assertT = assertT
98 > s.Assertions = require.New(assertT)
99 > s.ProtoAssertions = protorequire.New(assertT)
100 > s.HistoryRequire = historyrequire.New(assertT)
101 }
102
103 // T returns the *testing.T, panicking if the guard has been sealed.
104 > func (s *Suite[T]) T() *testing.T { suite.go
105 > if s.guardT.hasSubtests.Load() {
106 panic("parallelsuite: do not call T() after Run(); use the subtest callback's parameter instead")
107 }
108 > return s.guardT.T suite.go
109 }
110
161 //
162 // Use it for simple local predicates only. Do not use assertions or side effects; use [Await] instead.
163 > func (s *Suite[T]) AwaitTrue(fn func() bool, timeout, interval time.Duration) { suite.go
164 > s.AwaitTruef(fn, timeout, interval, "")
165 > }
166
167 // AwaitTruef is like [AwaitTrue] but includes a format string appended to the failure message.
168 > func (s *Suite[T]) AwaitTruef(fn func() bool, timeout, interval time.Duration, msg string, args ...any) { suite.go
169 > await.RequireTruef(s.T(), fn, timeout, interval, msg, args...)
170 > }
171
172 // Run discovers and runs all exported Test* methods on the given suite in parallel.
177 //
178 // The suite must embed [Suite] and have no other fields.
179 > func Run[T testingSuite](t *testing.T, s T, args ...any) { suite.go
180 > run(t, s, true, args...)
181 > }
182
183 // RunLegacySequential behaves like [Run] but does not mark test methods as parallel.
189 }
190
191 > func run[T testingSuite](t *testing.T, s T, methodsParallel bool, args ...any) { suite.go
192 > t.Helper()
193 >
194 > typ := reflect.TypeFor[T]()
195 > if typ.Kind() != reflect.Pointer || typ.Elem().Kind() != reflect.Struct {
196 panic(fmt.Sprintf("parallelsuite.Run: suite must be a pointer to a struct, got %v", typ))
197 }
198 > structType := typ.Elem() suite.go
199 >
200 > validateSuiteStruct(structType)
201 >
202 > methods := discoverTestMethods(typ, structType, args)
203 > if len(methods) == 0 {
204 panic(fmt.Sprintf("parallelsuite.Run: suite %s has no Test* methods", structType.Name()))
205 }
206
207 > methods = applyTestifyMFilter(methods) suite.go
208 > if len(methods) == 0 {
209 return // all methods filtered by -testify.m; nothing to run
210 }
211
212 > argVals := make([]reflect.Value, len(args)) suite.go
213 > for i, a := range args {
214 argVals[i] = reflect.ValueOf(a)
215 }
216
217 > s.initSuite(t, true, nil, nil) suite.go
218 >
219 > for _, method := range methods {
220 > t.Run(method.Name, func(t *testing.T) {
221 > cpS := s.copySuite(t, methodsParallel, nil, nil)
222 > callArgs := append([]reflect.Value{reflect.ValueOf(cpS)}, argVals...)
223 > method.Func.Call(callArgs)
224 > })
225 }
226 }
228 var inheritedMethods map[string]bool
229
230 > func init() { suite.go
231 > type ds struct{ Suite[*ds] }
232 > ptrType := reflect.TypeFor[*ds]()
233 > inheritedMethods = make(map[string]bool, ptrType.NumMethod())
234 > for method := range ptrType.Methods() {
235 > inheritedMethods[method.Name] = true
236 > }
237 }
238
239 > func validateSuiteStruct(structType reflect.Type) { suite.go
240 > if !strings.HasSuffix(structType.Name(), "Suite") {
241 panic(fmt.Sprintf("parallelsuite.Run: struct name %q must end with \"Suite\"", structType.Name()))
242 }
243
244 > if structType.NumField() != 1 { suite.go
245 panic(fmt.Sprintf(
246 "parallelsuite.Run: suite %s must have no fields besides the embedded parallelsuite.Suite; "+
249 ))
250 }
251 > f := structType.Field(0) suite.go
252 > if !f.Anonymous {
253 panic(fmt.Sprintf(
254 "parallelsuite.Run: suite %s must embed parallelsuite.Suite, found named field %q",
263 // The flag is registered by testify's suite package (imported above); we share
264 // that registration via flag.Lookup rather than registering it a second time.
265 > func applyTestifyMFilter(methods []reflect.Method) []reflect.Method { suite.go
266 > f := flag.Lookup("testify.m")
267 > if f == nil {
268 return methods
269 }
270 > pattern := f.Value.String() suite.go
271 > if pattern == "" {
272 > return methods suite.go
273 > }
274 re, err := regexp.Compile(pattern)
275 if err != nil {
285 }
286
287 > func discoverTestMethods(ptrType, structType reflect.Type, args []any) []reflect.Method { suite.go
288 > expectedNumIn := 1 + len(args)
289 >
290 > for method := range ptrType.Methods() {
291 > name := method.Name
292 > if !strings.HasPrefix(name, "Test") && !inheritedMethods[name] {
293 panic(fmt.Sprintf(
294 "parallelsuite.Run: suite %s has exported method %s that does not start with Test; "+
299 }
300
301 > var methods []reflect.Method suite.go
302 > for method := range ptrType.Methods() {
303 > if !strings.HasPrefix(method.Name, "Test") {
304 > continue
305 }
306
307 > mt := method.Type suite.go
308 > if mt.NumOut() != 0 {
309 panic(fmt.Sprintf(
310 "parallelsuite.Run: method %s.%s must not have return values, got %v",
312 ))
313 }
314 > if mt.NumIn() != expectedNumIn { suite.go
315 panic(fmt.Sprintf(
316 "parallelsuite.Run: method %s.%s has wrong number of parameters: expected %d, got %d (%v)",
319 }
320
321 > for j, a := range args { suite.go
322 paramType := mt.In(1 + j)
323 argType := reflect.TypeOf(a)
330 }
331
332 > methods = append(methods, method) suite.go
333 }
334 > return methods suite.go
335 }
go.temporal.io/server/common/testing/await/require_ctx.go 78 covered LOC · 28 ranges

Open complete file

40 const defaultHardDeadlockTimeout = 10 * time.Second
41
42 > func hardDeadlockTimeout() time.Duration { require_ctx.go
43 > if s := os.Getenv(hardDeadlockTimeoutEnvVar); s != "" {
44 if d, err := time.ParseDuration(s); err == nil {
45 return d
46 }
47 }
48 > return defaultHardDeadlockTimeout require_ctx.go
49 }
50
74 misuseHint string,
75 cancellable bool,
76 > ) { require_ctx.go
77 > tb.Helper()
78 >
79 > // Skip if the test already failed — no point polling.
80 > if tb.Failed() {
81 tb.Logf("%s: skipping (test already failed)", funcName)
82 return
83 }
84 // Guard: context.WithDeadline panics on a nil parent.
85 > if parentCtx == nil { require_ctx.go
86 tb.Fatalf("%s: nil context", funcName)
87 return
88 }
89
90 > deadline := time.Now().Add(cfg.totalTimeout) require_ctx.go
91 >
92 > // Cap at the parent context's deadline if it's earlier than our timeout.
93 > if parentDeadline, hasDeadline := parentCtx.Deadline(); hasDeadline && parentDeadline.Before(deadline) {
94 deadline = parentDeadline
95 }
97 // Cap at the test's deadline if it's earlier than our deadline.
98 // Ideally, the parent context already accounts for the test's deadline - but we are being defensive.
99 > if d, ok := tb.(interface{ Deadline() (time.Time, bool) }); ok { require_ctx.go
100 > if testDeadline, hasDeadline := d.Deadline(); hasDeadline && testDeadline.Before(deadline) { require_ctx.go
101 deadline = testDeadline
102 }
103 }
104
105 > effectiveTimeout := max(0, time.Until(deadline)) require_ctx.go
106 > awaitCtx, awaitCancel := context.WithDeadline(parentCtx, deadline)
107 > defer awaitCancel()
108 >
109 > report := timeoutReport{effectiveTimeout: effectiveTimeout}
110 >
111 > for {
112 > // Parent context was canceled while we were sleeping (not our deadline).
113 > if err := awaitCtx.Err(); err != nil && !deadlineReached(deadline) {
114 report.reportAttemptErrors(tb)
115 tb.Fatalf("%s: context canceled before condition was satisfied: %v", funcName, err)
117 }
118
119 > report.nextPoll() require_ctx.go
120 >
121 > // Per-attempt context: bounded by the configured attempt timeout and
122 > // further capped by the overall awaitCtx.
123 > attemptCtx, attemptCancel := context.WithTimeout(awaitCtx, cfg.attemptTimeout)
124 > t := &T{tb: tb, ctx: attemptCtx}
125 >
126 > // Run attempt.
127 > res := runAttempt(t, condition, attemptCancel, funcName, cancellable)
128 > attemptCancel()
129 > if res.panicVal != nil {
130 panic(res.panicVal) // propagate to caller
131 }
132 > if res.deadlocked { require_ctx.go
133 report.reportAttemptErrors(tb)
134 if cancellable {
141 return
142 }
143 > report.recordErrors(t.errors) require_ctx.go
144 >
145 > // Attempt-timeout expiry: attemptCtx is done but awaitCtx is not.
146 > // Record nothing special - the attempt's recorded errors (if any)
147 > // already describe what went wrong; otherwise we just retry.
148 > attemptHitOwnTimeout := attemptCtx.Err() == context.DeadlineExceeded && awaitCtx.Err() == nil
149 > if attemptHitOwnTimeout {
150 report.recordAttemptTimeout()
151 }
152
153 // Check misuse where the real test failed instead of just the attempt.
154 > if tb.Failed() { require_ctx.go
155 tb.Fatalf("%s: the test was marked failed directly — %s", funcName, misuseHint)
156 return
158
159 // Parent context was canceled during the attempt (not our deadline).
160 > if err := awaitCtx.Err(); err != nil && !deadlineReached(deadline) { require_ctx.go
161 report.reportAttemptErrors(tb)
162 tb.Fatalf("%s: context canceled before condition was satisfied: %v", funcName, err)
165
166 // Our deadline expired.
167 > if deadlineReached(deadline) { require_ctx.go
168 report.reportTimeout(tb, funcName, cfg.timeoutMsg)
169 return
171
172 // Success: attempt completed without failures.
173 > if !res.stopped && !t.Failed() && !attemptHitOwnTimeout { require_ctx.go
174 > return require_ctx.go
175 > }
176
177 // Wait for pollInterval, or context is canceled or deadline is reached.
178 > sleep(awaitCtx, deadline, cfg.pollInterval) require_ctx.go
179 }
180 }
211 funcName string,
212 cancellable bool,
213 > ) attemptResult { require_ctx.go
214 > done := make(chan attemptResult, 1)
215 >
216 > go func() {
217 > completed := false
218 > defer func() {
219 > if r := recover(); r != nil { require_ctx.go
220 if _, ok := r.(attemptFailed); ok {
221 done <- attemptResult{stopped: true}
227 // recover returned nil: either normal return (completed=true) or
228 // runtime.Goexit (completed=false; Goexit is not a panic).
229 > done <- attemptResult{stopped: !completed} require_ctx.go
230 }()
231 > condition(t) require_ctx.go
232 > completed = true
233 }()
234
235 > if cancellable { require_ctx.go
236 // Soft phase: wait for the condition, our soft timer, or parent cancel.
237 softTimer := time.NewTimer(softDeadlockTimeout())
255
256 // Hard phase: wait for the condition or the hard timer.
257 > hardTimer := time.NewTimer(hardDeadlockTimeout()) require_ctx.go
258 > defer hardTimer.Stop()
259 >
260 > select {
261 > case r := <-done: require_ctx.go
262 > return r
263 case <-hardTimer.C:
264 return attemptResult{deadlocked: true}
266 }
267
268 > func sleep(ctx context.Context, deadline time.Time, pollInterval time.Duration) { require_ctx.go
269 > remaining := time.Until(deadline)
270 > if remaining < pollInterval {
271 pollInterval = remaining
272 }
273
274 > timer := time.NewTimer(pollInterval) require_ctx.go
275 > defer timer.Stop()
276 >
277 > select {
278 case <-ctx.Done():
279 > case <-timer.C: require_ctx.go
280 }
281 }
282
283 > func deadlineReached(deadline time.Time) bool { require_ctx.go
284 > return !time.Now().Before(deadline)
285 > }
go.temporal.io/server/common/testing/testcontext/context.go 70 covered LOC · 15 ranges

Open complete file

39
40 // DefaultTimeout returns the effective default timeout for test-scoped contexts.
41 > func DefaultTimeout() time.Duration { context.go
42 > return effectiveTimeout(0)
43 > }
44
45 // For returns the test-scoped context for tb. The context is canceled
49 // return the same context, but an explicit different timeout fails instead of
50 // being silently ignored.
51 > func For(tb testing.TB, opts ...Option) context.Context { context.go
52 > tb.Helper()
53 >
54 > cfg := config{timeout: DefaultTimeout()}
55 > for _, opt := range opts {
56 opt(&cfg)
57 }
58
59 > st := getContextState(tb, cfg.timeout) context.go
60 > st.configure(tb, cfg)
61 > return st.context()
62 }
63
98 }
99
100 > func getContextState(tb testing.TB, timeout time.Duration) *contextState { context.go
101 > tb.Helper()
102 >
103 > testContexts.Lock()
104 > defer testContexts.Unlock()
105 >
106 > if st, ok := testContexts.byTest[tb]; ok {
107 return st
108 }
109
110 > ctx, cancel := context.WithTimeout(tb.Context(), timeout) context.go
111 >
112 > // Annotate gRPC requests with the test name for OTEL tracing.
113 > ctx = metadata.AppendToOutgoingContext(ctx, testNameMetadataKey, tb.Name())
114 >
115 > st := &contextState{
116 > ctx: ctx,
117 > cancel: cancel,
118 > timeout: timeout,
119 > decorators: make(map[any]struct{}),
120 > }
121 > testContexts.byTest[tb] = st
122 >
123 > tb.Cleanup(func() {
124 > err := st.err()
125 > st.cancel()
126 > testContexts.Lock()
127 > delete(testContexts.byTest, tb)
128 > testContexts.Unlock()
129 > if err == context.DeadlineExceeded {
130 tb.Errorf("test exceeded timeout of %v", st.timeout)
131 }
132 > st.release() context.go
133 })
134 > return st context.go
135 }
136
137 > func (s *contextState) configure(tb testing.TB, cfg config) { context.go
138 > tb.Helper()
139 >
140 > s.mu.Lock()
141 > defer s.mu.Unlock()
142 >
143 > if cfg.timeoutSet && cfg.timeout != s.timeout {
144 tb.Fatalf("testcontext: test context already exists with timeout %v; cannot change it to %v", s.timeout, cfg.timeout)
145 }
168 }
169
170 > func (s *contextState) context() context.Context { context.go
171 > s.mu.Lock()
172 > defer s.mu.Unlock()
173 > return s.ctx
174 > }
175
176 > func (s *contextState) err() error { context.go
177 > s.mu.Lock()
178 > defer s.mu.Unlock()
179 > return s.ctx.Err()
180 > }
181
182 > func (s *contextState) release() { context.go
183 > s.mu.Lock()
184 > defer s.mu.Unlock()
185 > s.ctx = nil
186 > }
187
188 > func effectiveTimeout(customTimeout time.Duration) (timeout time.Duration) { context.go
189 > defer func() {
190 > // Build flag TEMPORAL_DEBUG applies a timeout multiplier to all test timeouts.
191 > timeout *= debug.TimeoutMultiplier
192 > }()
193
194 // 1. Custom timeout (via WithTimeout option).
195 > if customTimeout > 0 { context.go
196 return customTimeout
197 }
198
199 // 2. TEMPORAL_TEST_TIMEOUT environment variable.
200 > if envTimeout := os.Getenv("TEMPORAL_TEST_TIMEOUT"); envTimeout != "" { context.go
201 if dur, err := time.ParseDuration(envTimeout); err == nil && dur > 0 {
202 return dur
205
206 // 3. Default timeout.
207 > return defaultTimeout context.go
208 }
go.temporal.io/server/common/testing/await/config.go 17 covered LOC · 3 ranges

Open complete file

17 }
18
19 > func newConfig() config { config.go
20 > return config{
21 > attemptTimeout: envDuration(attemptTimeoutEnvVar, 10*time.Second) * debug.TimeoutMultiplier,
22 > }
23 > }
24
25 > func legacyConfig(timeout, pollInterval time.Duration, timeoutMsg string) config { config.go
26 > cfg := newConfig()
27 > cfg.totalTimeout = timeout
28 > cfg.pollInterval = pollInterval
29 > cfg.timeoutMsg = timeoutMsg
30 > return cfg
31 > }
32
33 > func envDuration(name string, fallback time.Duration) time.Duration { config.go
34 > if s := os.Getenv(name); s != "" {
35 > if d, err := time.ParseDuration(s); err == nil && d > 0 {
36 > return d
37 > }
38 }
39 return fallback
go.temporal.io/server/common/testing/await/require_true.go 6 covered LOC · 2 ranges

Open complete file

27 // RequireTruef is like [RequireTrue] but accepts a format string that is included
28 // in the failure message when the condition is not satisfied before the timeout.
29 > func RequireTruef(tb testing.TB, condition func() bool, timeout, pollInterval time.Duration, msg string, args ...any) { require_true.go
30 > tb.Helper()
31 > run(testcontext.For(tb), tb, func(t *T) {
32 > if !condition() {
33 > t.Fail() require_true.go
34 > }
35 }, legacyConfig(timeout, pollInterval, fmt.Sprintf(msg, args...)), "RequireTruef", requireTrueMisuseHint, false)
36 }
go.temporal.io/server/common/testing/await/t.go 6 covered LOC · 2 ranges

Open complete file

47
48 // Fail marks the current attempt as failed without stopping it.
49 > func (t *T) Fail() { t.go
50 > t.failed = true
51 > }
52
53 // Error records an error message for reporting on timeout.
83
84 // Failed reports whether this attempt has failed.
85 > func (t *T) Failed() bool { t.go
86 > return t.failed
87 > }
88
89 // Helper marks the calling function as a test helper.
go.temporal.io/server/common/testing/await/report.go 5 covered LOC · 2 ranges

Open complete file

28 }
29
30 > func (r *timeoutReport) nextPoll() { report.go
31 > r.attempts++
32 > }
33
34 > func (r *timeoutReport) recordErrors(errors []string) { report.go
35 > if len(errors) > 0 {
36 r.failures = append(r.failures, attemptFailure{attempt: r.attempts, errors: errors})
37 }
go.temporal.io/server/common/testing/historyrequire/history_require.go 5 covered LOC · 1 range

Open complete file

36 )
37
38 > func New(t require.TestingT) HistoryRequire { history_require.go
39 > return HistoryRequire{
40 > t: t,
41 > }
42 > }
43
44 // TODO (maybe):
go.temporal.io/server/common/testing/parallelsuite/guard.go 3 covered LOC · 2 ranges

Open complete file

16 }
17
18 > func (g *guardT) Helper() { guard.go
19 > if g.hasSubtests.Load() {
20 panic(fmt.Sprintf(
21 "parallelsuite: assertion called on %q after Run() was called; "+
24 ))
25 }
26 > g.T.Helper() guard.go
27 }
28
go.temporal.io/server/common/testing/protorequire/require.go 3 covered LOC · 1 range

Open complete file

38 }
39
40 > func New(t require.TestingT) ProtoAssertions { require.go
41 > return ProtoAssertions{t}
42 > }
43
44 // ProtoEqual compares two proto messages for equality using proto semantics. Options can be passed to customize