From edce909525c26172dbc1152c0759c4303bd844f4 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 10 Mar 2026 00:04:38 -0700 Subject: [PATCH] ideas --- SHOUT.md | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 SHOUT.md diff --git a/SHOUT.md b/SHOUT.md new file mode 100644 index 0000000..c8176ea --- /dev/null +++ b/SHOUT.md @@ -0,0 +1,127 @@ +# Shout — Proposed Improvements + +Two additions to the shout test framework: automatic process cleanup and test isolation primitives. + +## 1. Automatic Process Cleanup + +### Problem + +Any test that backgrounds a process (`&`) must manually clean it up. If a test fails or times out before reaching its cleanup command, the process is orphaned and holds its port indefinitely. Every test author has to remember the `trap` pattern: + +``` +$ my-server &; SVR=$!; trap "kill $SVR 2>/dev/null" EXIT; ... +``` + +This is error-prone and noisy. + +### Proposal + +Shout already owns the shell process (via `Bun.spawn`). After the shell exits, shout should kill the entire process group to reap any lingering children. + +In `run.ts`, after the shell completes: + +```ts +// Kill the shell's process group to clean up backgrounded children +try { + process.kill(-proc.pid, "SIGTERM") +} catch {} +``` + +The `-pid` syntax sends the signal to the entire process group. Since shout spawns the shell, the shell and all its children share a process group. + +This requires no syntax changes, no test file modifications, and no action from test authors. Background a process, forget about it — shout cleans up. + +For defense in depth, follow up with SIGKILL after a short grace period: + +```ts +try { + process.kill(-proc.pid, "SIGTERM") +} catch {} +setTimeout(() => { + try { process.kill(-proc.pid, "SIGKILL") } catch {} +}, 500) +``` + +### Migration + +Remove the manual `kill` / `trap` lines from existing test files. They become no-ops but add visual noise. + +## 2. Test Isolation Primitives + +### Problem + +Every test file that needs a server repeats the same boilerplate: + +``` +$ PORT=19001 ... dev-server > /dev/null 2>&1 & SVR=$!; trap "kill $SVR 2>/dev/null" EXIT; i=0; while ! curl -sf http://localhost:19001/...; do ...; done; echo "ok" +ok + +$ mkdir -p .config/dev && echo '{"server":"http://localhost:19001",...}' > .config/dev/config.json +``` + +Each test picks a hardcoded port. Adding a new test means manually checking which ports are taken. Parallel test runs risk port collisions. + +### Proposal: `# setup` directive + +A new directive that includes commands from a shared file before the test's own commands: + +``` +# setup tests/setup.shout +``` + +The setup file is a normal `.shout` file. Its commands are prepended to the test's script (same shell, same working directory, same environment). This is purely textual inclusion — no new execution model. + +Example `tests/setup.shout`: + +``` +$ dev-server > /dev/null 2>&1 & +$ mkdir -p .config/dev && echo "{\"server\":\"http://localhost:$PORT\",\"token\":\"dev-token-1\"}" > .config/dev/config.json +$ i=0; while ! curl -sf http://localhost:$PORT/api/whoami -H "Authorization: Bearer dev-token-1" > /dev/null 2>&1; do i=$((i+1)); if [ $i -gt 30 ]; then echo "server failed"; exit 1; fi; sleep 0.2; done; echo "ok" +ok +``` + +Then a test file becomes: + +``` +# Phase 1 — Linear Timeline +# setup tests/setup.shout + +$ dev init myapp +initialized repo myapp in ./myapp + +$ cd myapp +... +``` + +### Proposal: `--port-from ` flag + +A CLI flag that auto-assigns ports to test files: + +```sh +shout --port-from 19000 tests/ +``` + +Shout sets `$PORT` in each test file's environment, incrementing from the base. When `--parallel` is used, each file gets a unique port with no coordination needed. + +Implementation in the runner: + +```ts +let nextPort = options.portFrom +for (const file of files) { + const env = { ...baseEnv, PORT: String(nextPort++) } + await runFile(file, { ...options, env }) +} +``` + +Test files reference `$PORT` instead of hardcoded values. Combined with `# setup`, the per-file boilerplate drops to one line. + +### Proposal: `# env` directive + +For cases simpler than `# setup` — setting environment variables without a separate file: + +``` +# env PORT=19001 +# env NODE_ENV=production +``` + +These are injected into the shell environment before any commands run. Lighter than a setup file when all you need is a few variables.