Skip to content

perf(assert): remove per-assertion subshell forks (strip_ansi, label, alias probe) #772

Description

@Chemaclass

Part of #761.

Problem

The assertion layer still forks avoidable subshells on its hottest paths (measured on macOS bash 3.2, Apple Silicon, commit 1ae3e82):

  1. assert_equals/assert_not_equals fork twice per call, even on success. src/assert.sh:159,161 and src/assert.sh:182,184 capture bashunit::str::strip_ansi with $(...). strip_ansi (src/str.sh:4) already has a pure-bash fast path for plain input, but callers still pay the command-substitution fork to read it. Measured: a file with 1000 assert_equals runs in ~1.45s vs ~0.68s for the trivial baseline, i.e. ~0.77ms extra per assertion.
  2. Failure-path label resolution forks 2-3 more times per failing assertion. bashunit::assert::label (src/assert.sh:15) captures find_test_function_name via $(...) and is itself always captured via $(...) at every call site; bashunit::handle_bool_assertion_failure (src/assert.sh:120) and bashunit::fail (src/assert.sh:26) repeat the same nested-capture pattern.
  3. bashunit::run_command_or_eval forks a pipeline per non-literal assert_true/assert_false. src/assert.sh:111 runs $(command -v "$cmd" | "$GREP" -cE '^alias') just to detect an alias.

Task — checklist, one commit each

  • Add a return-slot variant of strip_ansi (e.g. bashunit::str::strip_ansi_to_slot writing _BASHUNIT_STR_STRIPPED_OUT, per .claude/rules/bash-style.md), whose no-ANSI fast path sets the global with zero forks. Convert assert_equals and assert_not_equals to it. Keep the echoing strip_ansi as a thin wrapper for any other callers.
  • Convert bashunit::assert::label, bashunit::handle_bool_assertion_failure, and bashunit::fail to return slots: add slot variants of bashunit::helper::find_test_function_name (src/helpers.sh:14) and bashunit::helper::normalize_test_function_name (src/helpers.sh:40), then update all label="$(bashunit::assert::label ...)" call sites in src/assert.sh.
  • Replace the alias probe at src/assert.sh:111 with a fork-free builtin check, e.g. if alias -- "$cmd" >/dev/null 2>&1; then eval .... Verify equivalence: for functions/binaries/nonexistent commands alias exits non-zero, matching the current command -v | grep -c '^alias' result. Cover with a unit test that defines an alias (requires shopt -s expand_aliases in the test) and one that uses a function.

Tests first (TDD)

Behavior must not change; the existing suite is the spec. Add a regression test for each new return-slot helper: call it with one of its own internal variable names (dynamic-scoping trap documented in .claude/rules/bash-style.md, bug precedent PR #672).

Constraints

  • Bash 3.0+ only: no declare -A, no [[ ]], no ${var,,}, no printf -v (3.1+).
  • Console output must remain byte-identical (snapshot/acceptance tests will catch drift).

Acceptance criteria

  • ./bashunit tests/ and ./bashunit --parallel tests/ pass after every commit.
  • make sa, make lint pass; shfmt -w . produces no diff.
  • Report before/after timing of a 100-test x 10 assert_equals file in the PR (baseline above).

Line references valid at commit 1ae3e82.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions