seamster

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

README

Seamster

Seamster works the test-only instrumentation seams of a host package: named points planted in the host's code that a white-box test hooks into to drive failure and timing paths deterministically. 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 them.

It has three halves:

  • 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, freezes the host at an exact point (so a concurrent operation can be driven into a precise window with no timing hammer), or simply counts how many times the host has passed the point.
  • Recorded variables make a host-side value readable. The host records what it has computed; a test reads the last recording — for a value a test cannot reach otherwise, such as one the host computes and consumes without ever storing it.

All are inert in production 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. The package imports only the standard library, so a host may embed it on a production hot path without pulling in test-only dependencies.

Usage

The host embeds one Seamster per instance, plants consult sites where they affect, and names the valid fault/checkpoint set next to those sites.

type Host struct {
    // ...
    seams *seamster.Seamster
}

func NewHost() *Host {
    return &Host{seams: seamster.New(testing.Testing())}
}

Production consult sites (free when disabled):

// Make this write misbehave when a test arms the fault, targeting one task.
if h.seams.IsFault(taskFault(taskName)) {
    return errors.New("injected fault")
}

// Mark this point observable/pausable.
h.seams.Checkpoint(ctx, "beforeTransitionTx")

// Publish a value a test cannot otherwise see. Guarded, because the value is boxed at the call site,
// before the enabled gate is read.
if h.seams.Enabled() {
    h.seams.Variable("chosenBand", band)
}

Test side:

// Fault injection: arm a fault to fire once, naming the same entity the consult names.
h.seams.Inject(taskFault("Charge"))

// Rendezvous: arm BEFORE the operation, receive after it, so the checkpoint cannot fire in the gap.
reached := h.seams.Waiter("beforeTransitionTx")
h.Process(ctx)
<-reached

// Freeze: drive a concurrent op into a precise window.
h.seams.Break("beforeTransitionTx")
go h.Process(ctx)                                  // runs into the breakpoint and blocks
h.seams.Wait(ctx, "beforeTransitionTx")            // returns once frozen
// ... run the racing operation while the host is held ...
h.seams.Resume("beforeTransitionTx")

// Count: assert how many times the host passed a checkpoint.
if h.seams.Visits("beforeTransitionTx") != 3 {
    t.Fatalf("expected 3 attempts, got %d", h.seams.Visits("beforeTransitionTx"))
}

// Capture: read the last value the host recorded. Assert the type WITHOUT the comma-ok, so a drift in
// the host's type fails loudly instead of yielding a zero value the test goes on to assert against.
v, ok := h.seams.Capture("chosenBand")
if !ok {
    t.Fatal("host never recorded chosenBand")
}
band := v.(int)

API

Method Side Purpose
New(enabled bool) host Construct a Seamster; inert when enabled is false.
Enabled() bool host Skip building anything that only a consult would use.
IsFault(faultName) bool host Consult a fault, consuming one fire.
Inject(faultName) test Arm a fault once (additive).
InjectN(faultName, n) test Arm a fault n times (additive).
Withdraw(faultName) test Disarm a fault.
Checkpoint(ctx, checkpointName) host Pass through a checkpoint; counts the visit, wakes waiters, honors a breakpoint.
Waiter(checkpointName) <-chan struct{} test Arm a waiter for the host's next arrival; the channel closes when it arrives. Does not block - arm before the triggering operation, receive after it.
Wait(ctx, checkpointName) bool test Arm and block until the host arrives; reports whether it did, aborting on ctx. Only where the host is already running into the checkpoint.
WaitTimeout(ctx, checkpointName, timeout) bool test Wait bounded by a duration - the common shape in a test.
Break(checkpointName) test Freeze the host at the checkpoint.
Resume(checkpointName) test Unfreeze the host at the checkpoint.
Visits(checkpointName) int test Count how many times the host has passed the checkpoint (monotonic; never blocks).
Variable(variableName, value) host Record the current value of a host-side variable, replacing the previous recording. Wrap in an Enabled() check.
Capture(variableName) (any, bool) test Read the last recorded value, and whether one was ever recorded. Never blocks.

Names

Every name above is a plain string, and a consult and the arming aimed at it meet on that string and nothing else. A name identifying one entity, so a test targets it rather than "the next thing that happens," is best built by a function the host writes once and calls at both ends, so the two cannot drift apart:

func taskFault(task string) string { return "task:" + task }

// consult:  h.seams.IsFault(taskFault(taskName))
// arming:   h.seams.Inject(taskFault("Charge"))

Distinct names are distinct keys, with no cascade: firing stopName("flow-1") does not wake a waiter armed on "stopped", and each keeps its own visit count. A site wanting both granularities fires both.

On a hot path, mind what the name costs to assemble, since the caller assembles it before the gate is reached — it is paid whether or not the seams are live. A constant name is free. An assembled one costs one allocation, inline or through a helper. Variable costs one even with a constant name, because its value is boxed — which is why every Variable call is wrapped. if h.seams.Enabled() { ... } around the call elides all of it.

Recording overwrites: a variable 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. Capture has no rendezvous of its own — it reads whatever has been recorded by the moment it runs, which is all most reads need. Where a read must be ordered against the host, plant a checkpoint beside the recording site and compose: freeze, capture, resume.

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

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

func New(enabled bool) *Seamster

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) Break

func (s *Seamster) Break(checkpointName string)

func (*Seamster) Capture added in v0.4.0

func (s *Seamster) Capture(variableName string) (any, bool)

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

func (s *Seamster) Checkpoint(ctx context.Context, checkpointName string)

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

func (s *Seamster) Enabled() bool

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

func (s *Seamster) Inject(faultName string)

Inject arms the named fault to fire once (additive: calling twice fires twice).

func (*Seamster) InjectN

func (s *Seamster) InjectN(faultName string, n int)

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

func (s *Seamster) IsFault(faultName string) bool

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) Resume

func (s *Seamster) Resume(checkpointName string)

func (*Seamster) Variable added in v0.4.0

func (s *Seamster) Variable(variableName string, value any)

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) Visits added in v0.2.0

func (s *Seamster) Visits(checkpointName string) int

func (*Seamster) Wait

func (s *Seamster) Wait(ctx context.Context, checkpointName string) bool

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.

func (*Seamster) Waiter added in v0.3.1

func (s *Seamster) Waiter(checkpointName string) <-chan struct{}

func (*Seamster) Withdraw

func (s *Seamster) Withdraw(faultName string)

Withdraw disarms the named fault regardless of its remaining count.

Jump to

Keyboard shortcuts

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