diff --git a/CHANGELOG.md b/CHANGELOG.md index a2d42ef6..20f73ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,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 data-provider map is built once per file in the main shell (the header's test count reads a return slot instead of a `$(...)` capture, so the cache survives for the runner) — one awk scan per file instead of two - Faster test runs: stripping ANSI codes from short colored strings is pure bash now, so aligning the per-test execution time (shown on systems with a fork-free clock, e.g. Linux) no longer forks `sed` once per passing test - Faster parallel runs: publishing each test's result file no longer forks `basename`, `mkdir` and an `echo | tr | sed` pipeline per test — the suite dir name is parameter expansion, the dir is pre-created once per file before workers spawn, and arg sanitizing is skipped without provider args (a 10-test parallel run dropped from 61 to 21 forks). No behaviour change - Faster test runs: listing all defined functions uses the `compgen -A function` builtin instead of forking `declare -F | awk` (three call sites; 5 -> 3 `awk` forks per test file). No behaviour change diff --git a/src/console_header.sh b/src/console_header.sh index 44ce4fb1..9ffee1d2 100644 --- a/src/console_header.sh +++ b/src/console_header.sh @@ -39,7 +39,11 @@ function bashunit::console_header::print_version() { # Skip counting in parallel+simple mode for faster startup total_tests=0 else - total_tests=$(bashunit::helper::find_total_tests "$filter" "$@") + # Read via the return slot, not $(...): the capture subshell would discard + # the provider-map cache find_total_tests builds per file, forcing the + # runner to re-scan each file with a second awk fork. + bashunit::helper::find_total_tests "$filter" "$@" >/dev/null + total_tests=$_BASHUNIT_HELPER_TOTAL_TESTS_OUT fi if bashunit::env::is_header_ascii_art_enabled; then diff --git a/src/helpers.sh b/src/helpers.sh index 0d299c12..c8db3917 100755 --- a/src/helpers.sh +++ b/src/helpers.sh @@ -526,10 +526,15 @@ function bashunit::helper::get_latest_tag() { head -n 1 } +# Also written by find_total_tests so a main-shell caller can read the count +# without a $() capture (which would discard the provider-map cache built here). +_BASHUNIT_HELPER_TOTAL_TESTS_OUT=0 + function bashunit::helper::find_total_tests() { local filter=${1:-} shift || true + _BASHUNIT_HELPER_TOTAL_TESTS_OUT=0 if [ $# -eq 0 ]; then echo 0 return @@ -543,6 +548,12 @@ function bashunit::helper::find_total_tests() { continue fi + # Build the provider map in THIS shell before the counting subshell: the + # subshell inherits it (its own build call becomes a cache hit), and when + # the caller runs in the main shell the runner's later build for the same + # file is a cache hit too — one awk scan per file instead of two. + bashunit::helper::build_provider_map "$file" + local file_count file_count=$( ( # shellcheck source=/dev/null @@ -590,6 +601,7 @@ function bashunit::helper::find_total_tests() { total_count=$((total_count + file_count)) done + _BASHUNIT_HELPER_TOTAL_TESTS_OUT=$total_count echo "$total_count" } diff --git a/tests/acceptance/bashunit_run_forks_test.sh b/tests/acceptance/bashunit_run_forks_test.sh index f8703a56..e07b81e8 100644 --- a/tests/acceptance/bashunit_run_forks_test.sh +++ b/tests/acceptance/bashunit_run_forks_test.sh @@ -126,8 +126,9 @@ function test_running_a_test_file_does_not_fork_sort() { # Regression guard: listing all defined functions must use the `compgen -A # function` builtin, not a `declare -F | awk` fork. The remaining awk budget of -# a plain run is the per-file file scans (data-provider map, which runs in both -# the counting subshell and the runner, plus the duplicate-name check). +# a single-file run is one data-provider scan (built once in the main shell so +# the header-count subshell and the runner both hit the cache) plus the +# duplicate-name check. function test_running_a_test_file_stays_within_the_awk_fork_budget() { if bashunit::check_os::is_windows; then bashunit::skip "PATH shims are unreliable under Git Bash" && return @@ -155,7 +156,7 @@ function test_running_a_test_file_stays_within_the_awk_fork_budget() { awk_forks="$(grep -c . "$count_file" || true)" fi - assert_less_or_equal_than 3 "$awk_forks" + assert_less_or_equal_than 2 "$awk_forks" } # Regression guard: a run must remove its run-output scratch directory on exit.