eventually

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package eventually is the one way a test in this repository waits for something to become true.

It exists because of a measurement taken on 2026-09-02. Four named await helpers — awaitRaft, awaitReload, waitUntil, awaitOutput — plus five more inside a single end-to-end suite were each a private re-typing of the same loop, and a dozen further waits were spelled as a sleep inside a for. In a tree where 89 test files already had a polling library on the import path. The primitive was in the dependency tree the whole time; what was missing was a typed shell over it, so every author wrote their own.

The typed shell

Await takes the poll and the predicate as typed function values and hands back the value that satisfied the predicate. That is the whole difference from calling the polling library directly, and it is the shape CS-7 asks for: the reflection-driven, any-typed engine underneath is real, is correct, and stays — but it is erased exactly once, inside this package, instead of at every call site in the repository.

It buys a better failure as well as a better signature, which is the part that shows up on a Tuesday:

Eventually(func() bool { return len(sent) == 1 }).Should(BeTrue())

fails with "Expected <bool>: false to be true", which says nothing a reader can act on. Polling the value and judging it with a predicate fails with the slice that was actually there.

Budgets are named, and generous

A Budget is a wall clock, and the cost of the two mistakes is not symmetric: a budget that is too large makes a failing test slow, while a budget that is too small makes a correct test red on a loaded machine. State it generously and give it a name in the suite that uses it. The warden CLI contract suite spent a month flaking because "5s" sat inline in one helper where nobody argued with it.

What is not an await

A sleep that generates load, paces a sender, throttles a link, or defines an observation window is the subject of its test rather than a wait for one, and nothing here replaces it. The chaos suite's slow-client throttle is the clearest case: the sleep is the slow client. Converting one of those to a poll deletes the experiment.

Consistently is the negative-space twin, for the assertion that something does *not* happen — no violation is raised, no goroutine appears, the queue never grows. A bare sleep followed by one read is that assertion sampled once, at the least informative moment.

Index

Constants

View Source
const DefaultInterval = 20 * time.Millisecond

DefaultInterval is how often an await looks when its budget does not say.

It is deliberately short relative to any honest budget. Polling frequency costs a test a few function calls; the budget is the number that decides whether a loaded machine fails a correct test, and the two are not the same dial even though both are durations.

Variables

View Source
var ErrNoBudget = errors.New("eventually: a wait needs a positive budget")

ErrNoBudget reports a wait given no positive budget: a wait without a deadline is a watch, and nothing joins it.

View Source
var ErrNotMet = errors.New("eventually: not met within the budget")

ErrNotMet reports a Wait whose budget ran out before match accepted a value; the wrapped message names the last value polled.

Functions

func Await

func Await[Value any](
	reporter IReporter,
	what string,
	budget Budget,
	poll func() Value,
	match func(value Value) bool,
) Value

Await polls until match accepts a value, and returns that value.

what names the subject in the failure message and is required: the alternative sentence is "the predicate never matched", which tells a reader nothing they can act on. Write it as the thing being waited for — "the leader to report an elected term", "the reload to replace the document".

The returned value is the one match accepted, so a caller asserts against what it waited for rather than polling a second time and racing itself.

func Consistently

func Consistently[Value any](
	reporter IReporter,
	what string,
	budget Budget,
	poll func() Value,
	match func(value Value) bool,
) Value

Consistently polls for the whole budget and fails the first time match rejects a value, returning the last value polled.

This is the assertion for an absence — no violation was raised, no goroutine appeared, the queue never grew — and it is what a bare sleep followed by one read was reaching for. The sleep samples that claim once, at the least informative moment; this samples it throughout and names the value that broke it.

func Wait added in v0.3.0

func Wait[Value any](
	what string,
	budget Budget,
	poll func() Value,
	match func(value Value) bool,
) (Value, error)

Wait is Until on the host's clock with no context, for a caller that has neither: it returns the value match accepted, or ErrNotMet with the last value polled when the budget runs out.

Types

type Budget

type Budget struct {
	// Within is the whole wall clock the await may spend. It must be
	// positive; an await with no budget is not an await.
	Within time.Duration

	// Interval is how often poll is called. Zero means [DefaultInterval].
	Interval time.Duration
}

Budget is how long an await is willing to wait and how often it looks.

Declare one as a named variable beside the specifications that use it — readyBudget, convergeBudget — rather than writing the durations inline. A budget with a name is a claim somebody can argue with; a duration buried in an argument list is a number nobody revisits until it flakes.

type IClock added in v0.3.0

type IClock interface {
	Now() time.Time
	After(d time.Duration) <-chan time.Time
}

IClock is the time a wait outside a test reads and waits on: the current instant, and a channel that delivers once a duration has elapsed. The clock capability's system and manual clocks both satisfy it, so a binary grants the host's clock and a spec moves time itself.

type IReporter

type IReporter interface {
	Helper()
	Fatalf(format string, arguments ...any)
}

IReporter is the part of a test's own handle that an await needs: a helper marker, so a failure is reported at the call site rather than inside this package, and one fatal failure.

It is declared here rather than the signatures taking testing.TB, for two reasons. The narrow one is that testing.TB carries an unexported method and cannot be implemented outside the standard library, so this package's own specifications could not drive the failing path at all. The wider one is that these two methods are exactly what the polling engine underneath requires, so this is the contract rather than a subset of somebody else's.

*testing.T, *testing.B, and Ginkgo's GinkgoTB() all satisfy it.

type Outcome added in v0.3.0

type Outcome[Value any] struct {
	// Met is whether match accepted a value before the budget ran out.
	Met bool
	// Final is whether the wait ended because the value can no longer be
	// accepted, as final judged it, rather than on the deadline.
	Final bool
	// Elapsed is the clock's time from the first poll to the last.
	Elapsed time.Duration
	// Polls counts the calls to poll.
	Polls int
	// Last is the last value poll returned: the accepted one when Met.
	Last Value
}

Outcome is what a wait outside a test observed.

func Until added in v0.3.0

func Until[Value any](
	ctx context.Context,
	clock IClock,
	budget Budget,
	poll func(ctx context.Context) Value,
	match func(value Value) bool,
	final func(value Value) bool,
) (Outcome[Value], error)

Until is Await for code that is not a test: it polls until match accepts a value, final says no value ever will, the budget runs out on clock, or ctx ends, and reports what it saw rather than failing a test. The deadline is read on clock, so a spec that grants a manual clock decides every instant; poll itself is bounded by ctx, which the caller sizes to the budget. A nil final never ends a wait early.

It returns an error only for a budget that is not positive or for ctx ending first, with the outcome so far.

Jump to

Keyboard shortcuts

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