testsweep

package
v0.172.11 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 1 Imported by: 0

Documentation

Overview

Package testsweep is spec/plans/coverage-to-100/README.md task-8's generic fail-call-N sweep (decision 23): given a happy-path test body, it counts the external calls the body makes, reruns the body once per call with that call failing, and hands each run's outcome to the test to assert -- so one sweep call covers every error return a body reaches through a sequence of calls, instead of one hand-written failure test per call.

Sweep depends on nothing from internal/runner/runnertest or internal/gitcli: it is written against the two-method Failer interface alone, so any scriptable fake that counts its calls and can be told to fail one of them plugs in the same way. internal/runner/runnertest.Fake implements it today; task-9's file-write injector is designed to grow the same two methods next. See the package example for a fake that has nothing to do with either of those, to prove the point.

Usage

Write the fake's happy path once, as a body that takes the fake and returns an error:

body := func(fake *runnertest.Fake) error {
	fake.ExpectArgv([]string{"git", "fetch", "--quiet", "origin"}, runner.Result{}, nil)
	return gitcli.New(fake).Fetch(context.Background(), "/repo", "origin")
}

Then hand Sweep a constructor for a fresh fake, the error to inject, the body, and a check that runs once per call number:

testsweep.Sweep(t, func() *runnertest.Fake { return runnertest.New(t) }, errBoom,
	body,
	func(t testing.TB, callNum, total int, err error) {
		if !errors.Is(err, errBoom) {
			t.Fatalf("call %d/%d: err = %v, want it to wrap errBoom", callNum, total, err)
		}
	})

Sweep calls the constructor once to run body with no injected failure (the dry run), to learn how many calls a happy path makes. It then calls the constructor once per call number from 1 to that count, arranges (through Failer.FailCall) for that one call to fail, reruns body, and passes the call number, the dry run's total, and body's returned error to check.

Example

Example demonstrates Sweep against stepFake, a fake with no relationship to internal/runner/runnertest or internal/gitcli, to show that Sweep's core is generic: task-9's file-write injector can plug in the same way, by growing the same two methods.

package main

import (
	"errors"
	"fmt"
	"runtime"
	"testing"

	"github.com/sneat-dev/wb/internal/testsweep"
)

// stepFake is a tiny, standalone Failer: it has nothing to do with
// internal/runner/runnertest or internal/gitcli, and exists only to prove
// Sweep depends on neither -- exactly the decoupling task-9's file-write
// injector needs next. Each call to step records one "step" name; FailCall
// makes one chosen call fail instead of recording success.
type stepFake struct {
	failAt  int
	failErr error
	steps   []string
}

func (f *stepFake) CallCount() int { return len(f.steps) }

func (f *stepFake) FailCall(callNum int, err error) {
	f.failAt = callNum
	f.failErr = err
}

// step is stepFake's one operation: the "external call" Sweep counts and
// can make fail.
func (f *stepFake) step(name string) error {
	f.steps = append(f.steps, name)
	if f.failAt != 0 && len(f.steps) == f.failAt {
		return f.failErr
	}
	return nil
}

// threeStepBody is a happy-path body that makes three of stepFake's calls
// in a fixed order, stopping at the first error -- exactly the shape Sweep
// expects a well-behaved body to have.
func threeStepBody(f *stepFake) error {
	if err := f.step("open"); err != nil {
		return err
	}
	if err := f.step("write"); err != nil {
		return err
	}
	if err := f.step("close"); err != nil {
		return err
	}
	return nil
}

func main() {
	errBoom := errors.New("boom")

	testsweep.Sweep(&recordingTB{}, func() *stepFake { return &stepFake{} }, errBoom,
		threeStepBody,
		func(_ testing.TB, callNum, total int, err error) {
			fmt.Printf("call %d/%d failed: %v\n", callNum, total, errors.Is(err, errBoom))
		})
}

// recordingTB is a minimal testing.TB that records whether Fatalf ran and
// mimics *testing.T.Fatalf's runtime.Goexit, instead of the embedded nil
// testing.TB panicking, so a test can assert Sweep detected a bad body
// without a real *testing.T ending the outer test itself.
type recordingTB struct {
	testing.TB
	fataled bool
}

func (r *recordingTB) Helper() {}

func (r *recordingTB) Fatalf(string, ...any) {
	r.fataled = true
	runtime.Goexit()
}
Output:
call 1/3 failed: true
call 2/3 failed: true
call 3/3 failed: true

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Sweep

func Sweep[F Failer](t testing.TB, newFake func() F, failErr error, body func(F) error, check func(t testing.TB, callNum, total int, err error))

Sweep drives one happy-path body once per external call it makes.

It first calls newFake to build a fresh Failer and runs body against it with no injected failure -- the dry run -- to count body's calls. A dry run that returns a non-nil error, or makes no calls at all, is not a happy path, and Sweep fails t rather than guessing what to sweep.

Then, for each call number from 1 to the dry run's count, Sweep builds another fresh Failer, arranges (via Failer.FailCall) for that call to fail with failErr, runs body again, and passes the call number, the dry run's total, and body's returned error to check.

Sweep assumes a well-behaved body stops making calls as soon as one fails: it fails t directly, rather than passing a misleading result to check, when a faulty run either returns a nil error despite the injected failure, or has answered a different number of calls than the call number it was told to fail once it returns -- a body that swallows the error and keeps going, retries, or otherwise makes a non-deterministic number of calls before stopping.

Types

type Failer

type Failer interface {
	// CallCount reports how many calls have been answered so far.
	CallCount() int
	// FailCall arranges for the callNum'th call (1-indexed) this Failer
	// answers to fail with err instead of succeeding. Every other call
	// keeps returning its own scripted result -- FailCall affects exactly
	// one call number. It must be set before the call it targets is made.
	FailCall(callNum int, err error)
}

Failer is implemented by a scriptable fake with a fail-call-N mode: it counts each external call it answers, in the order it answers them, and can be told, before a run, to make exactly one of those calls fail instead of returning its normal result.

Jump to

Keyboard shortcuts

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