Summary
assert_true and assert_false run their argument as a command. When that command does not exist, the failure says:
✗ Failed: ...
Expected 'command or function with zero exit code'
but got 'exit code: 127'
127 is the shell exit code for "command not found", and the assertion has that information at the moment it fails. It reports the number instead.
Why this is worse than it looks
The most natural thing a shell author writes is a test expression, and it produces exactly this message:
assert_true "[ -d /tmp ]" # exit code: 127
assert_true "[ -L \"$path\" ]" # exit code: 127
Both are correct-looking bash. They fail because assert_true invokes its argument as a command word, and [ -d /tmp ] is not one — but nothing in the output says so. Reproduced on main:
function test_assert_true_with_command() { assert_true "true"; } # ✓
function test_assert_true_with_zero() { assert_true 0; } # ✓
function test_assert_true_with_bracket_expr() { assert_true "[ -d /tmp ]"; } # ✗ exit code: 127
function test_assert_true_with_missing_cmd() { assert_true "definitely_not_a_command"; }# ✗ exit code: 127
A reader seeing exit code: 127 on the third one has no reason to suspect the shape of their argument is the problem — the expression is valid bash and works in an if. They will look at /tmp first.
Proposal
Two parts, both small.
1. Name the failure. When the captured exit code is 127, say so:
Expected 'command or function with zero exit code'
but got 'command not found: [ -d /tmp ]'
That single change makes both cases self-diagnosing: the missing-command case becomes obvious, and the [ ... ] case now points at the argument instead of the filesystem.
The same treatment is worth considering for 126 ("found but not executable"), which is the other exit code the shell reserves and which is equally opaque as a bare number.
2. Decide what to do about test expressions, and document whichever way it goes. Options:
- Document only — state in
docs/assertions.md that assert_true takes a command, and show the working forms. Cheapest, and combined with part 1 the failure now teaches the rule.
- Support them — detect a leading
[ / [[ and evaluate rather than invoke. Tempting, but it means assert_true sometimes evals its argument, which is a meaningfully different security and quoting story. I would not do this without a deliberate decision.
I lean strongly to part 1 plus documenting, and not special-casing brackets.
Constraints
- Bash 3.0+; this is a
case on a number, so no compatibility surface.
- Per-assertion path — the check must not add a fork. A
case "$exit_code" in 127) is free.
assert_true/assert_false are public API. The message changes; the pass/fail verdict must not. Any test asserting on the old text needs updating — grep tests/ for exit code: 127.
- This project compares failure output verbatim in tests (
print_failed_test), so the change is mechanically verifiable.
CHANGELOG.md under ### Changed.
Acceptance criteria
Summary
assert_trueandassert_falserun their argument as a command. When that command does not exist, the failure says:127 is the shell exit code for "command not found", and the assertion has that information at the moment it fails. It reports the number instead.
Why this is worse than it looks
The most natural thing a shell author writes is a test expression, and it produces exactly this message:
Both are correct-looking bash. They fail because
assert_trueinvokes its argument as a command word, and[ -d /tmp ]is not one — but nothing in the output says so. Reproduced onmain:A reader seeing
exit code: 127on the third one has no reason to suspect the shape of their argument is the problem — the expression is valid bash and works in anif. They will look at/tmpfirst.Proposal
Two parts, both small.
1. Name the failure. When the captured exit code is 127, say so:
That single change makes both cases self-diagnosing: the missing-command case becomes obvious, and the
[ ... ]case now points at the argument instead of the filesystem.The same treatment is worth considering for 126 ("found but not executable"), which is the other exit code the shell reserves and which is equally opaque as a bare number.
2. Decide what to do about test expressions, and document whichever way it goes. Options:
docs/assertions.mdthatassert_truetakes a command, and show the working forms. Cheapest, and combined with part 1 the failure now teaches the rule.[/[[and evaluate rather than invoke. Tempting, but it meansassert_truesometimes evals its argument, which is a meaningfully different security and quoting story. I would not do this without a deliberate decision.I lean strongly to part 1 plus documenting, and not special-casing brackets.
Constraints
caseon a number, so no compatibility surface.case "$exit_code" in 127)is free.assert_true/assert_falseare public API. The message changes; the pass/fail verdict must not. Any test asserting on the old text needs updating — greptests/forexit code: 127.print_failed_test), so the change is mechanically verifiable.CHANGELOG.mdunder### Changed.Acceptance criteria
[ ... ]is supported or only documenteddocs/assertions.mdstates plainly thatassert_true/assert_falsetake a command, with working examples[ -d /tmp ]make sa·make lint·./bashunit --parallel --simple --strict tests/·bash build.sh bin -v