Skip to content

feat(assert): 'exit code: 127' should say the command was not found #982

Description

@Chemaclass

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

  • 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.md states plainly that assert_true/assert_false take 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

enhancementNew feature or request

Type

No type

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions