Documentation
¶
Overview ¶
Package seamster provides three test-only instrumentation seams that a white-box test uses to exercise a host's recovery and race paths deterministically, without forging state or hammering on timing. All are inert unless the host constructs the Seamster enabled:
- Fault injection makes a site MISBEHAVE: a test arms a named fault, the host consults it at the exact point it affects, and simulates an error, drop, or stale write that is otherwise hard to trigger on demand.
- Execution checkpoints make a site OBSERVABLE and PAUSABLE: a test rendezvouses with the host's progress (Seamster.Waiter / Seamster.Wait / Seamster.WaitTimeout), freezes the host at an exact point (Seamster.Break / Seamster.Resume), or counts arrivals (Seamster.Visits).
- Recorded variables make a host-side VALUE readable: the host records what it has computed (Seamster.Variable) and a test reads the last recording (Seamster.Capture) - for a value a test cannot reach otherwise, such as one the host computes and consumes without ever storing it.
A "seam" is a place where a test can alter or observe behavior without editing the code in place; a Seamster is the object that works a host's seams. The host embeds one Seamster, plants consult sites (Seamster.IsFault / Seamster.Checkpoint / Seamster.Variable) at the points they affect, and names the valid fault/checkpoint set next to those sites so a test cannot arm a fault or checkpoint no site consumes.
Names ¶
Every seam is addressed by a plain string name. A consult and the arming aimed at it meet on that string and nothing else. A name identifying one entity - a particular task, URL, or shard, so a test targets it rather than the next thing that happens - is best built by a function defined once and called at both ends, so the two cannot drift apart:
func taskFault(task string) string { return "task:" + task }
Production inertness is by construction: every consult short-circuits on the enabled bool the host passes to New (typically testing.Testing()), so a disabled Seamster pays a single lock-free bool read per site and no seam can ever fire. What the host must watch is the step BEFORE the gate, which is the caller's own work: a constant name costs nothing, but a name assembled at the call site is assembled on every pass whether or not the seams are live, as is the value handed to Seamster.Variable. Wrap those sites in an Seamster.Enabled check.
The package imports only the standard library, so a host may embed it on a production hot path without dragging in test-only dependencies.
Index ¶
- type Seamster
- func (s *Seamster) Break(checkpointName string)
- func (s *Seamster) Capture(variableName string) (any, bool)
- func (s *Seamster) Checkpoint(ctx context.Context, checkpointName string)
- func (s *Seamster) Enabled() bool
- func (s *Seamster) Inject(faultName string)
- func (s *Seamster) InjectN(faultName string, n int)
- func (s *Seamster) IsFault(faultName string) bool
- func (s *Seamster) Resume(checkpointName string)
- func (s *Seamster) Variable(variableName string, value any)
- func (s *Seamster) Visits(checkpointName string) int
- func (s *Seamster) Wait(ctx context.Context, checkpointName string) bool
- func (s *Seamster) WaitTimeout(ctx context.Context, checkpointName string, timeout time.Duration) bool
- func (s *Seamster) Waiter(checkpointName string) <-chan struct{}
- func (s *Seamster) Withdraw(faultName string)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Seamster ¶
type Seamster struct {
// contains filtered or unexported fields
}
Seamster works one host's instrumentation seams. It is safe for concurrent use. The zero value is a disabled Seamster; construct one with New. Embed one Seamster per host instance rather than sharing a package global, so distinct hosts arm and consult independently.
func New ¶
New returns a Seamster. When enabled is false every consult is inert, so a host passes its own under-test signal - most commonly testing.Testing() - and embeds the result unconditionally.
func (*Seamster) Capture ¶ added in v0.4.0
It has no rendezvous of its own: it reads whatever has been recorded by the moment it runs, which is all most reads need, since the host has finished the work under test by the time the test looks. Where a read must be ORDERED against the host - to catch an intermediate value a later pass would overwrite - plant a Seamster.Checkpoint beside the recording site and drive it with the checkpoint seam: Seamster.Break there, capture while the host is held, then Seamster.Resume.
The caller asserts the type, and should do so WITHOUT the comma-ok:
v, ok := h.seams.Capture("chosenBand")
if !ok {
t.Fatal("host never recorded chosenBand")
}
band := v.(int)
A bare assertion panics if the host's type drifts, which fails the test loudly. Discarding the assertion's ok instead yields a zero value that the test goes on to assert against, and it passes or fails for a reason that has nothing to do with what it was written to check.
func (*Seamster) Checkpoint ¶
Checkpoint is the host-side consult at an instrumented site, and the only one a host calls. It makes the site OBSERVABLE and PAUSABLE: a test rendezvouses with the host's arrival (Seamster.Waiter / Seamster.Wait / Seamster.WaitTimeout), freezes the host there (Seamster.Break / Seamster.Resume), or counts arrivals (Seamster.Visits).
Free in production: it returns on the enabled bool read, before any lock. When enabled it counts the visit, wakes any waiters, and - if a breakpoint is armed - blocks until Seamster.Resume, or until ctx is done, so a stuck test or a shutdown can never wedge the host goroutine forever.
Distinct names are DIFFERENT checkpoints: firing one does not wake a waiter armed on another, and each keeps its own visit count. A host wanting both a fleet-wide and a per-entity granularity fires both.
func (*Seamster) Enabled ¶
Enabled reports whether the seams are live, so a host can skip work that would only feed a consult and would otherwise be paid for in production. Anything the caller builds before the call is such work: a computed name (a concatenation, a closure, a struct boxed into the name parameter), a state read that only the name or the fault decision needs, and the value handed to Seamster.Variable. A constant string name needs no guard.
if h.seams.Enabled() {
h.seams.Checkpoint(ctx, shardKey{h.shard})
}
func (*Seamster) Inject ¶
Inject arms the named fault to fire once (additive: calling twice fires twice).
func (*Seamster) InjectN ¶
InjectN arms the named fault to fire n more times, added to any current count. A non-positive n is a no-op.
func (*Seamster) IsFault ¶
IsFault reports whether the named fault is armed, consuming one fire, and makes the site around it MISBEHAVE - simulating an error, drop, or stale write that is otherwise hard to trigger on demand. It is the only consult entry point and is free in production: it returns on the enabled bool read before any lock.
A fault name usually carries the entity it targets, so a test aims at one task, URL, or shard rather than the next thing that happens. The consult and the arming must produce the same string, which is why a composite name is best built by a shared function: a site consulting taskFault(taskName) is armed by Seamster.Inject with taskFault("Charge").
To keep a fault armed for the rest of a test (until Seamster.Withdraw), inject a large count, e.g. s.InjectN(name, math.MaxInt).
func (*Seamster) Variable ¶ added in v0.4.0
Variable records value as the current value of the named host-side variable, making it readable by a test through Seamster.Capture. It is for a value a test cannot reach any other way - one the host computes and consumes without storing it, one written inside a transaction that then rolls back, one overwritten before the test can look. A value the host already exposes, as a return, a field, or a persisted row, is better read there. Recording is inert on a disabled Seamster.
Recording OVERWRITES: a name holds the LAST value recorded under it, never a history, so a site on a hot path may record on every pass at a cost bounded by the number of distinct names rather than by how often it runs.
Two rules govern the call sites.
Wrap every call in an Seamster.Enabled check. This consult always takes a value, and converting it to the any parameter happens at the CALL SITE, before the enabled gate can short-circuit - so an unguarded call costs a production allocation on every pass, whatever the name costs.
if h.seams.Enabled() {
h.seams.Variable("chosenBand", band)
}
Record a value, never a reference to live state. A recorded pointer, slice, or map leaves the host free to go on mutating what it points at while a test reads it from another goroutine - a data race no guard here can cover. Record a scalar, or snapshot first.
func (*Seamster) WaitTimeout ¶ added in v0.3.1
func (s *Seamster) WaitTimeout(ctx context.Context, checkpointName string, timeout time.Duration) bool
WaitTimeout is Seamster.Wait bounded by a duration - the usual shape in a test, where the bound is "this long" rather than a deadline inherited from elsewhere. ctx still aborts, so a cancelled parent ends it early.