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
- func Contract(t testing.TB, m Main, verbs []string)
- func DryRunAgrees(t *testing.T, run func(args ...string) Result, cases []DryCase)
- func JSON[T any](t testing.TB, raw string) T
- func ReadFile(t testing.TB, path string) string
- func ReadJSON[T any](t testing.TB, path string) T
- func Refusals(t testing.TB, m Main, rows []Refusal)
- func SkipOn(t testing.TB, goos, why string)
- func Snapshot(t testing.TB, root string) map[string]string
- func Tree(t testing.TB, root string, files map[string]string) string
- func Unwritable(t *testing.T, dir string)
- func WriteFile(t testing.TB, path, body string)
- type Call
- type DryCase
- type GitRig
- type Main
- func (m Main) Do(t testing.TB, args ...string) Ran
- func (m Main) DoIn(t testing.TB, stdin string, args ...string) Ran
- func (m Main) NoStdin() func(args []string, stdout, stderr io.Writer) int
- func (m Main) OK(t testing.TB, args ...string) Result
- func (m Main) OKIn(t testing.TB, stdin string, args ...string) Result
- func (m Main) Run(args ...string) Result
- func (m Main) RunIn(stdin string, args ...string) Result
- type Ran
- type Refusal
- type Result
- type Script
- type Waits
Constants ¶
const ( NoSuchFlag = "--no-such-flag-breadcrumb" NoSuchFlag2 = "--no-such-flag-either" )
The probes' names: nothing defines them, so every tool refuses them.
Variables ¶
This section is empty.
Functions ¶
func Contract ¶
Contract runs the skeleton contract's probes on a tool (docs/STANDARD.md, the skeleton contract's "How a port is proved"): the bare command refuses in one line naming its door at exit 2; an unknown verb refuses naming the nearest; an unknown flag on each verb refuses naming it; two bad flags in one run are both named; --json on a refusal is one object on stdout at the same exit; <verb> -h answers on stdout at exit 0 with nothing on stderr; and a refusal's remedy run back through the tool does not refuse for the same reason. Every breach is reported with assert, naming the probe, so one call holds a tool to the whole shape.
verbs are the tool's verbs, each as its own words ("send", "fn load"). Every verb named must take --json and answer -h: Contract holds the verbs the caller lists, and a verb that prints its own output (tool.Flags.Prints) or refuses help (tool.HelpRefused) is left out by its tool's own contract test.
func DryRunAgrees ¶
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 JSON ¶
JSON decodes raw (a tool's --json output, a stored record) into a T, the three lines of a declared value, json.Unmarshal and its require as one expression, failing the test with the text when it does not decode.
func ReadJSON ¶
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 ¶
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 SkipOn ¶
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 ¶
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 ¶
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 Unwritable ¶
Unwritable takes the write permission off dir for the rest of the test.
Types ¶
type Call ¶
Call is one expected call of a Script: the transport method's name, its arguments, and the result and error the method returns.
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 ¶
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 ¶
Commit writes files (a path under the clone to its body) into the clone and commits them under the rig's fixed identity.
type Main ¶
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 ¶
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) NoStdin ¶
NoStdin is the entry point in the shape that reads no stdin, as testverbhelp.Run takes it.
func (Main) OK ¶
OK is Run that fails the test unless the exit code is 0, naming the arguments and both streams.
type Ran ¶
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).
type Refusal ¶
Refusal is one row of a tool's refusal table: the arguments, the exit code wanted, and what the refusal line must say.
type Script ¶
type Script struct {
// contains filtered or unexported fields
}
Script is a strict fake for a transport interface: the calls a test declares, consulted in order as the code under test makes them. An unexpected call, a call out of order, or a declared call the run never made fails the test naming it (docs/STANDARD.md section 8: "A fake is strict like the real tool: it refuses what the real one refuses"). Every call is declared: there is no matcher and no wildcard.
A package wraps a Script for its own transport interface: its fake holds the Script and each interface method passes its own name and arguments to Called, then returns the declared result and error. The example is TestScriptAnswersDeclaredCallsInOrder in script_test.go:
type fakeStore struct{ *testkit.Script }
func (f fakeStore) Get(key string) (string, error) {
got, err := f.Called("Get", key)
if err != nil {
return "", err
}
return got.(string), nil
}
s := testkit.NewScript(t, testkit.Call{Name: "Get", Args: []any{"k"}, Result: "v"})
func NewScript ¶
NewScript declares the calls a fake may receive, in order, and arms the leftover check at cleanup: every declared call the run never makes fails t then, naming it (the AssertExpectations step of the transport design). A caller that wants the check earlier calls AssertExpectations itself.
func (*Script) AssertExpectations ¶
AssertExpectations fails t for every declared call the run never made, naming them in order. NewScript arms it at cleanup, so a test names its calls and does not call this itself; a helper that has to check before cleanup calls it.
func (*Script) Called ¶
Called is one call the fake received. It checks the call against the next declared one and fails t naming both when they differ, so a wrong call is never answered: the declared result and error are returned only for the declared call. A wrapper passes the transport method's name and arguments, and reads the two values at the arity its method returns.
type Waits ¶
type Waits struct {
// contains filtered or unexported fields
}
Waits is a fake for a wait seam of the shape func(stop <-chan struct{}, d time.Duration) bool: "true once d has passed, false when stop is observed closed" (delayproxy.Clock.Wait). It is for code whose tests cannot run in a testing/synctest bubble because the code blocks on real I/O (a loopback socket), and it is not a clock: it records every d asked for and returns true at once, unless the wait is held.
A held wait is the test's hand inside the code: the code stops in its wait, the test looks at what it has done so far (Holding tells it when), and then releases the waits (true) or closes stop (false). An already-closed stop makes even an unheld wait return false. If stop and release are both ready when a held wait chooses, stop wins; Wait does not infer which channel was closed first. What a held wait waits for is the test, never the time, so the first of two waits of the same length can be held while the second goes through, which no clock can do.
func (*Waits) Hold ¶
Hold makes the first n waits held (every wait when n is negative) until Release or their stop. It is set before the code runs, and returns w.