diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cc78b8d..2df67a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Faster runtime-error detection: single `case` glob instead of 23-iteration loop in `detect_runtime_error` (#668) - Hot-path coverage flag now cached in `_BASHUNIT_COVERAGE_ON`, removing a function dispatch per call (#664) - Parallel runner blocks on `wait -n` on Bash 4.3+ instead of polling `jobs -r`, removing sleep-induced slot-release latency (#667) +- Hot-path result helpers (`extract_encoded_field`, `extract_subshell_type`, `format_subshell_output`, `compute_total_assertions`) use outvar pattern, dropping a fork per call per test (#662) ## [0.36.0](https://github.com/TypedDevs/bashunit/compare/0.35.0...0.36.0) - 2026-05-07 diff --git a/src/runner.sh b/src/runner.sh index 7a266141..dc071561 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -57,48 +57,71 @@ function bashunit::runner::apply_interpolated_title() { printf '%s' "$interpolated" } +# All four helpers below use the outvar pattern (first argument is the name of +# the variable to assign into) so callers can avoid the per-test $(...) subshell +# capture in the hot path. Internal locals use a `__bu_` prefix to avoid name +# collisions with caller variables passed by name. + +# Writes the value of an encoded field (##KEY=value##) into the named outvar. +# Arguments: $1 outvar name, $2 test_execution_result, $3 key function bashunit::runner::extract_encoded_field() { - local test_execution_result=$1 - local key=$2 - local marker="##${key}=" - case "$test_execution_result" in - *"$marker"*) - local rest="${test_execution_result#*"$marker"}" - printf '%s' "${rest%%##*}" + local __bu_out=$1 + local __bu_in=$2 + local __bu_key=$3 + local __bu_marker="##${__bu_key}=" + local __bu_val="" + case "$__bu_in" in + *"$__bu_marker"*) + local __bu_rest="${__bu_in#*"$__bu_marker"}" + __bu_val="${__bu_rest%%##*}" ;; - *) printf '' ;; esac + eval "$__bu_out=\$__bu_val" } +# Writes the sum of all ASSERTIONS_* counters into the named outvar. +# Arguments: $1 outvar name, $2 test_execution_result function bashunit::runner::compute_total_assertions() { - local test_execution_result=$1 - local failed passed skipped incomplete snapshot - failed="${test_execution_result##*##ASSERTIONS_FAILED=}" - failed="${failed%%##*}" - passed="${test_execution_result##*##ASSERTIONS_PASSED=}" - passed="${passed%%##*}" - skipped="${test_execution_result##*##ASSERTIONS_SKIPPED=}" - skipped="${skipped%%##*}" - incomplete="${test_execution_result##*##ASSERTIONS_INCOMPLETE=}" - incomplete="${incomplete%%##*}" - snapshot="${test_execution_result##*##ASSERTIONS_SNAPSHOT=}" - snapshot="${snapshot%%##*}" - printf '%d' "$((${failed:-0} + ${passed:-0} + ${skipped:-0} + ${incomplete:-0} + ${snapshot:-0}))" + local __bu_out=$1 + local __bu_in=$2 + local __bu_failed __bu_passed __bu_skipped __bu_incomplete __bu_snapshot + __bu_failed="${__bu_in##*##ASSERTIONS_FAILED=}" + __bu_failed="${__bu_failed%%##*}" + __bu_passed="${__bu_in##*##ASSERTIONS_PASSED=}" + __bu_passed="${__bu_passed%%##*}" + __bu_skipped="${__bu_in##*##ASSERTIONS_SKIPPED=}" + __bu_skipped="${__bu_skipped%%##*}" + __bu_incomplete="${__bu_in##*##ASSERTIONS_INCOMPLETE=}" + __bu_incomplete="${__bu_incomplete%%##*}" + __bu_snapshot="${__bu_in##*##ASSERTIONS_SNAPSHOT=}" + __bu_snapshot="${__bu_snapshot%%##*}" + local __bu_val + __bu_val=$((${__bu_failed:-0} + ${__bu_passed:-0} + ${__bu_skipped:-0})) + __bu_val=$((__bu_val + ${__bu_incomplete:-0} + ${__bu_snapshot:-0})) + eval "$__bu_out=\$__bu_val" } +# Writes the subshell type marker (text inside leading [...]) into the named outvar. +# Arguments: $1 outvar name, $2 subshell_output function bashunit::runner::extract_subshell_type() { - local subshell_output=$1 - local type="${subshell_output%%]*}" - printf '%s' "${type#[}" + local __bu_out=$1 + local __bu_in=$2 + local __bu_val="${__bu_in%%]*}" + __bu_val="${__bu_val#[}" + eval "$__bu_out=\$__bu_val" } +# Writes the subshell output (minus the leading [type] marker, with embedded +# status markers replaced by newlines) into the named outvar. +# Arguments: $1 outvar name, $2 subshell_output function bashunit::runner::format_subshell_output() { - local subshell_output=$1 - local line="${subshell_output#*]}" - line=${line//\[failed\]/$'\n'} - line=${line//\[skipped\]/$'\n'} - line=${line//\[incomplete\]/$'\n'} - printf '%s' "$line" + local __bu_out=$1 + local __bu_in=$2 + local __bu_val="${__bu_in#*]}" + __bu_val=${__bu_val//\[failed\]/$'\n'} + __bu_val=${__bu_val//\[skipped\]/$'\n'} + __bu_val=${__bu_val//\[incomplete\]/$'\n'} + eval "$__bu_out=\$__bu_val" } function bashunit::runner::detect_runtime_error() { @@ -815,8 +838,8 @@ function bashunit::runner::run_test() { if [ -n "$subshell_output" ]; then local type - type=$(bashunit::runner::extract_subshell_type "$subshell_output") - subshell_output=$(bashunit::runner::format_subshell_output "$subshell_output") + bashunit::runner::extract_subshell_type type "$subshell_output" + bashunit::runner::format_subshell_output subshell_output "$subshell_output" if ! bashunit::env::is_failures_only_enabled; then bashunit::state::print_line "$type" "$subshell_output" fi @@ -832,12 +855,12 @@ function bashunit::runner::run_test() { local test_exit_code="$_BASHUNIT_TEST_EXIT_CODE" local total_assertions - total_assertions=$(bashunit::runner::compute_total_assertions "$test_execution_result") + bashunit::runner::compute_total_assertions total_assertions "$test_execution_result" local encoded_test_title hook_failure encoded_hook_message - encoded_test_title=$(bashunit::runner::extract_encoded_field "$test_execution_result" "TEST_TITLE") - hook_failure=$(bashunit::runner::extract_encoded_field "$test_execution_result" "TEST_HOOK_FAILURE") - encoded_hook_message=$(bashunit::runner::extract_encoded_field "$test_execution_result" "TEST_HOOK_MESSAGE") + bashunit::runner::extract_encoded_field encoded_test_title "$test_execution_result" "TEST_TITLE" + bashunit::runner::extract_encoded_field hook_failure "$test_execution_result" "TEST_HOOK_FAILURE" + bashunit::runner::extract_encoded_field encoded_hook_message "$test_execution_result" "TEST_HOOK_MESSAGE" local test_title="" [ -n "$encoded_test_title" ] && test_title="$(bashunit::helper::decode_base64 "$encoded_test_title")" diff --git a/tests/unit/runner_test.sh b/tests/unit/runner_test.sh index 0e8745d9..5c1e3af0 100644 --- a/tests/unit/runner_test.sh +++ b/tests/unit/runner_test.sh @@ -127,3 +127,49 @@ function test_detect_runtime_error_matches_unexpected_eof() { assert_same "line 5: unexpected EOF while looking for matching" "$actual" } + +function test_extract_encoded_field_writes_value_to_outvar() { + local out="" + bashunit::runner::extract_encoded_field out \ + "preamble##TEST_TITLE=hello world##ASSERTIONS_PASSED=1" "TEST_TITLE" + + assert_same "hello world" "$out" +} + +function test_extract_encoded_field_writes_empty_when_key_missing() { + local out="prior" + bashunit::runner::extract_encoded_field out "##ASSERTIONS_PASSED=1" "TEST_TITLE" + + assert_empty "$out" +} + +function test_compute_total_assertions_sums_into_outvar() { + local out="" + bashunit::runner::compute_total_assertions out \ + "##ASSERTIONS_FAILED=1##ASSERTIONS_PASSED=2##ASSERTIONS_SKIPPED=3##ASSERTIONS_INCOMPLETE=4##ASSERTIONS_SNAPSHOT=5" + + assert_same "15" "$out" +} + +function test_compute_total_assertions_treats_missing_counters_as_zero() { + local out="" + bashunit::runner::compute_total_assertions out "##ASSERTIONS_PASSED=2" + + assert_same "2" "$out" +} + +function test_extract_subshell_type_strips_brackets_into_outvar() { + local out="" + bashunit::runner::extract_subshell_type out "[failed] something happened" + + assert_same "failed" "$out" +} + +function test_format_subshell_output_strips_type_and_expands_markers() { + local out="" + bashunit::runner::format_subshell_output out "[failed] line1[skipped]line2[incomplete]line3" + + local expected + expected=$' line1\nline2\nline3' + assert_same "$expected" "$out" +}