Documentation
¶
Overview ¶
Package testwait provides typed test-wait markers that replace bare require.Eventually / assert.Eventually polling. Two semantics are exposed as separate functions (typed function choice):
- External: polling REQUIRED because no channel signal is producible (HTTP /healthz, container readiness, subprocess signal handler). Caller MUST provide a const-literal kebab-case reason documenting why polling is unavoidable. Downstream form-uniqueness is locked by archtest TEST-POLLING-EXTERNAL-REASON-LITERAL-01 ((callee, arg) uniqueness on testwait.External); upstream form-uniqueness is locked by sibling archtest TEST-EVENTUALLY-FUNNEL-01 (bans bare require.Eventually / assert.Eventually / *WithT across the module). Together they form a Hard funnel per ai-robust.md §"Funnel 双向锁评级" and Hard 范本 "typed marker funnel for unbounded ops" (sibling of panicregister.Approved).
- Deterministic: blocks on a channel signal — no polling, no race window. The default choice; use External only as carve-out. Hard via Go type system: <-chan T signature makes "polling via Deterministic" unrepresentable. The internal safety-net timeout is [signalSafetyNet]; callers do NOT pass a timeout — the signal arrival IS the synchronization point, so any caller-supplied timeout would be a latency budget, not a hung-test guard.
Polling is the leading source of race-CI flakes in GoCell tests; see docs/plans/202605181600-042-archtest.md §1.1 (TEST-POLLING-DETERMINISM).
ref: stretchr/testify pull/1657 (synchronous Eventually proposal — root of
External's synchronous-poll design, eliminating testify #1611 goroutine leak and #865 race window)
ref: kubernetes/apimachinery pkg/util/wait/loop.go (synchronous main loop
with explicit ctx.Err checks between ticks)
ref: pkg/panicregister/panicregister.go (typed-marker funnel sibling pattern)
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Deterministic ¶
Deterministic blocks on signal until it receives a value or the internal safety-net timeout fires. On success returns the received value; on safety-net expiry calls t.Fatalf and returns the zero value of T.
The timeout is NOT caller-configurable. Deterministic is designed for signals that are guaranteed to arrive in a correct run; the internal [signalSafetyNet] is a hung-test guard only. A caller-supplied timeout would be a latency budget (the root cause of race-CI flakes in observer_test.go, issue #1320). If you genuinely need a poll with a configurable timeout, use External instead.
This is the polling-free wait: the channel signal IS the synchronization point. No closure capture, no race window with concurrent producers — Go's channel happens-before guarantee replaces the manual polling required by External.
Hard via Go type system: signal's <-chan T signature lets callers receive any payload type but forbids "polling via Deterministic" — the API name and signature together pin the channel-blocking semantics. The absence of a caller-facing timeout parameter is additionally locked by archtest TEST-DETERMINISTIC-NO-TIMEOUT-PARAM-01 (Hard downstream form-lock; Medium upstream — "no new timeout param" is a Go-language ceiling, same shape as #851/#893/#1282: the axis "who may declare a given function's parameters" is not expressible in the Go type system).
For CI grep-ability, pass a short label string as the first msgAndArg:
testwait.Deterministic(t, sig, "session-created")
The label appears verbatim in the t.Fatalf output, making the timeout site findable via `grep "session-created" ci.log`.
func External ¶
func External(t TB, reason string, condition func() bool, timeout, tick time.Duration, msgAndArgs ...any, )
External polls condition until it returns true or timeout fires.
Note: parameter order is (t, reason, condition, ...) which DIFFERS from testify's (t, condition, ...). reason precedes condition by design — see archtest TEST-POLLING-EXTERNAL-REASON-LITERAL-01 for the rationale: the reason must be a visible const literal at the callsite, placing it as the second argument (immediately after t) makes it hard to omit accidentally and easy to find when grep-scanning CI failure output.
reason MUST be a const string literal in kebab-case form (matching ^[a-z][a-z0-9-]+$); archtest TEST-POLLING-EXTERNAL-REASON-LITERAL-01 rejects any other form (variable, fmt.Sprintf, concatenation, const ident).
Polling runs synchronously on the caller goroutine. No per-tick goroutine is spawned — this eliminates the goroutine leak of testify Eventually (stretchr/testify#1611) and the closure-capture race window (stretchr/testify#865 / #835) that previously caused race-CI flakes (D500ms incident).
On timeout the function calls t.Fatalf with the reason and the supplied msgAndArgs. On condition panic the panic propagates to the caller — it is not recovered, mirroring `require.Eventually`'s synchronous main-thread runner semantics from testify pull/1657.
tick should be < timeout to ensure at least one poll occurs before the timeout fires.
Prefer Deterministic whenever a channel signal is producible from the system under test.
Types ¶
type TB ¶
TB is the testing surface required by External and Deterministic. It matches the subset of testing.TB the helpers actually use, so test code that supplies a spy implementation (e.g. for self-tests that need to observe Fatalf without aborting) can do so without depending on every method of testing.TB.