testkit

package
v1.4.1 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package testkit holds the test rigs that were copied from package to package: running a tool's entry point in process with both streams captured and checked (verb.go), writing and reading the files a test sets up (here and tree.go), a recording wait for code a synctest bubble cannot hold (waits.go) and a skip by platform (skip.go). Each helper fails the test through testify's require, so a caller's setup is one line. Time in a test is testing/synctest's first, a clockwork.FakeClock where code does real I/O, and never a clock of the kit's own.

A tool's tests keep one adapter of their own, the entry point as a Main, and call its methods. A tool whose entry point takes a clock, an environment or an app closes over it in that one line:

var cairn = testkit.Main(run)
var bus = testkit.Main(func(a []string, in io.Reader, o, e io.Writer) int { return run(a, in, o, e, now) })
out := cairn.OK(t, "open", "--store", dir).Stdout
r := cairn.Run("open") // r.Code, r.Stdout, r.Stderr
cairn.Do(t, "open").Exit(2).Refused("--store is required")

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CopyTree added in v1.2.14

func CopyTree(t testing.TB, src, dst string)

CopyTree copies the directory tree at src into dst, creating dst, and fails the test if any file cannot be copied. It is os.CopyFS over src as a file system, so the recursive copies the command tests each hand-rolled become one call; execute bits survive, other permission bits do not (os.CopyFS). dst must not already hold a name the tree carries: os.CopyFS refuses to overwrite, which is what a test wants a copy to say.

func DryRunAgrees

func DryRunAgrees(t *testing.T, run func(args ...string) Result, cases []DryCase)

DryRunAgrees holds a verb's --dry-run to its real run: on each case the dry run exits as the real run does, with the same status words opening its first line (a refusal refuses, a failure fails), and leaves the tree under its root byte-identical. The two runs get identical fresh trees, so the real run's writes cannot change what the dry run sees.

func ExecutableFirstRun added in v1.2.14

func ExecutableFirstRun(t *testing.T, tool string, main MainFunc)

ExecutableFirstRun runs a tool's documented first run in the test's own temporary directory, so the manifest the example lines name is written and read under t.TempDir and the process working directory is never changed: the example block the banner carries is RUN rather than read, and every line of the `### First run` transcript of docs/TESTS.md is compared with what the tool prints, field name by field name (ONBOARDING point 5a and 5c). It is the one body the command packages' TestExecutableFirstRun each keep, and the two call it with their own tool name and entry point.

func FirstRunArgs added in v1.2.14

func FirstRunArgs(args []string, dir string) []string

FirstRunArgs resolves the one relative path a documented first run names (versions.tsv) under dir, the test's own t.TempDir, so the manifest is written and read there instead of in the process working directory.

func PipeStore added in v1.2.14

func PipeStore(t *testing.T) (*miniredis.Miniredis, *redis.Client)

PipeStore is a miniredis fake reached in memory: the client's dialer hands it one end of a net.Pipe and the fake serves the other (ServeConn), so no TCP connection is made. miniredis binds a loopback listener when it starts; the cleanup fails the test when anything dialled it.

func ReadFile

func ReadFile(t testing.TB, path string) string

ReadFile returns the file's contents, failing the test when it cannot be read.

func ReadJSON

func ReadJSON[T any](t testing.TB, path string) T

ReadJSON decodes the JSON file at path into a T, failing the test when the file cannot be read or does not decode.

func Refusals

func Refusals(t testing.TB, m Main, rows []Refusal)

Refusals runs each row and checks it as Exit(row.Code) and Refused(row.Says) do, so a tool's refusal tests are one row each. A row that misses fails the test through assert, naming the run, and the rows after it still run. The rows run one after another: m may close over state its runs share.

func RepoRoot added in v1.2.14

func RepoRoot(t testing.TB) string

RepoRoot is the checkout root: both a command package (cmd/<tool>) and this package (pkg/testkit) sit two directories under it, so it is resolved relative to the test's working directory and checked against a file only the root holds, so that a failure names a path rather than an assumption.

