From 19355a0427d54663737c118f06317938366e8458 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Mon, 13 Jul 2026 11:19:05 +0200 Subject: [PATCH 1/4] perf(runner): resolve test labels via return slot in run_test Convert the two per-test normalize_test_function_name captures in run_test to the existing fork-free normalize_test_function_name_to_slot variant, removing a command-substitution fork on the hot path (and a second on the hook-failure path). No behaviour change. Part of #774. --- src/runner.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/runner.sh b/src/runner.sh index 18d563f6..bcc4460d 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -1186,15 +1186,16 @@ function bashunit::runner::run_test() { [ -n "$encoded_hook_message" ] && hook_message="$(bashunit::helper::decode_base64 "$encoded_hook_message")" bashunit::set_test_title "$test_title" - local label - label="$(bashunit::helper::normalize_test_function_name "$fn_name" "$interpolated_fn_name")" + bashunit::helper::normalize_test_function_name_to_slot "$fn_name" "$interpolated_fn_name" + local label=$_BASHUNIT_HELPER_NORMALIZED_OUT bashunit::state::reset_test_title bashunit::state::reset_current_test_interpolated_function_name local failure_label="$label" local failure_function="$fn_name" if [ -n "$hook_failure" ]; then - failure_label="$(bashunit::helper::normalize_test_function_name "$hook_failure")" + bashunit::helper::normalize_test_function_name_to_slot "$hook_failure" + failure_label=$_BASHUNIT_HELPER_NORMALIZED_OUT failure_function="$hook_failure" fi From 27b9bbb7ee3ff3f9b6544957d80a865d231d6b3f Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Mon, 13 Jul 2026 11:27:06 +0200 Subject: [PATCH 2/4] perf(clock): add now_to_slot fork-free clock capture for run_test Add bashunit::clock::now_to_slot writing _BASHUNIT_CLOCK_NOW_OUT and make bashunit::clock::now delegate to it (single source of truth). The shell branch reads EPOCHREALTIME directly, folding in shell_time so the Bash 5.0+ hot path pays no command-substitution fork; date-seconds forks once instead of twice. Interpreter/date branches keep one internal fork. Convert both run_test clock captures (start_time, end_time) to the slot. _choose_impl selection and the #765 auto-skip behaviour are unchanged. Part of #774. --- src/clock.sh | 40 ++++++++++++++++++++++++++++------------ src/runner.sh | 9 ++++++--- tests/unit/clock_test.sh | 27 ++++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/clock.sh b/src/clock.sh index c516e3bd..f6501b40 100644 --- a/src/clock.sh +++ b/src/clock.sh @@ -89,44 +89,55 @@ function bashunit::clock::is_expensive() { esac } -function bashunit::clock::now() { +_BASHUNIT_CLOCK_NOW_OUT="" + +# Return-slot variant of bashunit::clock::now: writes the current time in +# nanoseconds into _BASHUNIT_CLOCK_NOW_OUT. The `shell` branch reads +# EPOCHREALTIME directly (folding in shell_time) so the per-test hot path pays +# no command-substitution fork on Bash 5.0+; `date-seconds` forks once instead +# of twice. Interpreter/`date` branches keep a single internal fork. +# Returns: 0 on success, 1 when no clock implementation is available. +function bashunit::clock::now_to_slot() { if [ -z "$_BASHUNIT_CLOCK_NOW_IMPL" ]; then bashunit::clock::_choose_impl || return 1 fi case "$_BASHUNIT_CLOCK_NOW_IMPL" in perl) - perl -MTime::HiRes -e 'printf("%.0f\n", Time::HiRes::time() * 1000000000)' + _BASHUNIT_CLOCK_NOW_OUT="$(perl -MTime::HiRes -e 'printf("%.0f\n", Time::HiRes::time() * 1000000000)')" ;; python) - python - <<'EOF' + _BASHUNIT_CLOCK_NOW_OUT="$( + python - <<'EOF' import time, sys sys.stdout.write(str(int(time.time() * 1000000000))) EOF + )" ;; node) - node -e 'process.stdout.write((BigInt(Date.now()) * 1000000n).toString())' + _BASHUNIT_CLOCK_NOW_OUT="$(node -e 'process.stdout.write((BigInt(Date.now()) * 1000000n).toString())')" ;; powershell) - powershell -Command "\ + _BASHUNIT_CLOCK_NOW_OUT="$(powershell -Command "\ \$unixEpoch = [DateTime]'1970-01-01 00:00:00';\ \$now = [DateTime]::UtcNow;\ \$ticksSinceEpoch = (\$now - \$unixEpoch).Ticks;\ \$nanosecondsSinceEpoch = \$ticksSinceEpoch * 100;\ Write-Output \$nanosecondsSinceEpoch\ - " + ")" ;; date) - date +%s%N + _BASHUNIT_CLOCK_NOW_OUT="$(date +%s%N)" ;; date-seconds) local seconds seconds=$(date +%s) - echo "$((seconds * 1000000000))" + _BASHUNIT_CLOCK_NOW_OUT="$((seconds * 1000000000))" ;; shell) - # shellcheck disable=SC2155 - local shell_time="$(bashunit::clock::shell_time)" + # Read EPOCHREALTIME directly (no shell_time subshell) on the hot path; + # both '.' and ',' decimal separators are handled for locale portability. + local shell_time="${EPOCHREALTIME:-}" local seconds="${shell_time%%[.,]*}" local microseconds="${shell_time#*[.,]}" if [ "$seconds" = "$shell_time" ]; then @@ -137,15 +148,20 @@ EOF microseconds="${microseconds:0:6}" microseconds="${microseconds#"${microseconds%%[!0]*}"}" microseconds="${microseconds:-0}" - echo "$(( (seconds * 1000000000) + (microseconds * 1000) ))" + _BASHUNIT_CLOCK_NOW_OUT="$(((seconds * 1000000000) + (microseconds * 1000)))" ;; *) bashunit::clock::_choose_impl || return 1 - bashunit::clock::now + bashunit::clock::now_to_slot ;; esac } +function bashunit::clock::now() { + bashunit::clock::now_to_slot || return 1 + echo "$_BASHUNIT_CLOCK_NOW_OUT" +} + function bashunit::clock::shell_time() { # Get time directly from the shell variable EPOCHREALTIME (Bash 5+) [ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME" diff --git a/src/runner.sh b/src/runner.sh index bcc4460d..3d84aebb 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -1100,7 +1100,10 @@ function bashunit::runner::run_test() { # exactly once (on the final attempt) and nothing is double-counted. Each fork # in --parallel retries itself before writing its single .result file. while :; do - [ "$measure_duration" = true ] && start_time=$(bashunit::clock::now) + if [ "$measure_duration" = true ]; then + bashunit::clock::now_to_slot + start_time=$_BASHUNIT_CLOCK_NOW_OUT + fi if bashunit::env::is_test_timeout_enabled; then bashunit::runner::run_with_timeout "$test_file" "$fn_name" "$@" test_execution_result="$_BASHUNIT_RUNNER_EXEC_OUT" @@ -1129,8 +1132,8 @@ function bashunit::runner::run_test() { local duration=0 if [ "$measure_duration" = true ]; then - local end_time - end_time=$(bashunit::clock::now) + bashunit::clock::now_to_slot + local end_time=$_BASHUNIT_CLOCK_NOW_OUT duration=$(((end_time - start_time) / 1000000)) fi diff --git a/tests/unit/clock_test.sh b/tests/unit/clock_test.sh index d72e2b9d..7ebac848 100644 --- a/tests/unit/clock_test.sh +++ b/tests/unit/clock_test.sh @@ -100,13 +100,34 @@ function test_now_on_osx_without_perl() { mock_macos bashunit::mock bashunit::dependencies::has_perl mock_false - bashunit::mock bashunit::clock::shell_time <<<"1727708708.326957" + local EPOCHREALTIME="1727708708.326957" bashunit::mock bashunit::dependencies::has_python mock_false bashunit::mock bashunit::dependencies::has_node mock_false assert_same "1727708708326957000" "$(bashunit::clock::now)" } +function test_now_to_slot_shell_branch_computes_from_epochrealtime() { + local EPOCHREALTIME="1727708708.326957" + + bashunit::clock::now_to_slot + + assert_same "1727708708326957000" "$_BASHUNIT_CLOCK_NOW_OUT" +} + +function test_now_to_slot_date_seconds_branch() { + mock_unknown_linux_os + bashunit::mock perl mock_non_existing_fn + bashunit::mock bashunit::dependencies::has_python mock_false + bashunit::mock bashunit::dependencies::has_node mock_false + bashunit::mock bashunit::clock::shell_time mock_non_existing_fn + bashunit::mock date 'mock_date_seconds "$@"' + + bashunit::clock::now_to_slot + + assert_same "1727768951000000000" "$_BASHUNIT_CLOCK_NOW_OUT" +} + function test_runtime_in_milliseconds_when_not_empty_time() { bashunit::mock perl <<<"1720705883457" bashunit::mock bashunit::dependencies::has_python mock_false @@ -116,7 +137,7 @@ function test_runtime_in_milliseconds_when_not_empty_time() { } function test_now_prefers_shell_time_over_perl() { - bashunit::mock bashunit::clock::shell_time <<<"1234.567890" + local EPOCHREALTIME="1234.567890" bashunit::mock perl <<<"999999999999" bashunit::mock bashunit::dependencies::has_python mock_false bashunit::mock bashunit::dependencies::has_node mock_false @@ -125,7 +146,7 @@ function test_now_prefers_shell_time_over_perl() { } function test_now_handles_shell_time_with_comma_decimal_separator() { - bashunit::mock bashunit::clock::shell_time <<<"1234,567890" + local EPOCHREALTIME="1234,567890" assert_same "1234567890000" "$(bashunit::clock::now)" } From a187bd0774597c47a381fdf56537805c6d2a2d91 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Mon, 13 Jul 2026 11:36:04 +0200 Subject: [PATCH 3/4] perf(parallel): resolve parallel mode once into a global read bashunit::parallel::is_enabled was re-evaluating the env layer plus four OS checks (and logging) on every call, several times per test. Resolve it once after arg parsing via bashunit::parallel::resolve_enabled into _BASHUNIT_PARALLEL_ENABLED; is_enabled is now a pure global read that falls back to a fresh, uncached compute only when unresolved (e.g. unit tests that toggle env directly). The internal_log now fires once, not per call. Part of #774. --- src/main.sh | 4 ++++ src/parallel.sh | 37 ++++++++++++++++++++++++------ tests/unit/console_results_test.sh | 9 ++++++++ tests/unit/parallel_test.sh | 34 +++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/main.sh b/src/main.sh index 6aabd7d7..a4f92d51 100644 --- a/src/main.sh +++ b/src/main.sh @@ -751,6 +751,10 @@ function bashunit::main::exec_tests() { trap 'bashunit::main::cleanup' SIGINT trap '[ $? -eq $EXIT_CODE_STOP_ON_FAILURE ] && bashunit::main::handle_stop_on_failure_sync' EXIT + # Resolve parallel mode once now that --parallel/--no-parallel are parsed, so + # the per-test is_enabled reads a global instead of re-checking env + OS. + bashunit::parallel::resolve_enabled + if bashunit::env::is_parallel_run_enabled && ! bashunit::parallel::is_enabled; then printf "%sWarning: Parallel tests are supported on macOS, Ubuntu and Windows.\n" "${_BASHUNIT_COLOR_INCOMPLETE}" printf "For other OS (like Alpine), --parallel is not enabled due to inconsistent results,\n" diff --git a/src/parallel.sh b/src/parallel.sh index 1edafe20..9ea33529 100755 --- a/src/parallel.sh +++ b/src/parallel.sh @@ -143,14 +143,37 @@ function bashunit::parallel::init() { mkdir -p "$TEMP_DIR_PARALLEL_TEST_SUITE" } -function bashunit::parallel::is_enabled() { - bashunit::internal_log "bashunit::parallel::is_enabled" \ - "requested:$BASHUNIT_PARALLEL_RUN" "os:${_BASHUNIT_OS:-Unknown}" +# Cached result of resolve_enabled ("true"/"false"); empty until resolved. +_BASHUNIT_PARALLEL_ENABLED="" - if bashunit::env::is_parallel_run_enabled && +# Pure predicate: parallel requested AND running on a supported OS. No caching +# or logging, so it is safe to call fresh (e.g. from unit tests). +function bashunit::parallel::_compute_enabled() { + bashunit::env::is_parallel_run_enabled && (bashunit::check_os::is_macos || bashunit::check_os::is_ubuntu || - bashunit::check_os::is_alpine || bashunit::check_os::is_windows); then - return 0 + bashunit::check_os::is_alpine || bashunit::check_os::is_windows) +} + +# Resolve parallel mode once (after arg parsing) into _BASHUNIT_PARALLEL_ENABLED +# so is_enabled becomes a pure global read on the per-test hot path. The env + +# OS checks are constant for the whole run. +function bashunit::parallel::resolve_enabled() { + if bashunit::parallel::_compute_enabled; then + _BASHUNIT_PARALLEL_ENABLED=true + else + _BASHUNIT_PARALLEL_ENABLED=false fi - return 1 + bashunit::internal_log "bashunit::parallel::resolve_enabled" \ + "requested:$BASHUNIT_PARALLEL_RUN" "os:${_BASHUNIT_OS:-Unknown}" \ + "enabled:$_BASHUNIT_PARALLEL_ENABLED" +} + +function bashunit::parallel::is_enabled() { + case "$_BASHUNIT_PARALLEL_ENABLED" in + true) return 0 ;; + false) return 1 ;; + esac + # Not resolved yet (e.g. unit tests call is_enabled directly): compute fresh + # without caching so per-test env/OS changes are still honoured. + bashunit::parallel::_compute_enabled } diff --git a/tests/unit/console_results_test.sh b/tests/unit/console_results_test.sh index 7e9cfed3..8d886eb9 100644 --- a/tests/unit/console_results_test.sh +++ b/tests/unit/console_results_test.sh @@ -669,6 +669,9 @@ function test_print_hook_completed_output_milliseconds() { local original_parallel_run=$BASHUNIT_PARALLEL_RUN export BASHUNIT_SIMPLE_OUTPUT=false export BASHUNIT_PARALLEL_RUN=false + # Recompute is_enabled from this test's env, not a cache inherited from a + # --parallel meta-run of the suite. + _BASHUNIT_PARALLEL_ENABLED="" export TERMINAL_WIDTH=80 local output @@ -685,6 +688,9 @@ function test_print_hook_completed_output_seconds() { local original_parallel_run=$BASHUNIT_PARALLEL_RUN export BASHUNIT_SIMPLE_OUTPUT=false export BASHUNIT_PARALLEL_RUN=false + # Recompute is_enabled from this test's env, not a cache inherited from a + # --parallel meta-run of the suite. + _BASHUNIT_PARALLEL_ENABLED="" export TERMINAL_WIDTH=80 local output @@ -701,6 +707,9 @@ function test_print_hook_completed_output_minutes() { local original_parallel_run=$BASHUNIT_PARALLEL_RUN export BASHUNIT_SIMPLE_OUTPUT=false export BASHUNIT_PARALLEL_RUN=false + # Recompute is_enabled from this test's env, not a cache inherited from a + # --parallel meta-run of the suite. + _BASHUNIT_PARALLEL_ENABLED="" export TERMINAL_WIDTH=80 local output diff --git a/tests/unit/parallel_test.sh b/tests/unit/parallel_test.sh index c190bd1b..28adbfbf 100644 --- a/tests/unit/parallel_test.sh +++ b/tests/unit/parallel_test.sh @@ -12,6 +12,9 @@ function set_up_before_script() { function set_up() { original_parallel_run=$BASHUNIT_PARALLEL_RUN export BASHUNIT_PARALLEL_RUN=true + # Start each test unresolved so is_enabled recomputes from the per-test + # env/OS mocks (the global persists across tests in this no-parallel file). + _BASHUNIT_PARALLEL_ENABLED="" # Create isolated temp directory for tests _TEST_TEMP_DIR=$(mktemp -d) @@ -132,6 +135,37 @@ function test_parallel_disabled_on_unsupported_os() { assert_general_error "$(bashunit::parallel::is_enabled)" } +function test_resolve_enabled_caches_true_for_supported_os() { + _BASHUNIT_PARALLEL_ENABLED="" + bashunit::mock bashunit::check_os::is_macos mock_true + + bashunit::parallel::resolve_enabled + + assert_same "true" "$_BASHUNIT_PARALLEL_ENABLED" +} + +function test_resolve_enabled_caches_false_when_env_off() { + _BASHUNIT_PARALLEL_ENABLED="" + export BASHUNIT_PARALLEL_RUN=false + bashunit::mock bashunit::check_os::is_macos mock_true + + bashunit::parallel::resolve_enabled + + assert_same "false" "$_BASHUNIT_PARALLEL_ENABLED" +} + +function test_is_enabled_reads_resolved_global_without_recomputing() { + # A cached "true" wins even when the live OS checks would say false, + # proving is_enabled is a pure global read once resolved. + _BASHUNIT_PARALLEL_ENABLED=true + bashunit::mock bashunit::check_os::is_windows mock_false + bashunit::mock bashunit::check_os::is_macos mock_false + bashunit::mock bashunit::check_os::is_ubuntu mock_false + bashunit::mock bashunit::check_os::is_alpine mock_false + + assert_successful_code "$(bashunit::parallel::is_enabled)" +} + # === init/cleanup tests === function test_init_creates_temp_directory() { From 7e7698e381cb8dfeda55c4de5bf89f7ee84668b5 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Mon, 13 Jul 2026 11:43:44 +0200 Subject: [PATCH 4/4] perf(runner): detect no-parallel-tests opt-out in the single file scan Fold the per-file `grep -q '^# bashunit: no-parallel-tests'` probe into the existing build_provider_map awk pass, exposing the result via _BASHUNIT_PROVIDER_MAP_NO_PARALLEL. Removes one grep fork per test file. Also add the CHANGELOG entry for the #774 hot-path work. No behaviour change. Closes #774. --- CHANGELOG.md | 1 + src/helpers.sh | 13 +++++++++++++ src/runner.sh | 12 ++++++------ tests/unit/helpers_test.sh | 22 ++++++++++++++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bdadf29..ddd35f17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `--jobs auto` (and `-j auto`) caps parallel concurrency at the detected CPU core count, portable across Linux/macOS/BSD (`nproc`, then `sysctl`, then `getconf`, falling back to 4). The default stays unlimited (`--jobs 0`) (#766) ### Changed +- Faster test execution: removed the remaining `run_test` hot-path forks. Test-label normalization and the per-test clock capture use fork-free return slots (`bashunit::clock::now_to_slot` reads `EPOCHREALTIME` directly on Bash 5.0+), parallel mode is resolved once instead of re-checking env + OS on every `is_enabled` call, and the `no-parallel-tests` opt-out is detected in the existing single-pass file scan rather than a separate per-file `grep`. No behaviour change (#774) - Faster test execution: removed the remaining per-assertion subshell forks. `assert_equals`/`assert_not_equals` strip ANSI via a return slot (2 fewer forks each on the success path), failure-path label resolution (`assert::label`, `fail`, `handle_bool_assertion_failure`) uses fork-free return slots, and `assert_true`/`assert_false` detect aliases with the `alias` builtin instead of `command -v | grep`. 100x10 `assert_equals` ~1.50s -> ~0.76s on bash 3.2. No behaviour change (#772) - Faster test execution: removed avoidable subshell forks from the per-test hot path (runtime-error detection, subshell-output decode, name normalization, test-id generation, retry-count lookup, and the passing-test line are now fork-free), and per-test temp-file cleanup no longer forks `rm` when the test created none (`rm` 102 -> 2 on a 100-test file). No behaviour change (#764) - Per-test execution time now defaults to `auto` (`BASHUNIT_SHOW_EXECUTION_TIME=true|false|auto`): shown when the shell has a fork-free clock (Bash 5.0+, GNU `date`), hidden when measuring would fork an interpreter (e.g. Bash 3.2 on macOS, which falls back to `perl`). This removes ~2 `perl` forks per test on those shells. `--profile`, `--verbose`, reports, and `=true` still measure. See `adrs/adr-008-auto-skip-per-test-timing.md` (#765) diff --git a/src/helpers.sh b/src/helpers.sh index 3937fd12..081022ab 100755 --- a/src/helpers.sh +++ b/src/helpers.sh @@ -342,6 +342,9 @@ _BASHUNIT_PROVIDER_MAP_SCRIPT="" _BASHUNIT_PROVIDER_MAP_FNS=() _BASHUNIT_PROVIDER_MAP_PROVIDERS=() _BASHUNIT_PROVIDER_FN_OUT="" +# Set true when the scanned file carries the "# bashunit: no-parallel-tests" +# opt-out; detected in the same awk pass to avoid a per-file grep fork (#774). +_BASHUNIT_PROVIDER_MAP_NO_PARALLEL=false # # Resolves a script path, applying the issue #529 working-dir fallback. @@ -377,6 +380,7 @@ function bashunit::helper::build_provider_map() { _BASHUNIT_PROVIDER_MAP_SCRIPT="$1" _BASHUNIT_PROVIDER_MAP_FNS=() _BASHUNIT_PROVIDER_MAP_PROVIDERS=() + _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=false return fi @@ -387,18 +391,26 @@ function bashunit::helper::build_provider_map() { _BASHUNIT_PROVIDER_MAP_SCRIPT="$script" _BASHUNIT_PROVIDER_MAP_FNS=() _BASHUNIT_PROVIDER_MAP_PROVIDERS=() + _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=false local count=0 local fn provider # Single awk pass emits "\t" for every function whose # definition is at most two lines below a `# @data_provider` (or # `# data_provider`) annotation, mirroring the previous grep -B2 + sed. + # A reserved sentinel fn name carries the no-parallel-tests flag out of the + # single awk pass; real fn names are identifiers so they never collide. while IFS=$'\t' read -r fn provider; do [ -z "$fn" ] && continue + if [ "$fn" = "@@no_parallel@@" ]; then + [ "$provider" = "1" ] && _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=true + continue + fi _BASHUNIT_PROVIDER_MAP_FNS[count]="$fn" _BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider" count=$((count + 1)) done < <(awk ' + /^# bashunit: no-parallel-tests/ { no_parallel = 1; next } /^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ { p = $0 sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p) @@ -420,6 +432,7 @@ function bashunit::helper::build_provider_map() { pending = "" } } + END { printf "@@no_parallel@@\t%d\n", no_parallel } ' "$script" 2>/dev/null) } diff --git a/src/runner.sh b/src/runner.sh index 3d84aebb..1af6a875 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -764,20 +764,20 @@ function bashunit::runner::call_test_functions() { bashunit::helper::check_duplicate_functions "$script" || true - # Check if test file opts out of test-level parallelism - local allow_test_parallel=true - if grep -q "^# bashunit: no-parallel-tests" "$script" 2>/dev/null; then - allow_test_parallel=false - fi - local -a provider_data=() local provider_data_count=0 local -a parsed_data=() local parsed_data_count=0 # Scan the file once; per-test provider lookups below are pure-bash (#763). + # The same pass also detects the no-parallel-tests opt-out (#774). bashunit::helper::build_provider_map "$script" + local allow_test_parallel=true + if [ "$_BASHUNIT_PROVIDER_MAP_NO_PARALLEL" = true ]; then + allow_test_parallel=false + fi + for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then break diff --git a/tests/unit/helpers_test.sh b/tests/unit/helpers_test.sh index 3a73af5c..c57e0a2d 100644 --- a/tests/unit/helpers_test.sh +++ b/tests/unit/helpers_test.sh @@ -286,6 +286,28 @@ function test_provider_map_returns_empty_for_unreadable_script() { "$(provider_for "/no/such/path/nope_test.sh" "test_with_at_annotation")" } +function test_build_provider_map_flags_no_parallel_marker() { + local file + file="$(mktemp)" + printf '#!/usr/bin/env bash\n# bashunit: no-parallel-tests\nfunction test_foo() { :; }\n' >"$file" + + bashunit::helper::build_provider_map "$file" + + assert_same "true" "$_BASHUNIT_PROVIDER_MAP_NO_PARALLEL" + rm -f "$file" +} + +function test_build_provider_map_no_parallel_marker_defaults_false() { + local file + file="$(mktemp)" + printf '#!/usr/bin/env bash\nfunction test_foo() { :; }\n' >"$file" + + bashunit::helper::build_provider_map "$file" + + assert_same "false" "$_BASHUNIT_PROVIDER_MAP_NO_PARALLEL" + rm -f "$file" +} + function test_left_trim() { assert_same "foo" "$(bashunit::helper::trim " foo")" }