testwait

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 2 Imported by: 0

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

func Deterministic[T any](t TB, signal <-chan T, msgAndArgs ...any) T

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

type TB interface {
	Helper()
	Fatalf(format string, args ...any)
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL