4.4 KiB
shout
A transcript-based shell integration test runner.
Format
A .shout file is a plain text transcript of a shell session. Lines starting
with $ are commands. Everything after — until the next $ or end of file
— is the expected output (stdout and stderr combined).
$ dev new "add auth"
created draft 1 "add auth"
$ dev save
saved draft 1 (v1)
Blank lines within expected output are significant. Trailing newline on the file is ignored.
Comments
# after a command is a comment and is stripped before execution. Comments
in expected output are matched literally.
$ dev new "add auth" # create a draft from timeline HEAD
created draft 1 "add auth"
Wildcards
A ... on its own line in expected output matches any number of lines
(including zero).
$ dev log
...
draft 1 "add auth"
A ... inline matches any sequence of characters on that line.
$ dev status
draft 1 "add auth" (v...)
Environment
Each .shout file runs in a fresh temporary directory. The directory is
created before the first command and removed after the last (unless
--keep is passed).
All commands in a file run in a single shell session (/bin/sh), so cd,
export, and other shell state persists between commands.
The following environment variables are set for every command:
| Variable | Value |
|---|---|
HOME |
the temp directory |
PATH |
inherited from host (or prepended via --path) |
CUE_DIR |
the temp directory |
All other environment variables are inherited from the host unless explicitly
cleared with --clean-env.
Exit codes
By default, a non-zero exit code fails the test regardless of output. To
assert a specific exit code, append [N] on the last line of expected output:
$ dev rm
error: draft 1 has children. use dev rm -f to cascade.
[1]
[*] accepts any non-zero exit code without asserting the value.
CLI
shout [options] [files|dirs...]
If no files are given, shout runs all *.shout files in the current directory
and subdirectories. Each command in each shout file is run sequentially
(unless --parallel is passed).
Options
| Flag | Description |
|---|---|
--update / -u |
Rewrite expected output in-place with actual output |
--keep / -k |
Keep temp directories after run (printed to stderr) |
--clean-env |
Start with empty environment (only PATH and CUE_DIR set) |
--path <path> |
Prepend <path> to PATH (repeatable) |
--timeout <dur> |
Per-command timeout, e.g. 500ms, 10s, 1m (default: 10s) |
--verbose / -v |
Print each command as it runs |
--parallel |
Run files in parallel (implies all files run regardless of failures) |
Output
Passing files print a single . per file. Failing files print a unified diff:
FAIL tests/auth.shout
$ dev rm
- error: draft 1 has children. use dev rm -f to cascade.
+ error: draft 1 has dependents. use dev rm -f to cascade.
[1]
Summary line at the end:
12 passed, 1 failed in 340ms
Update mode
--update rewrites the expected output sections of each .shout file with the
actual output from the run. Commands, comments, and whitespace are preserved.
Wildcard lines are left in place if the actual output matches them; they are
only replaced if the match fails.
This makes it safe to run shout --update routinely after intentional output
changes — review the diff, commit if correct.
File layout
tests/
auth.shout
drafts.shout
stack.shout
No special directory structure is required. .shout files can live anywhere.
Implementation notes
- Bun + TypeScript
- Each file runs in a single
/bin/shsession viaBun.spawn - Stdout and stderr merged (same as a terminal)
- Shell state (
cd,export, etc.) persists across commands within a file - Commands are fed to the shell sequentially; output between commands is
captured by delimiting with sentinel
echostatements - shout exits
0if all tests pass,1if any fail
Example
$ dev new "add auth"
created draft 1 "add auth"
$ echo 'export function auth() {}' > auth.ts
$ dev save
saved draft 1 (v1)
$ echo 'export function auth(token: string) {}' > auth.ts
$ dev save
saved draft 1 (v2)
$ dev status
draft 1 "add auth" (v2)
modified: (none)
$ dev new "add db"
created draft 2 "add db"
$ dev rm 1
error: draft 1 has children. use dev rm -f to cascade.
[1]