func SkipOn

func SkipOn(t testing.TB, goos, why string)

SkipOn skips the test when it runs on the operating system goos (a runtime.GOOS value), saying why the property cannot be observed there:

testkit.SkipOn(t, "windows", "chmod 0 does not refuse reads")

func Snapshot

func Snapshot(t testing.TB, root string) map[string]string

Snapshot is every path under root with its mode and, for a regular file, its bytes; a link is recorded by its target and not followed. Two snapshots are equal only when nothing under root was written, created or removed.

func Tree

func Tree(t testing.TB, root string, files map[string]string) string

Tree writes each file of files under root, making root and the directories, overwriting a file already there, and returns root. A name is a slash-separated path inside root. Every name is checked before anything is written (filepath.IsLocal: not absolute, never climbing out with ".."), and every write goes through an os.Root opened on root, which refuses to follow a symlink out of it; either refusal fails the test naming the path.

root := testkit.Tree(t, t.TempDir(), map[string]string{"tla/CASES.tsv": plan, "go.mod": "module x\n"})

A copy of a fixture directory needs no helper: require.NoError(t, os.CopyFS(dst, os.DirFS(src))).

func UnitTierDials added in v1.2.14

func UnitTierDials(dir string, traps map[string]bool) ([]string, error)

UnitTierDials lists, as file:line: pkg.Name, every use of a dialer in the test files of dir that the unit tier builds: a file whose build constraint holds with the functional and perf tags off. A miniredis started inside a function traps names is allowed. It backs the no-sockets rule of the unit tier (docs/STANDARD.md section 8).

func Unwritable

func Unwritable(t *testing.T, dir string)

Unwritable takes the write permission off dir for the rest of the test.

func VersionLineShape added in v1.2.14

func VersionLineShape(t *testing.T, tool string, run VersionLineRun)

VersionLineShape asserts that one run of a tool's `version` verb is a single terminated line of four fields -- the tool's name, a build identity, the bench's GOOS/GOARCH, and the bench's Go release -- and wrote nothing to stderr. Asserting only that the output "contains" a version would pass over a line broken in two, which is the failure the escaping in pkg/buildinfo exists to prevent.

func WriteFile

func WriteFile(t testing.TB, path, body string)

WriteFile writes body to path with mode 0o644, making the parent directories first, and fails the test on any error.

Types

type DryCase

type DryCase struct {
	Name     string
	Unwrites bool // the case takes a directory's write permission away (skipped on Windows and as root)
	Setup    func(t *testing.T, root string) (verb, rest []string)
}

DryCase is one bad input for a verb that writes: Setup lays it out under a fresh root and returns the verb's words and the rest of its arguments, so the case runs as `<verb> --dry-run <rest>` and as `<verb> <rest>`.

type GitRig

type GitRig struct {

	// Remote is the bare remote's path; Clones are the clones', in the order made.
	Remote string
	Clones []string
	// contains filtered or unexported fields
}

GitRig is a bare git remote and clones of it, all under the test's own t.TempDir(), for the tests that must run real git (docs/STANDARD.md, section 8: shared rigs live in pkg/testkit, never copies; section 9 rule 10: a test writes only inside its own t.TempDir()). Every command runs with a fixed identity and GIT_CONFIG_GLOBAL pointed at an empty file inside the rig's directory, through the command's environment, never t.Setenv, so the machine's own git identity and config cannot reach the test.

func Git

func Git(t testing.TB, n int) *GitRig

Git builds a rig of one bare remote and n clones of it, failing the test when git is not on PATH or a git command fails. It runs the git exec.LookPath finds on PATH and no other.

func (*GitRig) Commit

func (g *GitRig) Commit(clone string, files map[string]string)

Commit writes files (a path under the clone to its body) into the clone and commits them under the rig's fixed identity.

func (*GitRig) Head

func (g *GitRig) Head(repo string) string

Head returns the commit HEAD names in repo, the remote's or a clone's.

