Behavioral Testing Engine — Deterministic testing for terminal applications.
BTE is like Playwright for the terminal. It spawns real processes in a PTY, sends input, captures output, and verifies behavior automatically.
- Language-agnostic — Test any CLI/TUI app: Rust, Python, Go, Node.js, Bash, C/C++
- Deterministic — Seed-based execution for reproducible tests
- Declarative — Define tests in YAML, no code required
- Comprehensive — Signal injection, mouse events, screen assertions, custom invariants
- Debuggable — Trace recording and replay for investigating failures
git clone https://github.com/syedazeez337/bte.git
cd bte
cargo build --releaseThe binary will be at ./target/release/bte.
Requirements: Rust 1.82+, Linux (macOS experimental)
Create test.yaml:
name: hello-world
command: "echo 'Hello, World!'"
steps:
- action: wait_for
pattern: "Hello, World"
invariants:
- type: cursor_boundsRun it:
$ bte run test.yaml
=== Run Result ===
Exit code: 0
Steps executed: 1
Ticks: 0
Status: SUCCESS (exit=0, ticks=0)bte run <scenario.yaml> # Run a test
bte run <scenario> -o trace.json # Run and save trace
bte validate <scenario.yaml> # Validate syntax
bte info <trace.json> # Inspect trace
bte replay <trace.json> # Replay for verificationOptions:
-s, --seed <N> Override random seed
--max-ticks <N> Max execution ticks (default: 10000)
-v, --verbose Debug output
name: my-test
command: bash
terminal: { cols: 80, rows: 24 }
steps:
- action: send_keys
keys: "echo hello\n"
- action: wait_for
pattern: "hello"
timeout_ms: 5000
- action: send_signal
signal: SIGTERM
invariants:
- type: cursor_bounds
- type: no_deadlock
timeout_ms: 5000
seed: 42
timeout_ms: 30000| Action | Description |
|---|---|
send_keys |
Send keystrokes (\n, ${Enter}, ${Ctrl_c}) |
wait_for |
Wait for regex pattern in output |
wait_screen |
Wait for pattern in screen buffer |
wait_ticks |
Wait N scheduling ticks |
send_signal |
Send signal (SIGTERM, SIGINT, SIGKILL, etc.) |
resize |
Resize terminal (cols, rows) |
mouse_click |
Click at position |
mouse_scroll |
Scroll at position |
assert_screen |
Assert screen contains pattern |
assert_cursor |
Assert cursor position |
snapshot |
Capture screen state |
| Type | Description |
|---|---|
cursor_bounds |
Cursor stays within screen |
no_deadlock |
Process produces output within timeout |
screen_contains |
Screen contains pattern |
screen_stable |
Screen unchanged for N ticks |
custom |
User-defined pattern/cursor checks |
${Enter} ${Escape} ${Tab} ${Backspace}
${Up} ${Down} ${Left} ${Right}
${Home} ${End} ${PageUp} ${PageDown}
${F1}..${F12}
${Ctrl_a}..${Ctrl_z}
${Alt_x}
Interactive shell test:
name: bash-test
command: bash
steps:
- action: send_keys
keys: "ls -la\n"
- action: wait_for
pattern: "total"
timeout_ms: 5000
- action: send_keys
keys: "exit\n"
invariants:
- type: cursor_boundsSignal handling:
name: signal-test
command: "sleep 30"
steps:
- action: wait_ticks
ticks: 10
- action: send_signal
signal: SIGTERMTUI application:
name: fzf-test
command: fzf
steps:
- action: send_keys
keys: "query"
- action: wait_screen
pattern: "query"
- action: send_keys
keys: "${Enter}"
invariants:
- type: no_deadlock
timeout_ms: 10000See examples/ and scenarios/ for more.
Save execution traces for debugging:
# Save trace
bte run test.yaml -o trace.json
# Inspect
bte info trace.json
# Replay to verify determinism
bte replay trace.jsonUse verbose mode for detailed output:
bte -v run test.yaml| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error |
| 124+ | Signaled (124 = SIGTERM, 137 = SIGKILL) |
- Tutorial — Step-by-step guide
- API Reference — Complete documentation
- Changelog — Version history
- Roadmap — Planned features
Contributions welcome! Please read CONTRIBUTING.md before submitting PRs.
cargo test # Run tests
cargo clippy # Lint
cargo fmt # FormatMIT — see LICENSE