From f32a530053896c1242b42728faa0092dd6892415 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 2 Oct 2026 17:28:50 +0200 Subject: [PATCH] perf(runner): reuse unchanged file metadata Closes #1372 --- CHANGELOG.md | 1 + src/helper/discovery.sh | 21 +- src/helper/provider.sh | 144 ++++++++---- tests/acceptance/bashunit_run_forks_test.sh | 70 +++++- tests/unit/helper/helper_test.sh | 246 ++++++++++++++++++++ 5 files changed, 426 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc915cb90..f5255f90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Changed +- Sequential multi-file runs reuse provider and annotation metadata from header counting for unchanged files up to 512 characters (#1372) - Parallel reports collect worker results without per-row base64 processes, preserving values and completion order (#1371) - Data providers pass arguments without per-argument base64 processes, preserving quoting, empty values and parser isolation (#1370) - JSON reports write ordinary filenames, test names and empty messages without per-field processes, preserving existing escaping (#1369) diff --git a/src/helper/discovery.sh b/src/helper/discovery.sh index 75113303c..e7ef08080 100644 --- a/src/helper/discovery.sh +++ b/src/helper/discovery.sh @@ -279,15 +279,23 @@ function bashunit::helper::find_total_tests() { local total_count=0 local file + if [ "$#" -gt 1 ]; then + _BASHUNIT_PROVIDER_HEADER_CACHE_PATHS=() + _BASHUNIT_PROVIDER_HEADER_CACHE_CONTENTS=() + _BASHUNIT_PROVIDER_HEADER_CACHE_ROWS=() + _BASHUNIT_PROVIDER_HEADER_CACHE_NEXT=0 + # Parallel file workers inherit separate cache cursors. + if [ "${_BASHUNIT_PARALLEL_ENABLED:-false}" != true ]; then + _BASHUNIT_PROVIDER_HEADER_CACHE_SEEDING=true + fi + fi + for file in "$@"; do if [ ! -f "$file" ]; then 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. + # Build in this shell so the isolated counting path inherits the metadata. bashunit::helper::build_provider_map "$file" if bashunit::helper::_can_count_statically; then @@ -298,6 +306,11 @@ function bashunit::helper::find_total_tests() { total_count=$((total_count + _BASHUNIT_HELPER_FILE_COUNT_OUT)) done + _BASHUNIT_PROVIDER_HEADER_CACHE_SEEDING=false + if [ "${#_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[@]}" -gt 0 ]; then + _BASHUNIT_PROVIDER_MAP_SCRIPT="" + fi + _BASHUNIT_HELPER_TOTAL_TESTS_OUT=$total_count echo "$total_count" } diff --git a/src/helper/provider.sh b/src/helper/provider.sh index b12417ed9..345e892de 100644 --- a/src/helper/provider.sh +++ b/src/helper/provider.sh @@ -2,6 +2,21 @@ # @data_provider resolution and the per-script provider map. +_BASHUNIT_PROVIDER_HEADER_CACHE_SEEDING=false +_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS=() +_BASHUNIT_PROVIDER_HEADER_CACHE_CONTENTS=() +_BASHUNIT_PROVIDER_HEADER_CACHE_ROWS=() +_BASHUNIT_PROVIDER_HEADER_CACHE_NEXT=0 +_BASHUNIT_PROVIDER_SHORT_CONTENT_OUT="" + +# EOF completes the snapshot; a NUL or the size limit makes it unusable. +function bashunit::helper::_read_short_provider_script() { + _BASHUNIT_PROVIDER_SHORT_CONTENT_OUT="" + [ -r "$1" ] || return 1 + IFS= read -r -n 513 -d '' _BASHUNIT_PROVIDER_SHORT_CONTENT_OUT 2>/dev/null <"$1" && return 1 + return 0 +} + # # Resolves a script path, applying the issue #529 working-dir fallback. # Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable). @@ -22,8 +37,7 @@ function bashunit::helper::_resolve_provider_script() { # -# Scans a script once and caches its test-function -> provider-function pairs. -# Memoized by resolved path, so repeated calls for the same file do not rescan. +# Scans provider pairs, annotations and test discovery metadata for the current script. # Arguments: $1 - path to the test script # function bashunit::helper::build_provider_map() { @@ -45,6 +59,38 @@ function bashunit::helper::build_provider_map() { return fi + local scan_rows="" cache_index="" index cache_size=${#_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[@]} + if [ "$_BASHUNIT_PROVIDER_HEADER_CACHE_SEEDING" != true ] && [ "$cache_size" -gt 0 ]; then + index=$_BASHUNIT_PROVIDER_HEADER_CACHE_NEXT + if [ "${_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[index]:-}" != "$script" ]; then + index="" + local candidate + for candidate in "${!_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[@]}"; do + if [ "${_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[candidate]}" = "$script" ]; then + index=$candidate + break + fi + done + fi + if [ -n "$index" ]; then + if bashunit::helper::_read_short_provider_script "$script" && + [ "$_BASHUNIT_PROVIDER_SHORT_CONTENT_OUT" = "${_BASHUNIT_PROVIDER_HEADER_CACHE_CONTENTS[index]}" ]; then + scan_rows="${_BASHUNIT_PROVIDER_HEADER_CACHE_ROWS[index]}" + cache_index=$index + fi + _BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[index]="" + while [ "$_BASHUNIT_PROVIDER_HEADER_CACHE_NEXT" -lt "$cache_size" ] && + [ -z "${_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[_BASHUNIT_PROVIDER_HEADER_CACHE_NEXT]:-}" ]; do + _BASHUNIT_PROVIDER_HEADER_CACHE_NEXT=$((_BASHUNIT_PROVIDER_HEADER_CACHE_NEXT + 1)) + done + if [ "$_BASHUNIT_PROVIDER_HEADER_CACHE_NEXT" -eq "$cache_size" ]; then + _BASHUNIT_PROVIDER_HEADER_CACHE_PATHS=() + _BASHUNIT_PROVIDER_HEADER_CACHE_CONTENTS=() + _BASHUNIT_PROVIDER_HEADER_CACHE_ROWS=() + fi + fi + fi + _BASHUNIT_PROVIDER_MAP_SCRIPT="$script" _BASHUNIT_PROVIDER_MAP_FNS=() _BASHUNIT_PROVIDER_MAP_PROVIDERS=() @@ -56,51 +102,14 @@ function bashunit::helper::build_provider_map() { local count=0 local fn provider annot_timeout annot_retry annot_skip annot_reason - # 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. - # - # The per-test `# @timeout` / `# @retry` / `# @skip` markers ride on this same - # pass, as "@@annot@@" rows: this scan already visits every file exactly once - # in the main shell, and the fork budget leaves no room for a second awk per - # file (#773). Unlike the provider marker, those follow the `# @tag` rule -- - # the contiguous comment block directly above the definition. - # - # Fed by a here-string rather than `< <(awk …)`. Bash 3.x only reaps the - # descriptors process substitution allocates when it returns to the top level - # or after forking an external command, and this scan runs once per file from - # inside a function that the run never leaves -- so each file leaked one, and - # the per-file `rm` fork that used to follow was the only thing collecting - # them. Removing that fork for #1271 turned the leak loose: with 120 files - # under a 120-descriptor limit the run stopped after two assertions and - # reported "risky" rather than failing. A here-string allocates nothing. - while IFS=$'\t' read -r fn provider annot_timeout annot_retry annot_skip annot_reason; do - [ -z "$fn" ] && continue - if [ "$fn" = "@@no_parallel@@" ]; then - [ "$provider" = "1" ] && _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=true - continue - fi - if [ "$fn" = "@@testfns@@" ]; then - _BASHUNIT_PROVIDER_MAP_TEST_FNS="$provider" - continue - fi - if [ "$fn" = "@@dynamic@@" ]; then - _BASHUNIT_PROVIDER_MAP_DYNAMIC=true - continue + if [ -z "$cache_index" ]; then + local before_content="" can_cache=false + if [ "$_BASHUNIT_PROVIDER_HEADER_CACHE_SEEDING" = true ] && + bashunit::helper::_read_short_provider_script "$script"; then + before_content=$_BASHUNIT_PROVIDER_SHORT_CONTENT_OUT + can_cache=true fi - if [ "$fn" = "@@annot@@" ]; then - [ "$annot_timeout" = "@@none@@" ] && annot_timeout="" - [ "$annot_retry" = "@@none@@" ] && annot_retry="" - bashunit::helper::annotations_record \ - "$provider" "$annot_timeout" "$annot_retry" "$annot_skip" "$annot_reason" - continue - fi - _BASHUNIT_PROVIDER_MAP_FNS[count]="$fn" - _BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider" - count=$((count + 1)) - done <<<"$(awk ' + local awk_program=' /^# bashunit: no-parallel-tests/ { no_parallel = 1; next } /^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ { p = $0 @@ -216,7 +225,47 @@ function bashunit::helper::build_provider_map() { if (dynamic) { printf "@@dynamic@@\t1\n" } else { printf "@@testfns@@\t%s\n", top_level_fns } } - ' "$script" 2>/dev/null)" + ' + if [ "$can_cache" = true ]; then + # The scanner must see the same bytes the cache later validates. + scan_rows=$(awk "$awk_program" 2>/dev/null <<<"$before_content") || can_cache=false + else + scan_rows=$(awk "$awk_program" "$script" 2>/dev/null) + fi + if [ "$can_cache" = true ]; then + index=${#_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[@]} + _BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[index]="$script" + _BASHUNIT_PROVIDER_HEADER_CACHE_CONTENTS[index]="$before_content" + _BASHUNIT_PROVIDER_HEADER_CACHE_ROWS[index]="$scan_rows" + fi + fi + + # A here-string avoids the descriptor leak from process substitution on Bash 3. + while IFS=$'\t' read -r fn provider annot_timeout annot_retry annot_skip annot_reason; do + [ -z "$fn" ] && continue + if [ "$fn" = "@@no_parallel@@" ]; then + [ "$provider" = "1" ] && _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=true + continue + fi + if [ "$fn" = "@@testfns@@" ]; then + _BASHUNIT_PROVIDER_MAP_TEST_FNS="$provider" + continue + fi + if [ "$fn" = "@@dynamic@@" ]; then + _BASHUNIT_PROVIDER_MAP_DYNAMIC=true + continue + fi + if [ "$fn" = "@@annot@@" ]; then + [ "$annot_timeout" = "@@none@@" ] && annot_timeout="" + [ "$annot_retry" = "@@none@@" ] && annot_retry="" + bashunit::helper::annotations_record \ + "$provider" "$annot_timeout" "$annot_retry" "$annot_skip" "$annot_reason" + continue + fi + _BASHUNIT_PROVIDER_MAP_FNS[count]="$fn" + _BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider" + count=$((count + 1)) + done <<<"$scan_rows" } @@ -251,4 +300,3 @@ function bashunit::helper::get_provider_data() { bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT" fi } - diff --git a/tests/acceptance/bashunit_run_forks_test.sh b/tests/acceptance/bashunit_run_forks_test.sh index 87439ea1d..c4271306d 100644 --- a/tests/acceptance/bashunit_run_forks_test.sh +++ b/tests/acceptance/bashunit_run_forks_test.sh @@ -204,10 +204,72 @@ function test_running_a_test_file_stays_within_the_awk_fork_budget() { assert_less_or_equal_than 2 "$awk_forks" } -# Regression guard: a run must remove its run-output scratch directory on exit. -# The directory (introduced in #801 under $TMPDIR/bashunit/run/) had no cleanup, -# leaking one empty directory per bashunit invocation. Point the nested run at a -# private TMPDIR so the check is deterministic and parallel-safe. +# Each mode needs its own temp owner when the acceptance suite runs in parallel. +function _assert_multifile_metadata_scan_budget() { + if bashunit::check_os::is_windows; then + bashunit::skip "PATH shims are unreliable under Git Bash" && return + fi + + local mode=$1 expected=$2 dir real_awk count_file file + dir="$(bashunit::temp_dir)" + count_file="$dir/awk_count" + : >"$count_file" + real_awk="$(command -v awk)" + { + printf '%s\n' '#!/usr/bin/env bash' + printf 'printf "awk\\n" >>"%s"\n' "$count_file" + printf 'exec "%s" "$@"\n' "$real_awk" + } >"$dir/awk" + chmod +x "$dir/awk" + + local -a fixtures=() flags=(--no-parallel --simple) + for file in 1 2 3; do + fixtures[${#fixtures[@]}]="$dir/metadata_${file}_test.sh" + printf 'function test_metadata_case_%s() { assert_same same same; }\n' "$file" \ + >"$dir/metadata_${file}_test.sh" + done + + local header=true + case "$mode" in + no_header) header=false ;; + list) flags=(--no-parallel --list) ;; + json) flags=(--no-parallel --output json) ;; + parallel_simple) flags=(--parallel --simple) ;; + parallel_header) flags=(--parallel) ;; + esac + + local status=0 + PATH="$dir:$PATH" BASHUNIT_SHOW_HEADER="$header" ./bashunit --skip-env-file \ + "${flags[@]}" "${fixtures[@]}" >"$dir/output" 2>&1 || status=$? + + assert_same 0 "$status" + assert_same "$expected" "$(wc -l <"$count_file" | tr -d ' ')" +} + +function test_multifile_metadata_scans_with_a_header() { + _assert_multifile_metadata_scan_budget header 6 +} + +function test_multifile_metadata_scans_without_a_header() { + _assert_multifile_metadata_scan_budget no_header 6 +} + +function test_multifile_metadata_listing_skips_scans() { + _assert_multifile_metadata_scan_budget list 0 +} + +function test_multifile_metadata_machine_output_scans() { + _assert_multifile_metadata_scan_budget json 6 +} + +function test_multifile_metadata_parallel_simple_scans() { + _assert_multifile_metadata_scan_budget parallel_simple 6 +} + +function test_multifile_metadata_parallel_header_scans() { + _assert_multifile_metadata_scan_budget parallel_header 8 +} + function test_run_removes_its_run_output_dir() { local dir dir="$(bashunit::temp_dir)" diff --git a/tests/unit/helper/helper_test.sh b/tests/unit/helper/helper_test.sh index 9943e0dae..32c64735c 100644 --- a/tests/unit/helper/helper_test.sh +++ b/tests/unit/helper/helper_test.sh @@ -493,6 +493,252 @@ function test_find_total_tests_multiple_files() { assert_same "5" "$(helpers_test::find_total_in_subshell "" "$file1" "$file2")" } +function test_provider_scan_reuses_unchanged_multifile_header_metadata() { + local dir first second marker + local _BASHUNIT_PARALLEL_ENABLED=false + dir="$(bashunit::temp_dir)" + first="$dir/first_test.sh" + second="$dir/second_test.sh" + marker="$dir/awk_calls" + { + printf '%s\n' '# bashunit: no-parallel-tests' + printf '%s\n' 'function provide_rows() { echo one; }' + printf '%s\n' '# @timeout 5' '# @retry 2' '# @skip pause' '# @data_provider provide_rows' + printf '%s\n' 'function test_first() { :; }' + } >"$first" + printf '%s\n' 'eval "function test_second() { :; }"' >"$second" + + function awk() { + printf 'awk\n' >>"$marker" + command awk "$@" + } + bashunit::helper::find_total_tests "" "$first" "$second" >/dev/null + bashunit::helper::build_provider_map "$first" + bashunit::helper::provider_for_function test_first + assert_same 'provide_rows' "$_BASHUNIT_PROVIDER_FN_OUT" + assert_same 'true' "$_BASHUNIT_PROVIDER_MAP_NO_PARALLEL" + assert_same 'false' "$_BASHUNIT_PROVIDER_MAP_DYNAMIC" + assert_contains 'test_first' "$_BASHUNIT_PROVIDER_MAP_TEST_FNS" + bashunit::helper::annotations_for_function test_first + assert_same '5' "$_BASHUNIT_ANNOT_TIMEOUT_OUT" + assert_same '2' "$_BASHUNIT_ANNOT_RETRY_OUT" + assert_same 'true' "$_BASHUNIT_ANNOT_SKIP_OUT" + assert_same 'pause' "$_BASHUNIT_ANNOT_REASON_OUT" + + bashunit::helper::build_provider_map "$second" + assert_same 'true' "$_BASHUNIT_PROVIDER_MAP_DYNAMIC" + assert_same 'false' "$_BASHUNIT_PROVIDER_MAP_NO_PARALLEL" + + assert_same '2' "$(wc -l <"$marker" | tr -d ' ')" + unset -f awk +} + +function test_provider_scan_detects_later_same_size_same_mtime_rewrite() { + local dir first second stamp original_size + local _BASHUNIT_PARALLEL_ENABLED=false + dir="$(bashunit::temp_dir)" + first="$dir/first_test.sh" + second="$dir/second_test.sh" + stamp="$dir/original_stamp" + printf '%s\n' '# @retry 1' 'function test_before() { :; }' >"$second" + original_size="$(wc -c <"$second")" + touch -r "$second" "$stamp" + { + printf '%s\n' 'function test_rewrite() {' + printf '%s\n' ' printf "%s\n" "# @retry 2" "function test_after_() { :; }" >"$LATER_FILE"' + printf '%s\n' ' touch -r "$STAMP_FILE" "$LATER_FILE"' + printf '%s\n' '}' + } >"$first" + + bashunit::helper::find_total_tests "" "$first" "$second" >/dev/null + bashunit::helper::build_provider_map "$first" + # shellcheck source=/dev/null + source "$first" + LATER_FILE="$second" STAMP_FILE="$stamp" test_rewrite + assert_same "$original_size" "$(wc -c <"$second")" + bashunit::helper::build_provider_map "$second" + bashunit::helper::annotations_for_function test_after_ + assert_same '2' "$_BASHUNIT_ANNOT_RETRY_OUT" + assert_contains 'test_after_' "$_BASHUNIT_PROVIDER_MAP_TEST_FNS" + assert_not_contains 'test_before' "$_BASHUNIT_PROVIDER_MAP_TEST_FNS" +} + +function test_provider_scan_uses_the_snapshot_that_it_caches() { + local dir first second marker + local _BASHUNIT_PARALLEL_ENABLED=false + dir="$(bashunit::temp_dir)" + first="$dir/first_test.sh" + second="$dir/second_test.sh" + marker="$dir/race_started" + printf '%s\n' 'function test_first() { :; }' >"$first" + printf '%s\n' '# @retry 1' 'function test_second() { :; }' >"$second" + + local RACE_FILE="$second" RACE_MARKER="$marker" + function awk() { + local metadata=false + case "$1" in *'@@no_parallel@@'*) metadata=true ;; esac + if [ "$metadata" = true ] && [ "$_BASHUNIT_PROVIDER_MAP_SCRIPT" = "$RACE_FILE" ] && + [ ! -e "$RACE_MARKER" ]; then + printf '%s\n' "$#" >"$RACE_MARKER" + if [ "$#" -eq 2 ]; then + printf '%s\n' '# @retry 2' 'function test_second() { :; }' | command awk "$1" + return + fi + fi + command awk "$@" + } + + bashunit::helper::find_total_tests "" "$first" "$second" >/dev/null + assert_file_exists "$marker" + assert_same '1' "$(cat "$marker")" + assert_same '2' "${#_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[@]}" + assert_contains '# @retry 1' "$(cat "$second")" + bashunit::helper::annotations_for_function test_second + assert_same 'retry=1' "retry=$_BASHUNIT_ANNOT_RETRY_OUT" + bashunit::helper::build_provider_map "$first" + bashunit::helper::build_provider_map "$second" + bashunit::helper::annotations_for_function test_second + assert_same 'restored=1' "restored=$_BASHUNIT_ANNOT_RETRY_OUT" + unset -f awk +} + +function test_header_cache_sees_later_file_rewritten_by_earlier_test() { + local dir first second stamp report + dir="$(bashunit::temp_dir)" + first="$dir/first_test.sh" + second="$dir/second_test.sh" + stamp="$dir/original_stamp" + report="$dir/report.json" + printf '%s\n' '# @skip old' 'function test_later() { assert_true true; }' >"$second" + touch -r "$second" "$stamp" + { + printf '%s\n' 'function test_rewrite() {' + printf '%s\n' ' printf "%s\n" "# @skip new" "function test_later() { assert_true true; }" >"$LATER_FILE"' + printf '%s\n' ' touch -r "$STAMP_FILE" "$LATER_FILE"' + printf '%s\n' ' assert_true true' + printf '%s\n' '}' + } >"$first" + + local output status=0 + output=$(LATER_FILE="$second" STAMP_FILE="$stamp" NO_COLOR=1 ./bashunit \ + --no-parallel --report-json "$report" "$first" "$second" 2>&1) || status=$? + + assert_same '0' "$status" + assert_contains '2 total' "$output" + assert_contains 'Skipped: Later new' "$output" + assert_not_contains 'Skipped: Later old' "$output" + assert_contains '"skipped": 1' "$(cat "$report")" +} + +function test_header_cache_validates_last_file_after_filtered_first_file_rewrites_it() { + local dir first second stamp + dir="$(bashunit::temp_dir)" + first="$dir/first_test.sh" + second="$dir/second_test.sh" + stamp="$dir/original_stamp" + printf '%s\n' '# @skip old' 'function test_selected() { assert_true true; }' >"$second" + touch -r "$second" "$stamp" + { + printf '%s\n' 'printf "%s\n" "# @skip new" "function test_selected() { assert_true true; }" >"$LATER_FILE"' + printf '%s\n' 'touch -r "$STAMP_FILE" "$LATER_FILE"' + printf '%s\n' 'function test_other() { assert_true true; }' + } >"$first" + + local output status=0 + output=$(LATER_FILE="$second" STAMP_FILE="$stamp" NO_COLOR=1 ./bashunit \ + --no-parallel --filter selected "$first" "$second" 2>&1) || status=$? + + assert_same '0' "$status" + assert_contains '1 total' "$output" + assert_contains 'Skipped: Selected new' "$output" + assert_not_contains 'Skipped: Selected old' "$output" +} + +function test_provider_scan_checks_reordered_cache_entries_for_changes() { + local dir first second third marker + local _BASHUNIT_PARALLEL_ENABLED=false + dir="$(bashunit::temp_dir)" + first="$dir/first_test.sh" + second="$dir/second_test.sh" + third="$dir/third_test.sh" + marker="$dir/awk_calls" + printf '%s\n' 'function test_first() { :; }' >"$first" + printf '%s\n' '# @retry 1' 'function test_second() { :; }' >"$second" + printf '%s\n' 'function test_third() { :; }' >"$third" + + function awk() { + printf 'awk\n' >>"$marker" + command awk "$@" + } + bashunit::helper::find_total_tests "" "$first" "$second" "$third" >/dev/null + bashunit::helper::build_provider_map "$third" + bashunit::helper::build_provider_map "$first" + printf '%s\n' '# @retry 2' 'function test_second() { :; }' >"$second" + bashunit::helper::build_provider_map "$second" + bashunit::helper::annotations_for_function test_second + + assert_same '2' "$_BASHUNIT_ANNOT_RETRY_OUT" + assert_same '4' "$(wc -l <"$marker" | tr -d ' ')" + unset -f awk +} + +function test_provider_scan_large_file_keeps_the_existing_rescan() { + local dir first second marker + local _BASHUNIT_PARALLEL_ENABLED=false + dir="$(bashunit::temp_dir)" + first="$dir/large_test.sh" + second="$dir/small_test.sh" + marker="$dir/awk_calls" + { + printf '#%*s\n' 5000 '' + printf '%s\n' 'function test_large() { :; }' + } >"$first" + printf '%s\n' 'function test_small() { :; }' >"$second" + + function awk() { + printf 'awk\n' >>"$marker" + command awk "$@" + } + bashunit::helper::find_total_tests "" "$first" "$second" >/dev/null + bashunit::helper::build_provider_map "$first" + + assert_same '3' "$(wc -l <"$marker" | tr -d ' ')" + assert_contains 'test_large' "$_BASHUNIT_PROVIDER_MAP_TEST_FNS" + unset -f awk +} + +function test_provider_scan_parallel_header_count_does_not_seed_metadata_cache_or_read_snapshots() { + local dir first second reads scans entries + dir="$(bashunit::temp_dir)" + first="$dir/first_test.sh" + second="$dir/second_test.sh" + reads="$dir/snapshot_reads" + scans="$dir/awk_calls" + entries="$dir/cache_entries" + printf '%s\n' 'function test_first() { :; }' >"$first" + printf '%s\n' 'function test_second() { :; }' >"$second" + + ( + local _BASHUNIT_PARALLEL_ENABLED=true + function bashunit::helper::_read_short_provider_script() { + printf 'read\n' >>"$reads" + return 1 + } + function awk() { + printf 'awk\n' >>"$scans" + command awk "$@" + } + bashunit::helper::find_total_tests "" "$first" "$second" >/dev/null + printf '%s\n' "${#_BASHUNIT_PROVIDER_HEADER_CACHE_PATHS[@]}" >"$entries" + bashunit::helper::build_provider_map "$first" + bashunit::helper::build_provider_map "$second" + ) + + assert_same '0' "$(cat "$entries")" + assert_file_not_exists "$reads" + assert_same '4' "$(wc -l <"$scans" | tr -d ' ')" +} + function test_find_total_tests_with_filter() { local file1 local file2