Repository navigation
Description
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: 127Both 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: 127A 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.mdthatassert_truetakes 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 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
- Bash 3.0+; this is a
caseon 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_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.- This project compares failure output verbatim in tests (
print_failed_test), so the change is mechanically verifiable. CHANGELOG.mdunder### Changed.
Acceptance criteria
- Exit code 127 reports "command not found" naming the argument
- The pass/fail verdict for every existing case is unchanged (only the message differs)
- A decision is recorded on 126, and on whether
[ ... ]is supported or only documented -
docs/assertions.mdstates plainly thatassert_true/assert_falsetake a command, with working examples - Tests cover: existing command, missing command, non-executable file, and
[ -d /tmp ] -
make sa·make lint·./bashunit --parallel --simple --strict tests/·bash build.sh bin -v
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
Chemaclass commentedon Aug 8, 2026
Done in #991 (merged).
Shipped slightly wider than the issue described:
assert_falsealso had a false pass — exit 127 counted as "non-zero, therefore false", so a typo'd command name satisfied it while running nothing. 126/127 now fail both assertions.The wording is
unknown command:rather thancommand not found:, becauserunner/diagnostics.shclassifies runtime errors by string-matching that exact phrase and the obvious wording produced a duplicate Failed+Error report. Filed as #992.