Summary
Two new features for EXIT NEVER (background process) entries:
[Finally] section — send a signal to the process at file-end and assert exit behavior
later assert modifier — defer assertion evaluation until file-end
later modifier
Asserts marked with later are not evaluated during polling. They remain open and are checked against the accumulated output at file-end (before [Finally]).
@timeout 5000
php -S localhost:8080
EXIT NEVER
[Asserts]
stderr contains "Development Server"
stderr contains later "[404]: GET / - No such file or directory"
The 404 log is produced by a later entry (e.g. curl localhost:8080). later decouples the assertion from polling timing.
[Finally] section
Entry-scoped section for EXIT NEVER entries. Executed at file-end in LIFO order (before @defer).
Syntax:
[Finally]
<SIGNAL> EXIT <code> [timeout <ms>]
<optional asserts>
Examples:
[Finally]
TERM EXIT 0 timeout 3000
stderr contains "graceful shutdown... bye"
Semantics
SIGNAL — signal name to send (TERM, KILL, INT, HUP, QUIT, etc.)
EXIT <code> — expected exit code (explicit, because processes may catch signals and exit cleanly)
timeout <ms> — max wait time for process to die (default: 1000ms)
- If timeout reached → FAIL (no automatic SIGKILL escalation)
- Asserts after the signal line evaluate against the process output after signal delivery
Signals and exit codes
| Signal |
Unhandled exit code |
| TERM |
143 (128+15) |
| KILL |
137 (128+9) |
| INT |
130 (128+2) |
| HUP |
129 (128+1) |
| QUIT |
131 (128+3) |
Processes with signal handlers may exit with any code (e.g. EXIT 0 for graceful shutdown).
Execution order at file-end
- All regular entries complete
later asserts evaluated (against accumulated output)
[Finally] sections executed (LIFO)
@defer entries executed (LIFO, cleanup fallback)
Full example
# Start web server
@timeout 5000
php -S localhost:8080
EXIT NEVER
[Asserts]
stderr contains "Development Server"
stderr contains later "[404]: GET / - No such file or directory"
[Finally]
TERM EXIT 0 timeout 3000
stderr contains "graceful shutdown... bye"
# Trigger 404 log
curl localhost:8080
EXIT 0
Design decisions
@timeout only controls assert polling deadline (unchanged semantics)
later ignores @timeout — it waits until file-end
[Finally] is decoupled from @timeout — always triggers at file-end
timeout in [Finally] is a keyword on the signal line, not a directive (directives only appear before commands)
- Default Finally timeout: 1000ms
- No automatic SIGKILL fallback on timeout —
@defer handles cleanup if needed
Summary
Two new features for
EXIT NEVER(background process) entries:[Finally]section — send a signal to the process at file-end and assert exit behaviorlaterassert modifier — defer assertion evaluation until file-endlatermodifierAsserts marked with
laterare not evaluated during polling. They remain open and are checked against the accumulated output at file-end (before[Finally]).The 404 log is produced by a later entry (e.g.
curl localhost:8080).laterdecouples the assertion from polling timing.[Finally]sectionEntry-scoped section for
EXIT NEVERentries. Executed at file-end in LIFO order (before@defer).Syntax:
Examples:
Semantics
SIGNAL— signal name to send (TERM, KILL, INT, HUP, QUIT, etc.)EXIT <code>— expected exit code (explicit, because processes may catch signals and exit cleanly)timeout <ms>— max wait time for process to die (default: 1000ms)Signals and exit codes
Processes with signal handlers may exit with any code (e.g.
EXIT 0for graceful shutdown).Execution order at file-end
laterasserts evaluated (against accumulated output)[Finally]sections executed (LIFO)@deferentries executed (LIFO, cleanup fallback)Full example
Design decisions
@timeoutonly controls assert polling deadline (unchanged semantics)laterignores@timeout— it waits until file-end[Finally]is decoupled from@timeout— always triggers at file-endtimeoutin[Finally]is a keyword on the signal line, not a directive (directives only appear before commands)@deferhandles cleanup if needed