func (*GitRig) Push

func (g *GitRig) Push(clone string)

Push is one git push of the clone's current branch from the clone to the rig's remote, so the remote's branch follows the clone's.

type Main

type Main func(args []string, stdin io.Reader, stdout, stderr io.Writer) int

Main is a tool's entry point in process: the arguments after the tool's name, stdin, the two output streams, and the exit code it returns.

func (Main) Do

func (m Main) Do(t testing.TB, args ...string) Ran

Do runs the entry point with args and an empty stdin. It checks nothing by itself: the checks are the Ran's methods.

func (Main) DoIn

func (m Main) DoIn(t testing.TB, stdin string, args ...string) Ran

DoIn is Do with stdin holding the given text.

func (Main) NoStdin

func (m Main) NoStdin() func(args []string, stdout, stderr io.Writer) int

NoStdin is the entry point in the shape that reads no stdin, as testverbhelp.Run takes it.

func (Main) OK

func (m Main) OK(t testing.TB, args ...string) Result

OK is Run that fails the test unless the exit code is 0, naming the arguments and both streams.

func (Main) OKIn

func (m Main) OKIn(t testing.TB, stdin string, args ...string) Result

OKIn is OK with stdin holding the given text.

func (Main) Run

func (m Main) Run(args ...string) Result

Run calls the entry point with args and an empty stdin. It never fails the test: the caller asserts on the Result.

func (Main) RunIn

func (m Main) RunIn(stdin string, args ...string) Result

RunIn is Run with stdin holding the given text.

type MainFunc added in v1.2.14

type MainFunc func(name string, args []string, stamp string, stdout, stderr io.Writer) int

MainFunc is a tool's entry point in the shape the update/version binaries share: the tool's own name, the arguments after it, the build stamp (empty unless a release stamped one), and the two output streams.

type Ran

type Ran struct {
	Result
	Args []string
	// contains filtered or unexported fields
}

Ran is one run of a Main made inside a test, with its checks as methods. Each check is the testify require call it names, with the Ran as its message, so a failing line prints the arguments run, the exit code and both streams; each returns the Ran, so checks chain:

tool.Do(t, "send").Exit(2).Err("--to is required").NotOut("OK")

The fields are the Result's; a check the methods do not cover is a testify call with the Ran as its message: assert.Empty(t, r.Stdout, r).

func (Ran) Err

func (r Ran) Err(wants ...string) Ran

Err is require.Contains on stderr, for each of wants.

func (Ran) Exit

func (r Ran) Exit(want int) Ran

Exit is require.Equal on the exit code.

func (Ran) NotErr

func (r Ran) NotErr(nots ...string) Ran

NotErr is require.NotContains on stderr, for each of nots.

func (Ran) NotOut

func (r Ran) NotOut(nots ...string) Ran

NotOut is require.NotContains on stdout, for each of nots.

func (Ran) Out

func (r Ran) Out(wants ...string) Ran

Out is require.Contains on stdout, for each of wants.

func (Ran) Refused

func (r Ran) Refused(says string) Ran

Refused fails the test unless the run is a refusal in the house grammar that says says: a non-zero exit, and a stderr line holding says that also carries the status word REFUSED and a remedy (`; run: `). Every clause it misses is named at once.

func (Ran) String

func (r Ran) String() string

String is the run as a failure prints it: the arguments, the exit code and both streams.

type Refusal

type Refusal struct {
	Args []string
	Code int
	Says string
}

Refusal is one row of a tool's refusal table: the arguments, the exit code wanted, and what the refusal line must say.

type Result

type Result struct {
	Code           int
	Stdout, Stderr string
}

Result is one run of a Main: the exit code and everything each stream got.

type VersionLineRun added in v1.2.14

type VersionLineRun func() (code int, stdout, stderr string)

VersionLineRun runs a tool's `version` verb and returns the exit code and both streams, the one parameter the shared shape check cannot know.

Jump to

Keyboard shortcuts

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