From dcd25ef5387c9757cacf1689d93c8374727c7204 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Wed, 15 Jul 2026 23:58:51 +0200 Subject: [PATCH] perf(runner): match source/pattern probes with case, drop 2 grep forks Two per-file grep forks in the run path became shell case matches: - runner: scanning sourcing stderr for 'syntax error'/'unexpected EOF' - discovery: deciding whether to also match the .bash pattern variant 2 -> 0 grep forks per sourced test file; matters across the acceptance suite's ~258 nested runs. No behaviour change. --- CHANGELOG.md | 1 + src/helpers.sh | 12 ++++----- src/runner.sh | 13 ++++++++-- tests/acceptance/bashunit_run_forks_test.sh | 28 +++++++++++++++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 tests/acceptance/bashunit_run_forks_test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 916591d7..ecdbe1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: 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 - Faster cold start: selecting the `perl` clock reads the time once instead of an empty `Time::HiRes` probe followed by a separate first read (2 -> 1 `perl` fork per cold start on bash 3.2 / macOS). No behaviour change - Faster cold start: the deferred-output scratch files (failures/skipped/incomplete/risky/profile/rerun) now share one run-unique directory instead of forking `mktemp` once each (6 -> 0 `mktemp` forks per cold start; ~62ms -> ~50ms per nested run on bash 3.2). No behaviour change (#798) diff --git a/src/helpers.sh b/src/helpers.sh index 998d4363..2e6492c9 100755 --- a/src/helpers.sh +++ b/src/helpers.sh @@ -300,13 +300,13 @@ function bashunit::helper::find_files_recursive() { local path="${1%%/}" local pattern="${2:-*[tT]est.sh}" + # When the pattern targets *.sh test files, also match the .bash variant. Both + # the plain `*test.sh` and the default glob `*[tT]est.sh` end in `.sh`; a case + # match on either (no grep fork) is enough to decide. local alt_pattern="" - local _re='\[tT\]est\.sh$' - local _pattern_match=false - case "$pattern" in *test.sh) _pattern_match=true ;; esac - if [ "$_pattern_match" = true ] || [ "$(echo "$pattern" | "$GREP" -cE "$_re" || true)" -gt 0 ]; then - alt_pattern="${pattern%.sh}.bash" - fi + case "$pattern" in + *test.sh | *'[tT]est.sh') alt_pattern="${pattern%.sh}.bash" ;; + esac local _has_glob=false case "$path" in *"*"*) _has_glob=true ;; esac diff --git a/src/runner.sh b/src/runner.sh index 298352e6..fdc67470 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -402,8 +402,17 @@ function bashunit::runner::load_test_files() { source_err="$(cat "$source_err_file")" fi rm -f "$source_err_file" - if [ "$source_status" -ne 0 ] || [ "$(printf '%s' "$source_err" | - "$GREP" -cE 'syntax error|unexpected EOF' || true)" -gt 0 ]; then + # A non-zero source status, or a syntax-error line on stderr, means the file + # failed to load. Match the captured stderr with `case` (no grep fork). + local source_failed=false + if [ "$source_status" -ne 0 ]; then + source_failed=true + else + case "$source_err" in + *"syntax error"* | *"unexpected EOF"*) source_failed=true ;; + esac + fi + if [ "$source_failed" = true ]; then local message="$source_err" [ -z "$message" ] && message="Failed to source '$test_file' (exit $source_status)" bashunit::runner::record_file_hook_failure \ diff --git a/tests/acceptance/bashunit_run_forks_test.sh b/tests/acceptance/bashunit_run_forks_test.sh new file mode 100644 index 00000000..3e1c89f3 --- /dev/null +++ b/tests/acceptance/bashunit_run_forks_test.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Regression guard for the per-file run path. Running a test file used to fork +# `grep` twice: once in the runner to scan sourcing stderr for "syntax error"/ +# "unexpected EOF", and once in discovery to decide whether to also match the +# `.bash` variant of the test pattern. Both are shell `case` matches now, so a +# plain run forks `grep` zero times — this matters across the acceptance suite's +# ~258 nested runs, each of which sources at least one test file. +function test_running_a_test_file_does_not_fork_grep() { + 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/grep_forks_test.sh" + printf 'function test_ok() { assert_true true; }\n' >"$fixture" + + local trace + trace="$(PS4='+ ' bash -x ./bashunit --no-parallel "$fixture" 2>&1 >/dev/null)" + + # Count real `grep` process executions (resolved absolute path with args). + local grep_forks + grep_forks="$(printf '%s\n' "$trace" | grep -cE '^\++ +/[^ ]*grep ' || true)" + + assert_equals 0 "$grep_forks" +}