seamster

package module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: Apache-2.0 Imports: 4 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 two 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.

Both 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 neither 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, scoped to one task.
if h.seams.IsFault("transitionCommit", taskName) {
    return errors.New("injected fault")
}

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

Test side:

// Fault injection: arm a scoped fault to fire once (scope args match IsFault's).
h.seams.Inject("transitionCommit", "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"))
}

API

Method Side Purpose
New(enabled bool) host Construct a Seamster; inert when enabled is false.
Enabled() bool host Skip building a scoped consult that would only feed IsFault.
IsFault(faultName, scope...) bool host Consult a fault, consuming one fire.
Inject(faultName, scope...) test Arm a fault once (additive); scope args match IsFault.
InjectN(n, faultName, scope...) test Arm a fault n times (additive); scope args match IsFault.
Withdraw(faultName, scope...) 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).

Scoping: a fault is usually scoped so a test targets one entity rather than "the next thing that happens." The consult passes the scope to IsFault; the test arms the fault with the same scope args (Inject/InjectN/ Withdraw all join scope the same way), so it is spelled one way at both ends. Unscoped (process-wide) faults take no scope.

Documentation

Overview

Package seamster provides two 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. Both 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).

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) 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. Both seams take an optional trailing scope, spelled the same way at the consult and at the arming, so a test targets one entity rather than the next thing that happens.

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 neither seam can ever fire. 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, scope ...string)

Break arms a breakpoint so the host blocks the next time it reaches the named checkpoint, until Seamster.Resume releases it. It holds every goroutine that reaches the checkpoint while armed, not only the first, so a fan-out can be gathered at one point and released together by a single Resume; a rendezvous returns as soon as the first of them arrives.

func (*Seamster) Checkpoint

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

Checkpoint is the host-side consult at an instrumented site, and the only one a host calls. Free in production: it returns on the enabled bool read, before any lock or key build. 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.

func (*Seamster) Enabled

func (s *Seamster) Enabled() bool

Enabled reports whether the seams are live. A host uses it to skip building a scoped consult (a state read, a key concatenation) that would only feed IsFault, so the setup work is also elided in production.

func (*Seamster) Inject

func (s *Seamster) Inject(faultName string, scope ...string)

Inject arms the named fault to fire once (additive: calling twice fires twice). Optional scope args target it, joined the same way Seamster.IsFault joins its scope.

func (*Seamster) InjectN

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

InjectN arms the named fault to fire n more times, added to any current count. Optional scope args target it, joined the same way Seamster.IsFault joins its scope. A non-positive n is a no-op.

func (*Seamster) IsFault

func (s *Seamster) IsFault(faultName string, scope ...string) bool

IsFault reports whether the named fault is armed, consuming one fire. It is the only consult entry point and is free in production: it returns on the enabled bool read before any lock or key build. Optional scope args target the fault (a task name, a URL, ...) and are joined into the key, but only once the enabled gate has passed, so a scoped consult on a disabled Seamster allocates no throwaway key. To keep a fault armed for the rest of a test (until Seamster.Withdraw), inject a large count, e.g. s.InjectN(math.MaxInt, name).

func (*Seamster) Resume

func (s *Seamster) Resume(checkpointName string, scope ...string)

Resume releases the host frozen at the named breakpoint and disarms it. A no-op if none is armed.

func (*Seamster) Visits added in v0.2.0

func (s *Seamster) Visits(checkpointName string, scope ...string) int

Visits reports how many times the host has passed the named checkpoint, counting a call that then blocked on a breakpoint (reaching the checkpoint is the visit; blocking comes after). It never blocks, consumes nothing, and can be read repeatedly. The count is monotonic for the Seamster's lifetime, so capture a baseline first to assert on visits accrued within a window. Zero for a checkpoint never reached, and always zero on a disabled Seamster.

func (*Seamster) Wait

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

Wait blocks until the host reaches the named checkpoint and reports whether it did, abandoning the wait if ctx is done. A false return is the caller's cue to fail:

assert.True(s.Wait(ctx, "beforeCommit"))

Returns true immediately on a disabled Seamster, or if a breakpoint is already holding the host at name.

Use it where the host is ALREADY running into the checkpoint: frozen at a breakpoint, or driven from another goroutine. If the caller's own next statement is the trigger, this cannot arm in time - use Seamster.Waiter.

func (*Seamster) WaitTimeout added in v0.3.1

func (s *Seamster) WaitTimeout(ctx context.Context, timeout time.Duration, checkpointName string, scope ...string) 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, scope ...string) <-chan struct{}

Waiter registers a waiter for the named checkpoint and returns a channel closed when the host reaches it. It does not block: arming and receiving are separate steps, which is the whole point. Arm before the operation that triggers the checkpoint, receive after it:

reached := s.Waiter("beforeCommit")
host.DoTheThing()
<-reached

Use it whenever the caller's OWN next statement drives the host to the checkpoint: Seamster.Wait and Seamster.WaitTimeout can only arm once that statement is already running, so an arrival in between is lost.

The channel is already closed if a breakpoint is currently holding the host at name, and on a disabled Seamster - so a receive never blocks in production.

func (*Seamster) Withdraw

func (s *Seamster) Withdraw(faultName string, scope ...string)

Withdraw disarms the named fault regardless of its remaining count. Optional scope args target it, joined the same way Seamster.IsFault joins its scope.

Jump to

Keyboard shortcuts

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