Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- `--jobs auto` / `-j auto` caps parallel concurrency at the CPU core count (portable across Linux/macOS/BSD); the default stays unlimited (#766)

### Changed
- Faster test runs: each test's subshell result payload is emitted with `printf` (a builtin) instead of a `cat <<EOF` heredoc, removing one `cat` fork per test. No behaviour change
- Faster cold start: the run-output scratch directory and the shared temp directory are created with a single `mkdir -p` call instead of one fork each (2 -> 1 `mkdir` forks per cold start). No behaviour change
- Faster test runs: the runner detects a failed source (syntax error / unexpected EOF) and discovery detects the `.bash` pattern variant with shell `case` matches instead of `grep` forks (2 -> 0 `grep` forks per sourced test file). No behaviour change
- Faster cold start: the base64 `-w` capability is detected with a shell `case` on `base64 --help` instead of piping into a `grep` fork (1 -> 0 `grep` forks per cold start). Same detection, no behaviour change
Expand Down
10 changes: 7 additions & 3 deletions src/state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,11 @@ function bashunit::state::export_subshell_context() {
bashunit::state::encode_field "$_BASHUNIT_TEST_HOOK_MESSAGE"
encoded_test_hook_message=$_BASHUNIT_STATE_ENCODED_OUT

cat <<EOF
# Emit the encoded result payload with `printf` (a builtin) instead of a
# `cat <<EOF` heredoc: this runs once per test, so avoiding the fork removes
# one process per test. The `\`-continued string keeps the per-field layout
# and produces the exact same single line the heredoc did.
local payload="\
##ASSERTIONS_FAILED=$_BASHUNIT_ASSERTIONS_FAILED\
##ASSERTIONS_PASSED=$_BASHUNIT_ASSERTIONS_PASSED\
##ASSERTIONS_SKIPPED=$_BASHUNIT_ASSERTIONS_SKIPPED\
Expand All @@ -272,8 +276,8 @@ function bashunit::state::export_subshell_context() {
##TEST_HOOK_FAILURE=$_BASHUNIT_TEST_HOOK_FAILURE\
##TEST_HOOK_MESSAGE=$encoded_test_hook_message\
##TEST_TITLE=$encoded_test_title\
##TEST_OUTPUT=$encoded_test_output##
EOF
##TEST_OUTPUT=$encoded_test_output##"
printf '%s\n' "$payload"
}

function bashunit::state::calculate_total_assertions() {
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/bashunit_run_forks_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,31 @@ function test_running_a_test_file_does_not_fork_grep() {

assert_equals 0 "$grep_forks"
}

# Regression guard: exporting a test's subshell result must not fork `cat`. It
# used to emit the encoded result payload with a `cat <<EOF` heredoc β€” one fork
# per test β€” which `printf` (a builtin) does without forking. Four independent
# tests must therefore fork `cat` far fewer than four times.
function test_running_tests_does_not_fork_cat_per_test() {
if bashunit::check_os::is_windows; then
bashunit::skip "process tracing is unreliable under Git Bash" && return
fi

local dir
dir="$(bashunit::temp_dir)"
local fixture="$dir/cat_forks_test.sh"
{
echo 'function test_a() { assert_true true; }'
echo 'function test_b() { assert_true true; }'
echo 'function test_c() { assert_true true; }'
echo 'function test_d() { assert_true true; }'
} >"$fixture"

local trace
trace="$(PS4='+ ' bash -x ./bashunit --no-parallel "$fixture" 2>&1 >/dev/null)"

local cat_forks
cat_forks="$(printf '%s\n' "$trace" | grep -cE '^\++ +/?[a-z/]*cat( |$)' || true)"

assert_less_or_equal_than 1 "$cat_forks"
}
Loading