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

Fields

No fields configured for issues without a type.

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions