Today — the integration suite spawns /bin/sh (or sh) at 98 sites across 17 of its 20 test files, each with a printf script as the program under test — 208 printf invocations in all. Measured against 0.9.0:
$ grep -c 'spawn("/bin/sh")\|spawn("sh")' crates/termlens/tests/*.rs | awk -F: '{s+=$2} END {print s}'
98
Every escape-sequence test is written as printf '\033(0lqk'; every "print, then hold the terminal open" is printf …; read _. The five fixtures in fixtures/ cover only the shapes that need a real event loop: crossterm input, resize, images, width.
Why it is worth fixing — three reasons, in order of weight.
The shell is a variable in every test that is not about the shell. Which sh runs — dash on Debian, bash in POSIX mode on macOS, busybox elsewhere — decides how printf treats \033 and %s, what read _ does at EOF, and how fast while :; do …; sleep 0.01; done iterates. The stress workflow caught exactly this in #243: a test's premise held only until the loop's sleep took longer to spawn than the stillness the test was measuring. The shell's process model leaked into a wait-semantics test.
It is why the suite cannot run where there is no /bin/sh. Windows is the obvious case — #149 names this as the first of its four steps — but so is any minimal container image.
And it is a barrier for contributors who think in Rust: printf '\033)0\016lqk\017 lqk \016x\017' asks its reader to decode octal escapes and shell quoting before the terminal semantics the test is about.
Fix — one more fixture binary, fixtures/emit (name open), replacing the sh -c 'printf …; read _' idiom with a small argument language and no shell in between:
emit 'ESC ( 0' lqk NL 'ESC ( B' --wait # print, then hold stdin open until Enter
emit --raw 'caf\xe9' ' done' NL # raw bytes, for the non-UTF-8 cases
emit first --sleep 2s ' second' --wait # timed output, for the wait tests
Concretely: an enum Step { Text, Esc, Csi, Raw, Sleep, Wait, Exit(code) } parsed from argv, written to stdout with one write_all per step and a flush, and --wait reading one line from stdin the way every fixture already does (docs/DESIGN.md §2, the instant-exit caveat). Then a tests/common helper emit(steps) -> Terminal, and a mechanical migration of the sh(...) helpers one test file per commit, so a reviewer diffs one file's scripts against their emit form. Tests that are about a shell (stty, a read loop that echoes) keep /bin/sh, with a comment saying why.
Two rules for the fixture's //! header: no timing inside the binary except the explicit --sleep, and std only, so the fixture cannot become a second thing the suite tests.
Done when — every printf-only script in crates/termlens/tests/ goes through the fixture; grep -c 'spawn("/bin/sh")' crates/termlens/tests/*.rs counts only tests that are about a shell, each with a comment saying so; the fixture is std-only with its steps documented in its header; and the stress workflow passes on the migrated suite.
Today — the integration suite spawns
/bin/sh(orsh) at 98 sites across 17 of its 20 test files, each with aprintfscript as the program under test — 208printfinvocations in all. Measured against 0.9.0:Every escape-sequence test is written as
printf '\033(0lqk'; every "print, then hold the terminal open" isprintf …; read _. The five fixtures infixtures/cover only the shapes that need a real event loop: crossterm input, resize, images, width.Why it is worth fixing — three reasons, in order of weight.
The shell is a variable in every test that is not about the shell. Which
shruns — dash on Debian, bash in POSIX mode on macOS, busybox elsewhere — decides howprintftreats\033and%s, whatread _does at EOF, and how fastwhile :; do …; sleep 0.01; doneiterates. The stress workflow caught exactly this in #243: a test's premise held only until the loop'ssleeptook longer to spawn than the stillness the test was measuring. The shell's process model leaked into a wait-semantics test.It is why the suite cannot run where there is no
/bin/sh. Windows is the obvious case — #149 names this as the first of its four steps — but so is any minimal container image.And it is a barrier for contributors who think in Rust:
printf '\033)0\016lqk\017 lqk \016x\017'asks its reader to decode octal escapes and shell quoting before the terminal semantics the test is about.Fix — one more fixture binary,
fixtures/emit(name open), replacing thesh -c 'printf …; read _'idiom with a small argument language and no shell in between:Concretely: an
enum Step { Text, Esc, Csi, Raw, Sleep, Wait, Exit(code) }parsed from argv, written to stdout with onewrite_allper step and a flush, and--waitreading one line from stdin the way every fixture already does (docs/DESIGN.md§2, the instant-exit caveat). Then atests/commonhelperemit(steps) -> Terminal, and a mechanical migration of thesh(...)helpers one test file per commit, so a reviewer diffs one file's scripts against theiremitform. Tests that are about a shell (stty, areadloop that echoes) keep/bin/sh, with a comment saying why.Two rules for the fixture's
//!header: no timing inside the binary except the explicit--sleep, and std only, so the fixture cannot become a second thing the suite tests.Done when — every
printf-only script incrates/termlens/tests/goes through the fixture;grep -c 'spawn("/bin/sh")' crates/termlens/tests/*.rscounts only tests that are about a shell, each with a comment saying so; the fixture is std-only with its steps documented in its header; and the stress workflow passes on the migrated suite.