Skip to content

perf(runner): remove avoidable subshell forks from the run_test hot path #764

Description

@Chemaclass

Part of #761.

Problem

bashunit::runner::run_test (src/runner.sh:1036) and its helpers fork ~12-14 pure-bash $(...) subshells plus several avoidable external commands per test. Individually cheap (~0.33ms per subshell on macOS bash 3.2), together ~5-7ms per test, ~7s across the suite, compounding in nested acceptance runs.

The repo already has the documented fix pattern: the dedicated-global-return-slot ("outvar") convention in .claude/rules/bash-style.md, already used by _BASHUNIT_RUNNER_FIELD_OUT and friends in src/runner.sh.

Task — checklist, one commit each

  • bashunit::runner::detect_runtime_error is called twice per test with the same input (src/runner.sh:1081 and src/runner.sh:1125): compute once, reuse; convert to a return slot.
  • retry_max=$(bashunit::env::retry_count) (src/runner.sh:1063) forks per test for a value constant across the whole run: resolve once at startup (env layer) and read a global.
  • subshell_output=$(bashunit::runner::decode_subshell_output ...) (src/runner.sh:1110): return-slot conversion (the inner base64 -d fork is covered by perf(state): avoid base64 forks for empty title and hook message in export_subshell_context #762; this item is just the wrapper subshell).
  • bashunit::helper::normalize_test_function_name (src/runner.sh:1152 -> src/helpers.sh:40) nests $(bashunit::helper::get_test_title ...) (src/helpers.sh:45): flatten to return slots.
  • bashunit::console_results::print_successful_test builds its output line via $(printf ...) (src/console_results.sh:232,243): build with printf -v-free pure-bash concatenation or a return slot (printf -v is fine per se but has the dynamic-scope caveat documented in the style rule — prefer the slot).
  • bashunit::cleanup_testcase_temp_files (src/globals.sh:67-72) runs rm -rf "$BASHUNIT_TEMP_DIR/${ID}_*" for every test even when the test never called bashunit::temp_file/temp_dir: set a flag (or counter) in bashunit::temp_file/bashunit::temp_dir (src/globals.sh:38-65) and skip the rm fork when nothing was created. Mind that the flag must live in the test subshell where the temp file was created and the cleanup runs (cleanup_on_exit, src/runner.sh:1300) — verify they share the same process.
  • bashunit::runner::generate_id (src/runner.sh:53 -> src/helpers.sh:454) and resolve_test_location (src/runner.sh:77): return-slot conversions if straightforward; skip if they require behavior changes.
  • Verify-and-decide: bashunit::helper::get_functions_to_run (src/helpers.sh:203-225) iterates the full declare -F list (~970 functions once the framework is loaded) per file. Cheap per file, but if the input can be pre-filtered to the script's own functions (_BASHUNIT_CACHED_ALL_FUNCTIONS, src/runner.sh:392), do it; otherwise note why not in the PR.

Danger zone

Bash local is dynamically scoped: a helper's local can shadow the caller's variable when returning via eval/${!name}. This exact bug caused 12 parallel failures in PR #672. Follow .claude/rules/bash-style.md (dedicated _BASHUNIT_* global slots, natural local names) and add a regression test whenever a helper takes an outvar name.

Tests first (TDD)

Behavior must not change; the existing suite is the spec. For each conversion, run the affected unit/acceptance files before and after. Add regression tests only where a new return-slot helper is introduced (call it with one of its own internal variable names, per the style rule).

Constraints

  • Bash 3.0+ only: no declare -A, no [[ ]], no ${var,,}, no negative array indexing, no &>>; printf -v is 3.1+ — avoid it.
  • 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.
  • Fork census (tracking issue) shows subshell count in the trace drops measurably; report before/after 100-noop timing in the PR.

Line references valid at commit 4d80e7c.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestrefactoringRefactoring or cleaning related

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions