Documentation
¶
Overview ¶
Package tuitest is a headless testing harness for terminal programs. It drives a program under test through a real pseudo-terminal, interprets its output with a VT emulator, and lets tests assert on the resulting screen as a grid of cells rather than as a raw byte stream.
The typical flow: Start (or StartT under go test) spawns the program, SendKeys and Type drive input, the WaitFor family synchronizes on screen state without sleeping, and Snapshot / AssertGolden capture the result. Close (registered automatically by StartT) tears down the whole process group.
Index ¶
- Constants
- Variables
- func Diff(want, got string) string
- type Cell
- type ClosedError
- type Color
- type ColorKind
- type ExitStatus
- type Key
- type KeyMods
- type MouseAction
- type MouseButton
- type MouseEncoding
- type MouseEvent
- type Option
- func WithDir(path string) Option
- func WithEnv(kv ...string) Option
- func WithInheritEnv() Option
- func WithLog(w io.Writer) Option
- func WithOutputMirror(w io.Writer) Option
- func WithSemanticMarkers() Option
- func WithSize(cols, rows int) Option
- func WithStabilizeInterval(d time.Duration) Option
- func WithTerm(term string) Option
- func WithTrueColor() Option
- type Scope
- type Screen
- type TermState
- type Terminal
- func (t *Terminal) AssertGolden(tb testing.TB, name string)
- func (t *Terminal) AssertGoldenStyled(tb testing.TB, name string)
- func (t *Terminal) Close() error
- func (t *Terminal) Done() <-chan struct{}
- func (t *Terminal) ExitCode() (int, bool)
- func (t *Terminal) ExitStatus() (ExitStatus, bool)
- func (t *Terminal) LastCommandExit() (int, bool)
- func (t *Terminal) Paste(s string) error
- func (t *Terminal) Pid() int
- func (t *Terminal) Progress() (bytes int64, last time.Time)
- func (t *Terminal) Resize(cols, rows int) error
- func (t *Terminal) Screen() Screen
- func (t *Terminal) SendKeys(items ...any) error
- func (t *Terminal) SendMouse(ev MouseEvent) error
- func (t *Terminal) Snapshot() string
- func (t *Terminal) SnapshotStyled() string
- func (t *Terminal) TermState() TermState
- func (t *Terminal) Type(s string) error
- func (t *Terminal) Wait(timeout time.Duration) (int, error)deprecated
- func (t *Terminal) WaitExit(timeout time.Duration) (int, error)
- func (t *Terminal) WaitFor(cond func(Screen) bool, timeout time.Duration) error
- func (t *Terminal) WaitForCommand(timeout time.Duration) error
- func (t *Terminal) WaitForMatch(re *regexp.Regexp, scope Scope, timeout time.Duration) error
- func (t *Terminal) WaitForOutput(timeout time.Duration) error
- func (t *Terminal) WaitForPrompt(timeout time.Duration) error
- func (t *Terminal) WaitForText(substr string, timeout time.Duration) error
- func (t *Terminal) WaitStable(timeout time.Duration) error
- type TimeoutError
Constants ¶
const DefaultStabilizeInterval = 150 * time.Millisecond
DefaultStabilizeInterval is the quiet window WaitStable uses unless overridden.
Variables ¶
var ( // ErrTimeout is wrapped by every wait that runs out of time. ErrTimeout = errors.New("tuitest: timed out") // ErrChildExited is wrapped when the program under test exits before a // wait's condition is met. ErrChildExited = errors.New("tuitest: child exited before the condition was met") // ErrSemanticMarkers is wrapped by the OSC 133 waits when the terminal was // started without WithSemanticMarkers. ErrSemanticMarkers = errors.New("tuitest: semantic markers are not enabled (use WithSemanticMarkers)") )
Sentinel errors for the three ways a wait can fail. Every wait returns an error that wraps one of these, so a caller can branch on the kind of failure without type-asserting the concrete error:
if err := term.WaitForText("ready", time.Second); errors.Is(err, tuitest.ErrTimeout) {
// the program is merely slow
}
Functions ¶
Types ¶
type Cell ¶
type Cell struct {
// Rune is the cell's first rune. A cell can hold a whole grapheme cluster,
// so this is not always the character a user sees: "e" plus a combining
// acute reports 'e' here. Use Content to compare against text.
Rune rune
// Content is the cell's full grapheme cluster: the base rune together with
// any combining marks, joiners and modifiers that attach to it. It is what
// Line and Text render, and what a caller should match against. Empty for
// the continuation column of a wide rune.
Content string
// Width is 1 for normal runes, 2 for wide runes, and 0 for the
// continuation column that follows a wide rune.
Width int
// Fg and Bg are the foreground and background colors.
Fg, Bg Color
Bold bool
Faint bool
Italic bool
Underline bool
Reverse bool
Strikethrough bool
Blink bool
// Conceal reports SGR 8 (hidden). A real terminal draws a concealed cell as
// a blank, so Line and Text render these cells as spaces; the rune is still
// available here for a caller that needs to know what was concealed.
Conceal bool
}
Cell is a single grid cell with its rune and visual attributes.
type ClosedError ¶
type ClosedError struct {
// Op is the wait that failed, such as "WaitForText".
Op string
// Want describes the condition in words.
Want string
// ExitCode is the child's exit code, or -1 if it could not be determined.
ExitCode int
// Screen is the plain-text screen at the moment of the failure.
Screen string
// TailLog is the tail of the mirrored PTY I/O.
TailLog string
}
ClosedError is returned when the child exits before a wait's condition is met. It unwraps to ErrChildExited.
func (*ClosedError) Error ¶
func (e *ClosedError) Error() string
func (*ClosedError) Unwrap ¶
func (e *ClosedError) Unwrap() error
Unwrap makes errors.Is(err, ErrChildExited) true for every early exit.
type Color ¶
type Color struct {
// Kind selects which of the remaining fields is meaningful.
Kind ColorKind
// Index is the palette entry when Kind is ColorIndexed.
Index uint8
// R, G and B are the channel values when Kind is ColorRGB.
R, G, B uint8
}
Color is a cell color in one of three encodings. Only the fields matching Kind carry meaning; the others are zero.
type ColorKind ¶
type ColorKind int
ColorKind distinguishes the three color encodings a cell can carry.
type ExitStatus ¶
type ExitStatus struct {
// Code is the exit status, or -1 when the child died from a signal.
Code int
// Signaled is true when the child was killed by a signal rather than
// exiting on its own.
Signaled bool
// Signal is the killing signal when Signaled is true.
Signal syscall.Signal
}
ExitStatus describes how the child finished.
func (ExitStatus) Crashed ¶
func (s ExitStatus) Crashed() bool
Crashed reports whether the child died in a way that indicates a bug: killed by a fault signal, or exited non-zero. A clean zero exit is not a crash.
func (ExitStatus) String ¶
func (s ExitStatus) String() string
type Key ¶
type Key string
Key is a named key or chord expressed as the escape sequence it sends. Using typed values means a mistyped key name is a compile error, not a silent mismatch at runtime.
const ( Enter Key = "\r" Tab Key = "\t" Esc Key = "\x1b" Space Key = " " Backspace Key = "\x7f" Delete Key = "\x1b[3~" Up Key = "\x1b[A" Down Key = "\x1b[B" Right Key = "\x1b[C" Left Key = "\x1b[D" Home Key = "\x1b[H" End Key = "\x1b[F" PageUp Key = "\x1b[5~" PageDown Key = "\x1b[6~" Insert Key = "\x1b[2~" F1 Key = "\x1bOP" F2 Key = "\x1bOQ" F3 Key = "\x1bOR" F4 Key = "\x1bOS" F5 Key = "\x1b[15~" F6 Key = "\x1b[17~" F7 Key = "\x1b[18~" F8 Key = "\x1b[19~" F9 Key = "\x1b[20~" F10 Key = "\x1b[21~" F11 Key = "\x1b[23~" F12 Key = "\x1b[24~" )
Named keys. Values are the byte sequences a terminal sends for each key.
type MouseAction ¶
type MouseAction int
MouseAction is what the button did.
const ( // MousePress is a button going down. MousePress MouseAction = iota // MouseRelease is a button coming up. MouseRelease // MouseMove is motion with no button held. MouseMove // MouseDrag is motion with a button held. On the wire it is the same // motion bit as MouseMove; the two are distinguished by whether a button // is reported, and separating them is what lets a tape read as a drag. MouseDrag )
type MouseButton ¶
type MouseButton int
MouseButton identifies a mouse button or wheel direction.
const ( // MouseLeft is the primary button. MouseLeft MouseButton = iota // MouseMiddle is the middle button or wheel click. MouseMiddle // MouseRight is the secondary button. MouseRight // MouseWheelUp is one wheel notch away from the user. MouseWheelUp // MouseWheelDown is one wheel notch toward the user. MouseWheelDown // MouseWheelLeft is one horizontal wheel notch to the left. MouseWheelLeft // MouseWheelRight is one horizontal wheel notch to the right. MouseWheelRight // MouseBackward is the fourth button, "back" on most mice. MouseBackward // MouseForward is the fifth button, "forward" on most mice. MouseForward // MouseNone is no button: the button field of a motion report with nothing // held, and of a legacy release report, which does not say which button // came up. MouseNone )
type MouseEncoding ¶
type MouseEncoding int
MouseEncoding is the wire format a mouse report used.
It is part of the event because replaying a recorded session has to send the program the same bytes it originally received: a program that enabled only mode 1000 does not understand an SGR report, so re-encoding a captured X10 report as SGR would silently change what the test exercises.
const ( // MouseSGR is the modern SGR encoding (mode 1006), and the default for // events constructed by hand. It is the only encoding with no coordinate // limit and the only one that reports which button was released. MouseSGR MouseEncoding = iota // MouseX10 is the original encoding (modes 9, 1000, 1002 and 1003), which // packs each field into one byte offset by 32. MouseX10 // MouseURXVT is the urxvt encoding (mode 1015): X10's packing written as // decimal parameters, so it has no coordinate limit but still cannot say // which button was released. MouseURXVT )
type MouseEvent ¶
type MouseEvent struct {
// Col and Row are zero-based cell coordinates; the wire format's 1-based
// coordinates are produced during encoding. When Pixel is set they are
// zero-based pixel offsets instead.
Col, Row int
// Button is the button or wheel direction involved. It is ignored for
// MouseMove, which is motion with nothing held: to send motion with a
// button down, use MouseDrag.
Button MouseButton
// Action is what the button did.
Action MouseAction
// Mods are the modifier keys held at the time.
Mods KeyMods
// Pixel reports coordinates in pixels rather than cells (mode 1016). The
// wire format is identical to SGR's, so this can only ever be known from
// whether the program asked for pixel reporting.
Pixel bool
// Enc is the wire encoding to use. The zero value is MouseSGR.
Enc MouseEncoding
}
MouseEvent is a single mouse event at a zero-based cell coordinate.
func (MouseEvent) Encode ¶
func (e MouseEvent) Encode() (string, bool)
Encode renders the event in whichever wire encoding Enc names.
func (MouseEvent) EncodeSGR ¶
func (e MouseEvent) EncodeSGR() (string, bool)
EncodeSGR renders the event as an SGR (1006) or SGR-pixel (1016) sequence. Coordinates in the wire format are 1-based. It reports false for an event the encoding cannot express.
func (MouseEvent) EncodeURXVT ¶
func (e MouseEvent) EncodeURXVT() (string, bool)
EncodeURXVT renders the event in the urxvt encoding (mode 1015): X10's control byte and 1-based coordinates written as decimal parameters, which lifts X10's coordinate limit but keeps its inability to name a released button.
func (MouseEvent) EncodeX10 ¶
func (e MouseEvent) EncodeX10() (string, bool)
EncodeX10 renders the event in the original encoding, where the control byte and both coordinates are single bytes offset by 32.
It reports false for anything that packing cannot hold: a coordinate past column 223, which has no representation at all, and a release, which the encoding reports as the button-3 code without saying which button came up. Falling back rather than approximating is what keeps a recorded session's bytes intact.
type Option ¶
type Option func(*config)
Option configures a spawn.
func WithInheritEnv ¶
func WithInheritEnv() Option
WithInheritEnv starts from the parent process environment instead of the minimal hermetic default.
func WithOutputMirror ¶
WithOutputMirror copies the child's output to w as it arrives. Unlike WithLog, which mirrors both directions for debugging, this carries only what the program wrote, so w can be a real terminal the program is rendered onto while the harness still drives it headlessly. Used by tuitest record and tuitest replay.
func WithSemanticMarkers ¶
func WithSemanticMarkers() Option
WithSemanticMarkers enables OSC 133 tracking so the WaitForPrompt / WaitForCommand / LastCommandExit primitives work.
func WithStabilizeInterval ¶
WithStabilizeInterval sets the quiet window used by WaitStable.
func WithTrueColor ¶
func WithTrueColor() Option
WithTrueColor sets COLORTERM=truecolor for programs that gate 24-bit color.
type Screen ¶
type Screen interface {
// Size returns the grid size in cells.
Size() (cols, rows int)
// Cell returns the cell at the given zero-based column and row. Out-of-bounds
// coordinates return the zero Cell.
Cell(col, row int) Cell
// Cursor returns the cursor position (zero-based) and whether it is visible.
Cursor() (col, row int, visible bool)
// Text returns the plain-text screen, one row per line, with each line's
// trailing blanks trimmed and trailing blank lines dropped.
Text() string
// Line returns the plain text of a single physical row with trailing blanks
// trimmed, or "" for an out-of-range row. It does not de-wrap: a logical
// line that soft-wrapped at the right margin occupies several rows and will
// not match as one string. Match per row, or use Text and account for the
// wrap, or widen the terminal with WithSize so the line fits.
Line(row int) string
// ExitCode reports the child's exit code and whether it has exited.
ExitCode() (code int, exited bool)
}
Screen is a read-only view of the terminal grid handed to wait conditions and returned by snapshots. Every Screen value is an immutable copy taken under the terminal's lock, so a condition callback may stash it without observing a torn write from the output pump.
type TermState ¶
type TermState struct {
AltScreen bool
MouseTracking bool
BracketedPaste bool
FocusReporting bool
CursorHidden bool
// contains filtered or unexported fields
}
TermState is a snapshot of the terminal modes a program has left set, plus cursor visibility. It answers "did this TUI restore the terminal?", which is a common and user-visible bug class: a program that exits without leaving the alternate screen, or with mouse reporting still on, leaves the user's shell unusable.
func (TermState) Describe ¶
Describe lists the offending modes in a stable order, for error messages.
type Terminal ¶
type Terminal struct {
// contains filtered or unexported fields
}
Terminal is the harness handle for one spawned program.
func StartT ¶
StartT is the testing.TB-friendly constructor: it wires the debug log to t.Log, registers Close via t.Cleanup, and fails the test on spawn error.
func (*Terminal) AssertGolden ¶
AssertGolden compares the plain-text snapshot against testdata/<name>.golden, failing the test on mismatch with a unified diff. When UPDATE_GOLDEN is set in the environment or -update is passed, it rewrites the golden instead.
func (*Terminal) AssertGoldenStyled ¶
AssertGoldenStyled is AssertGolden for the styled encoding.
func (*Terminal) Done ¶
func (t *Terminal) Done() <-chan struct{}
Done returns a channel closed once the child has exited and been reaped. It lets a caller select on program exit alongside its own events, which Wait cannot express because it blocks.
func (*Terminal) ExitCode ¶
ExitCode reports the child's exit code and whether it has exited.
The code is -1 for a child killed by a signal, not the shell's 128+signal convention, so that it can never be confused with a program that exited with that number itself. It is -1 before the child has exited too, and the second return value is the only thing that separates those two cases. Use ExitStatus to tell a crash apart from an ordinary non-zero exit.
func (*Terminal) ExitStatus ¶
func (t *Terminal) ExitStatus() (ExitStatus, bool)
ExitStatus reports how the child finished and whether it has exited at all.
func (*Terminal) LastCommandExit ¶
LastCommandExit returns the exit code of the last finished command (OSC 133 D) and whether one has been seen. It reports false both when no command has finished and when the terminal was started without WithSemanticMarkers, so enable that option before relying on it; the WaitForPrompt and WaitForCommand waits return ErrSemanticMarkers in that case and are the better signal.
func (*Terminal) Paste ¶
Paste sends text wrapped in bracketed-paste markers, the way a terminal delivers a real paste. Programs that enable mode 2004 take a different code path for pasted text than for typed text, and that path is often the less tested one.
func (*Terminal) Progress ¶
Progress reports how many bytes the child has written so far and when the most recent write landed. A caller that sends input and then sees neither counter move has evidence the program stopped responding.
func (*Terminal) Resize ¶
Resize changes the PTY window size and the emulator grid; the child receives SIGWINCH. Like sending keys, a resize counts as input for WaitStable, since the redraw it provokes has not arrived yet.
func (*Terminal) SendKeys ¶
SendKeys types a sequence of named keys, chords, runes, and strings. Plain strings and runes are sent literally; Key values carry their own escape sequences. Items may be string, rune, Key, []string, []Key, or []any of those; anything else is rejected with an error rather than sent. Example:
term.SendKeys("git status", tuitest.Enter)
term.SendKeys(tuitest.Ctrl('b'), "%")
func (*Terminal) SendMouse ¶
func (t *Terminal) SendMouse(ev MouseEvent) error
SendMouse encodes a mouse event and sends it to the child, using the wire encoding named by the event's Enc field. The program under test must have enabled the matching mouse reporting mode for it to react.
func (*Terminal) SnapshotStyled ¶
SnapshotStyled returns the styled, diff-friendly encoding of the screen: each row's plain text followed by indented attribute runs for spans that differ from the default style. A screen with no styling degrades to the plain form.
func (*Terminal) TermState ¶
TermState returns the current mode state of the emulated terminal. Call it after the child has exited to check that it restored the terminal.
func (*Terminal) Type ¶
Type sends literal text with no key-name interpretation (tmux send-keys -l).
func (*Terminal) Wait
deprecated
func (*Terminal) WaitExit ¶
WaitExit blocks until the child exits or timeout elapses, returning the exit code. On timeout it returns -1 and a *TimeoutError, which unwraps to ErrTimeout like every other wait. A child killed by a signal also returns -1, with a nil error; ExitStatus is what tells the two apart.
func (*Terminal) WaitFor ¶
WaitFor blocks until cond returns true on the current screen, or timeout.
func (*Terminal) WaitForCommand ¶
WaitForCommand blocks until the current command finishes (OSC 133 D). Requires WithSemanticMarkers.
func (*Terminal) WaitForMatch ¶
WaitForMatch blocks until re matches within the given scope.
func (*Terminal) WaitForOutput ¶
WaitForOutput blocks until the child writes anything at all after the call begins, or it exits, or timeout elapses.
This is the primitive for "wait until the program reacts to what I just sent", which WaitStable does not express: WaitStable asks whether output has been quiet for a window, and after a pause that is already true, so it returns immediately without the program having done anything. Use WaitStable to wait for a burst of output to finish, and WaitForOutput to wait for a reaction to begin. A child that has exited counts as settled, since no further output can arrive.
func (*Terminal) WaitForPrompt ¶
WaitForPrompt blocks until a new shell prompt (OSC 133 A) is drawn. Requires WithSemanticMarkers.
func (*Terminal) WaitForText ¶
WaitForText blocks until the plain-text screen contains substr.
func (*Terminal) WaitStable ¶
WaitStable blocks until the terminal has been quiet for the stabilize interval (see WithStabilizeInterval), or until timeout. A child that has exited counts as stable.
The quiet window is measured from the later of the last output byte and the last input tuitest sent. That matters: measured from output alone, calling WaitStable immediately after SendKeys would return against the pre-keystroke screen whenever the program had already been idle for the interval. Waiting out the window from the keystroke instead gives the program that long to start reacting, and any byte it produces restarts the window.
It is still a heuristic. A program that takes longer than the stabilize interval to produce its first byte will be reported stable too early, and no quiescence rule can distinguish that from a program with nothing to say. Prefer WaitForText, WaitForMatch or WaitFor whenever the expected end state is known, and reach for WaitStable only after heavy output where it is not.
type TimeoutError ¶
type TimeoutError struct {
// Op is the wait that failed, such as "WaitForText".
Op string
// Want describes the condition in words, such as `text "ready"`.
Want string
// Elapsed is how long the wait actually took.
Elapsed time.Duration
// Screen is the plain-text screen at the moment of the failure.
Screen string
// TailLog is the tail of the mirrored PTY I/O.
TailLog string
}
TimeoutError is returned by every wait that times out. Its message includes a full screen dump and the tail of the mirrored I/O log, so a failing CI run shows exactly what was on screen instead of a bare "timeout". It unwraps to ErrTimeout.
func (*TimeoutError) Error ¶
func (e *TimeoutError) Error() string
func (*TimeoutError) Unwrap ¶
func (e *TimeoutError) Unwrap() error
Unwrap makes errors.Is(err, ErrTimeout) true for every wait timeout.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
tuitest
command
Command tuitest tests terminal programs from the command line.
|
Command tuitest tests terminal programs from the command line. |
|
Package fixtures provides testing utilities for tuitest, including a fake shell that produces predictable output and sends/receives ANSI sequences, plus an ANSI escape-sequence builder.
|
Package fixtures provides testing utilities for tuitest, including a fake shell that produces predictable output and sends/receives ANSI sequences, plus an ANSI escape-sequence builder. |
|
Package fuzz drives a terminal program with randomised but structured input and watches for the ways a TUI breaks: crashing, hanging, corrupting the screen model, growing without bound, or exiting without restoring the terminal.
|
Package fuzz drives a terminal program with randomised but structured input and watches for the ways a TUI breaks: crashing, hanging, corrupting the screen model, growing without bound, or exiting without restoring the terminal. |
|
vtgen
Package vtgen generates terminal input that looks like something a program would actually emit.
|
Package vtgen generates terminal input that looks like something a program would actually emit. |
|
internal
|
|
|
cli
Package cli implements the tuitest command line.
|
Package cli implements the tuitest command line. |
|
emu
Package emu holds the VT emulator adapter used by tuitest.
|
Package emu holds the VT emulator adapter used by tuitest. |
|
ptyproc
Package ptyproc owns the process and PTY lifecycle for tuitest: spawning a child attached to a pseudo-terminal, pumping its output, resizing, EOF and exit-code handling, and process-tree teardown.
|
Package ptyproc owns the process and PTY lifecycle for tuitest: spawning a child attached to a pseudo-terminal, pumping its output, resizing, EOF and exit-code handling, and process-tree teardown. |
|
textdist
Package textdist provides the string distance behind "did you mean" hints.
|
Package textdist provides the string distance behind "did you mean" hints. |
|
vt
Package vt provides a virtual terminal implementation.
|
Package vt provides a virtual terminal implementation. |
|
Package tape implements the small VHS-inspired tape language for tuitest's CLI and a player that drives a tuitest.Terminal.
|
Package tape implements the small VHS-inspired tape language for tuitest's CLI and a player that drives a tuitest.Terminal. |
|
Package tuiosx holds the tuios-specific conveniences for tuitest: a prefix chord helper and a spawn helper that isolates each tuios instance in its own temporary XDG state so parallel tests do not collide on a shared daemon socket.
|
Package tuiosx holds the tuios-specific conveniences for tuitest: a prefix chord helper and a spawn helper that isolates each tuios instance in its own temporary XDG state so parallel tests do not collide on a shared daemon socket. |


