🎯 Goal
Eliminate the per-test mktemp + mv (and, for data-provider tests, the extra echo | tr | sed sanitize pipeline) that every --parallel worker forks when writing its .result file. Replace the unique-filename dance with a per-suite monotonic ordinal minted in the (single-threaded) dispatch loop.
This is the single largest remaining chunk of the parallel fork budget: ~10 mktemp + 10 mv on a 10-test file today, plus up to 3 more forks per provider-argument test.
📍 Where
- Result file is written at the tail of
bashunit::runner::run_test — src/runner.sh:1595-1622:
template="${fn_name}.XXXXXX" (or ${fn_name}-${sanitized_args}.XXXXXX)unique_test_result_file=$("$MKTEMP" -p "$test_suite_dir" "$template") → forkmv "$unique_test_result_file" "${unique_test_result_file}.result" → forksanitized_args=$(echo "${args[*]}" | tr … | sed …) → 3 forks, provider tests only (src/runner.sh:1600)
- Callers loop sequentially inside one worker (one file = one worker):
bashunit::runner::call_test_functions at src/runner.sh:867 (fn loop) and :896 (provider-data loop), calling run_test at :877/:879/:907/:909. - Result files live in a per-suite subdir:
${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh} (src/runner.sh:864,1595). - Aggregation globs
*.result in that dir: bashunit::parallel::aggregate_test_results (src/runner.sh:522).
💡 Why it is safe (design)
Parallelism is per file, not per test — call_test_functions is what gets backgrounded with & (src/runner.sh:499-502). Inside a worker, tests run sequentially, and every worker writes into its own per-suite subdir. Therefore a counter that increments once per run_test call within a suite is unique within that subdir with no cross-worker collision — which is exactly why deterministic names were previously rejected (they were keyed on the sanitized test name, and different provider args can sanitize identically). An ordinal sidesteps that.
🔧 Approach
- In
call_test_functions, keep a local _test_index=0; increment before each run_test call (both the plain loop at :867 and the provider loop at :896). - Pass the index to
run_test (new trailing/leading parameter — keep the existing positional args intact; prefer a dedicated global slot or an explicit param, not reuse of $@ which carries provider data). - In
run_test's parallel tail, replace the whole template/mktemp/mv/sanitized_args block with:
local unique_test_result_file="${test_suite_dir}/${_test_index}.result"
No mktemp, no mv, no sanitize pipeline. echo "$execution_result" >"$unique_test_result_file" stays. - Confirm nothing else parses the old
fn_name/sanitized_args filename shape (aggregation only globs *.result; verify src/parallel.sh).
✅ Acceptance criteria
--parallel runs produce one <N>.result per test in the per-suite subdir; aggregation totals are byte-identical to before.- Zero
mktemp/mv forks on the per-test parallel path; zero echo|tr|sed for provider tests. - Sequential path unchanged.
🧪 TDD / measurement
- RED first: extend the parallel budget assertion in
tests/acceptance/bashunit_run_forks_test.sh to pin the new (lower) mktemp/mv counts — it must fail before the change. Use the PATH-shim census method in .claude/rules/perf-fork-budget.md (note: $MKTEMP is pinned at startup and bypasses PATH shims — shim mktemp/mv accordingly or trace). - Add/adjust a functional test asserting a 10-test file + a data-provider file still aggregate correct pass/fail counts under
--parallel. - Full gate:
./bashunit tests/ and ./bashunit --parallel --simple --strict tests/, make sa, make lint.
⛓️ Constraints
- Bash 3.0+ (
.claude/rules/bash-style.md): no [[, declare -A, ${var,,}, negative indices, BASHPID. A monotonic local counter is fine — it lives in the single worker shell, no shared-$$ problem. - Follow the return-slot pattern if threading the index via a global (
_BASHUNIT_*). - Update
.claude/rules/perf-fork-budget.md "Current budgets" table to reflect the new parallel count.
📈 Impact
Roughly halves the per-10-test-file parallel fork count; larger win on data-provider-heavy suites.
🎯 Goal
Eliminate the per-test
mktemp+mv(and, for data-provider tests, the extraecho | tr | sedsanitize pipeline) that every--parallelworker forks when writing its.resultfile. Replace the unique-filename dance with a per-suite monotonic ordinal minted in the (single-threaded) dispatch loop.This is the single largest remaining chunk of the parallel fork budget: ~10
mktemp+ 10mvon a 10-test file today, plus up to 3 more forks per provider-argument test.📍 Where
bashunit::runner::run_test—src/runner.sh:1595-1622:template="${fn_name}.XXXXXX"(or${fn_name}-${sanitized_args}.XXXXXX)unique_test_result_file=$("$MKTEMP" -p "$test_suite_dir" "$template")→ forkmv "$unique_test_result_file" "${unique_test_result_file}.result"→ forksanitized_args=$(echo "${args[*]}" | tr … | sed …)→ 3 forks, provider tests only (src/runner.sh:1600)bashunit::runner::call_test_functionsatsrc/runner.sh:867(fn loop) and:896(provider-data loop), callingrun_testat:877/:879/:907/:909.${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}(src/runner.sh:864,1595).*.resultin that dir:bashunit::parallel::aggregate_test_results(src/runner.sh:522).💡 Why it is safe (design)
Parallelism is per file, not per test —
call_test_functionsis what gets backgrounded with&(src/runner.sh:499-502). Inside a worker, tests run sequentially, and every worker writes into its own per-suite subdir. Therefore a counter that increments once perrun_testcall within a suite is unique within that subdir with no cross-worker collision — which is exactly why deterministic names were previously rejected (they were keyed on the sanitized test name, and different provider args can sanitize identically). An ordinal sidesteps that.🔧 Approach
call_test_functions, keep alocal _test_index=0; increment before eachrun_testcall (both the plain loop at:867and the provider loop at:896).run_test(new trailing/leading parameter — keep the existing positional args intact; prefer a dedicated global slot or an explicit param, not reuse of$@which carries provider data).run_test's parallel tail, replace the wholetemplate/mktemp/mv/sanitized_argsblock with:mktemp, nomv, no sanitize pipeline.echo "$execution_result" >"$unique_test_result_file"stays.fn_name/sanitized_argsfilename shape (aggregation only globs*.result; verifysrc/parallel.sh).✅ Acceptance criteria
--parallelruns produce one<N>.resultper test in the per-suite subdir; aggregation totals are byte-identical to before.mktemp/mvforks on the per-test parallel path; zeroecho|tr|sedfor provider tests.🧪 TDD / measurement
tests/acceptance/bashunit_run_forks_test.shto pin the new (lower)mktemp/mvcounts — it must fail before the change. Use the PATH-shim census method in.claude/rules/perf-fork-budget.md(note:$MKTEMPis pinned at startup and bypasses PATH shims — shimmktemp/mvaccordingly or trace).--parallel../bashunit tests/and./bashunit --parallel --simple --strict tests/,make sa,make lint.⛓️ Constraints
.claude/rules/bash-style.md): no[[,declare -A,${var,,}, negative indices,BASHPID. A monotoniclocalcounter is fine — it lives in the single worker shell, no shared-$$problem._BASHUNIT_*)..claude/rules/perf-fork-budget.md"Current budgets" table to reflect the new parallel count.📈 Impact
Roughly halves the per-10-test-file parallel fork count; larger win on data-provider-heavy suites.