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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,9 @@

## Unreleased

### Changed
- Faster test execution: each test file's data-provider annotations are scanned once and cached, replacing a per-test `grep`+`sed` probe with a pure-bash lookup (no behaviour change) (#763)

## [0.41.0](https://github.com/TypedDevs/bashunit/compare/0.40.0...0.41.0) - 2026-07-11

### Added
Expand Down
132 changes: 115 additions & 17 deletions src/helpers.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -292,29 +292,123 @@ function bashunit::helper::normalize_variable_name() {
builtin echo "$normalized_string"
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"
# Provider map for the most recently scanned script. Scanning a file once and
# caching the test-function -> provider-function pairs replaces a per-test
# grep+sed fork with a pure-bash lookup on the hot path (issue #763).
_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""

#
# Resolves a script path, applying the issue #529 working-dir fallback.
# Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable).
#
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
local script=$1
# Handle directory changes in set_up_before_script (issue #529)
# If relative path doesn't exist, try with BASHUNIT_WORKING_DIR
if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
script="$BASHUNIT_WORKING_DIR/$script"
fi

if [ ! -f "$script" ]; then
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
return
fi
_BASHUNIT_PROVIDER_RESOLVED_OUT=$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.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_provider_map() {
bashunit::helper::_resolve_provider_script "$1"
local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

if [ -z "$script" ]; then
# Unreadable path: reset to an empty map keyed to this argument so a
# follow-up lookup returns empty without rescanning.
_BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
return
fi

local data_provider_function
data_provider_function=$(
# shellcheck disable=SC1087
grep -B 2 -E "(function[[:space:]]+)?$function_name[[:space:]]*\(\)" "$script" 2>/dev/null |
sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
)
if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
return
fi

_BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()

local count=0
local fn provider
# Single awk pass emits "<fn>\t<provider>" for every function whose
# definition is at most two lines below a `# @data_provider` (or
# `# data_provider`) annotation, mirroring the previous grep -B2 + sed.
while IFS=$'\t' read -r fn provider; do
[ -z "$fn" ] && continue
_BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
_BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
count=$((count + 1))
done < <(awk '
/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
p = $0
sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
sub(/[[:space:]]+$/, "", p)
pending = p
pending_line = NR
next
}
{
if (pending != "" && NR - pending_line <= 2) {
if (match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)) {
fn = $0
sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
sub(/[[:space:]]*\(\).*/, "", fn)
printf "%s\t%s\n", fn, pending
pending = ""
}
} else if (pending != "" && NR - pending_line > 2) {
pending = ""
}
}
' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached provider map.
# Writes the provider-function name (or empty) into _BASHUNIT_PROVIDER_FN_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::provider_for_function() {
local function_name=$1
local i=0
local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
while [ "$i" -lt "$total" ]; do
if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
_BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
return
fi
i=$((i + 1))
done
_BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"

bashunit::helper::build_provider_map "$script"
bashunit::helper::provider_for_function "$function_name"

if [ -n "$data_provider_function" ]; then
bashunit::helper::execute_function_if_exists "$data_provider_function"
if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
fi
}

Expand DownExpand Up@@ -379,15 +473,19 @@ function bashunit::helper::find_total_tests() {
local -a provider_data=()
local provider_data_count=0
local fn_name line
# Scan once; functions without a provider count as 1 with no fork (#763).
bashunit::helper::build_provider_map "$file"
for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
provider_data=()
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
count=$((count + 1))
continue
fi
provider_data_count=0
while IFS=" " read -r line; do
[ -z "$line" ] && continue
# shellcheck disable=SC2034
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$file")"
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

if [ "$provider_data_count" -eq 0 ]; then
count=$((count + 1))
Expand Down
26 changes: 15 additions & 11 deletions src/runner.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,22 +767,17 @@ function bashunit::runner::call_test_functions() {
local -a parsed_data=()
local parsed_data_count=0

# Scan the file once; per-test provider lookups below are pure-bash (#763).
bashunit::helper::build_provider_map "$script"

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
break
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$script")"

# No data provider found
if [ "$provider_data_count" -eq 0 ]; then
# No data provider found: run once without forking to capture provider output.
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
bashunit::runner::wait_for_job_slot
bashunit::runner::run_test "$script" "$fn_name" &
Expand All@@ -793,6 +788,15 @@ function bashunit::runner::call_test_functions() {
continue
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

# Execute the test function for each line of data
local data
for data in "${provider_data[@]+"${provider_data[@]}"}"; do
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/fixtures/provider_map/sample_providers.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Fixture for provider-map scanner tests. Not run as a suite; scanned as text.

# @data_provider provide_at_form
function test_with_at_annotation() {
return 0
}

# data_provider provide_plain_form
function test_without_at() {
return 0
}

# @data_provider provide_two_lines_up
# shellcheck disable=SC2317
function test_annotation_two_lines_up() {
return 0
}

# @data_provider provide_shared
function test_shares_provider_one() {
return 0
}

# @data_provider provide_shared
test_shares_provider_two() {
return 0
}

function test_without_provider() {
return 0
}
45 changes: 45 additions & 0 deletions tests/unit/helpers_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,51 @@ function test_get_provider_data_should_returns_empty_when_not_exists_provider_fu
"$(bashunit::helper::get_provider_data "fake_function_get_not_existing_provider_data" "${BASH_SOURCE[0]}")"
}

FIXTURE_PROVIDER_MAP="$(dirname "${BASH_SOURCE[0]}")/fixtures/provider_map/sample_providers.sh"

function provider_for() {
bashunit::helper::build_provider_map "$1"
bashunit::helper::provider_for_function "$2"
echo "$_BASHUNIT_PROVIDER_FN_OUT"
}

function test_provider_map_resolves_at_annotation() {
assert_same "provide_at_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_with_at_annotation")"
}

function test_provider_map_resolves_plain_annotation_without_at() {
assert_same "provide_plain_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_at")"
}

function test_provider_map_resolves_annotation_two_lines_above_function() {
assert_same "provide_two_lines_up" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_annotation_two_lines_up")"
}

function test_provider_map_resolves_shared_provider_for_both_functions() {
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_one")"
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_two")"
}

function test_provider_map_returns_empty_when_function_has_no_provider() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_provider")"
}

function test_provider_map_returns_empty_for_unknown_function() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_does_not_exist")"
}

function test_provider_map_returns_empty_for_unreadable_script() {
assert_same "" \
"$(provider_for "/no/such/path/nope_test.sh" "test_with_at_annotation")"
}

function test_left_trim() {
assert_same "foo" "$(bashunit::helper::trim " foo")"
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,9 @@

## Unreleased

### Changed
- Faster test execution: each test file's data-provider annotations are scanned once and cached, replacing a per-test `grep`+`sed` probe with a pure-bash lookup (no behaviour change) (#763)

## [0.41.0](https://github.com/TypedDevs/bashunit/compare/0.40.0...0.41.0) - 2026-07-11

### Added
Expand Down
132 changes: 115 additions & 17 deletions src/helpers.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -292,29 +292,123 @@ function bashunit::helper::normalize_variable_name() {
builtin echo "$normalized_string"
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"
# Provider map for the most recently scanned script. Scanning a file once and
# caching the test-function -> provider-function pairs replaces a per-test
# grep+sed fork with a pure-bash lookup on the hot path (issue #763).
_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""

#
# Resolves a script path, applying the issue #529 working-dir fallback.
# Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable).
#
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
local script=$1
# Handle directory changes in set_up_before_script (issue #529)
# If relative path doesn't exist, try with BASHUNIT_WORKING_DIR
if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
script="$BASHUNIT_WORKING_DIR/$script"
fi

if [ ! -f "$script" ]; then
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
return
fi
_BASHUNIT_PROVIDER_RESOLVED_OUT=$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.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_provider_map() {
bashunit::helper::_resolve_provider_script "$1"
local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

if [ -z "$script" ]; then
# Unreadable path: reset to an empty map keyed to this argument so a
# follow-up lookup returns empty without rescanning.
_BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
return
fi

local data_provider_function
data_provider_function=$(
# shellcheck disable=SC1087
grep -B 2 -E "(function[[:space:]]+)?$function_name[[:space:]]*\(\)" "$script" 2>/dev/null |
sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
)
if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
return
fi

_BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()

local count=0
local fn provider
# Single awk pass emits "<fn>\t<provider>" for every function whose
# definition is at most two lines below a `# @data_provider` (or
# `# data_provider`) annotation, mirroring the previous grep -B2 + sed.
while IFS=$'\t' read -r fn provider; do
[ -z "$fn" ] && continue
_BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
_BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
count=$((count + 1))
done < <(awk '
/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
p = $0
sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
sub(/[[:space:]]+$/, "", p)
pending = p
pending_line = NR
next
}
{
if (pending != "" && NR - pending_line <= 2) {
if (match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)) {
fn = $0
sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
sub(/[[:space:]]*\(\).*/, "", fn)
printf "%s\t%s\n", fn, pending
pending = ""
}
} else if (pending != "" && NR - pending_line > 2) {
pending = ""
}
}
' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached provider map.
# Writes the provider-function name (or empty) into _BASHUNIT_PROVIDER_FN_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::provider_for_function() {
local function_name=$1
local i=0
local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
while [ "$i" -lt "$total" ]; do
if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
_BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
return
fi
i=$((i + 1))
done
_BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"

bashunit::helper::build_provider_map "$script"
bashunit::helper::provider_for_function "$function_name"

if [ -n "$data_provider_function" ]; then
bashunit::helper::execute_function_if_exists "$data_provider_function"
if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
fi
}

Expand DownExpand Up@@ -379,15 +473,19 @@ function bashunit::helper::find_total_tests() {
local -a provider_data=()
local provider_data_count=0
local fn_name line
# Scan once; functions without a provider count as 1 with no fork (#763).
bashunit::helper::build_provider_map "$file"
for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
provider_data=()
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
count=$((count + 1))
continue
fi
provider_data_count=0
while IFS=" " read -r line; do
[ -z "$line" ] && continue
# shellcheck disable=SC2034
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$file")"
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

if [ "$provider_data_count" -eq 0 ]; then
count=$((count + 1))
Expand Down
26 changes: 15 additions & 11 deletions src/runner.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,22 +767,17 @@ function bashunit::runner::call_test_functions() {
local -a parsed_data=()
local parsed_data_count=0

# Scan the file once; per-test provider lookups below are pure-bash (#763).
bashunit::helper::build_provider_map "$script"

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
break
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$script")"

# No data provider found
if [ "$provider_data_count" -eq 0 ]; then
# No data provider found: run once without forking to capture provider output.
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
bashunit::runner::wait_for_job_slot
bashunit::runner::run_test "$script" "$fn_name" &
Expand All@@ -793,6 +788,15 @@ function bashunit::runner::call_test_functions() {
continue
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

# Execute the test function for each line of data
local data
for data in "${provider_data[@]+"${provider_data[@]}"}"; do
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/fixtures/provider_map/sample_providers.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Fixture for provider-map scanner tests. Not run as a suite; scanned as text.

# @data_provider provide_at_form
function test_with_at_annotation() {
return 0
}

# data_provider provide_plain_form
function test_without_at() {
return 0
}

# @data_provider provide_two_lines_up
# shellcheck disable=SC2317
function test_annotation_two_lines_up() {
return 0
}

# @data_provider provide_shared
function test_shares_provider_one() {
return 0
}

# @data_provider provide_shared
test_shares_provider_two() {
return 0
}

function test_without_provider() {
return 0
}
45 changes: 45 additions & 0 deletions tests/unit/helpers_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,51 @@ function test_get_provider_data_should_returns_empty_when_not_exists_provider_fu
"$(bashunit::helper::get_provider_data "fake_function_get_not_existing_provider_data" "${BASH_SOURCE[0]}")"
}

FIXTURE_PROVIDER_MAP="$(dirname "${BASH_SOURCE[0]}")/fixtures/provider_map/sample_providers.sh"

function provider_for() {
bashunit::helper::build_provider_map "$1"
bashunit::helper::provider_for_function "$2"
echo "$_BASHUNIT_PROVIDER_FN_OUT"
}

function test_provider_map_resolves_at_annotation() {
assert_same "provide_at_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_with_at_annotation")"
}

function test_provider_map_resolves_plain_annotation_without_at() {
assert_same "provide_plain_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_at")"
}

function test_provider_map_resolves_annotation_two_lines_above_function() {
assert_same "provide_two_lines_up" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_annotation_two_lines_up")"
}

function test_provider_map_resolves_shared_provider_for_both_functions() {
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_one")"
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_two")"
}

function test_provider_map_returns_empty_when_function_has_no_provider() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_provider")"
}

function test_provider_map_returns_empty_for_unknown_function() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_does_not_exist")"
}

function test_provider_map_returns_empty_for_unreadable_script() {
assert_same "" \
"$(provider_for "/no/such/path/nope_test.sh" "test_with_at_annotation")"
}

function test_left_trim() {
assert_same "foo" "$(bashunit::helper::trim " foo")"
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,9 @@

## Unreleased

### Changed
- Faster test execution: each test file's data-provider annotations are scanned once and cached, replacing a per-test `grep`+`sed` probe with a pure-bash lookup (no behaviour change) (#763)

## [0.41.0](https://github.com/TypedDevs/bashunit/compare/0.40.0...0.41.0) - 2026-07-11

### Added
Expand Down
132 changes: 115 additions & 17 deletions src/helpers.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -292,29 +292,123 @@ function bashunit::helper::normalize_variable_name() {
builtin echo "$normalized_string"
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"
# Provider map for the most recently scanned script. Scanning a file once and
# caching the test-function -> provider-function pairs replaces a per-test
# grep+sed fork with a pure-bash lookup on the hot path (issue #763).
_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""

#
# Resolves a script path, applying the issue #529 working-dir fallback.
# Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable).
#
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
local script=$1
# Handle directory changes in set_up_before_script (issue #529)
# If relative path doesn't exist, try with BASHUNIT_WORKING_DIR
if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
script="$BASHUNIT_WORKING_DIR/$script"
fi

if [ ! -f "$script" ]; then
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
return
fi
_BASHUNIT_PROVIDER_RESOLVED_OUT=$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.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_provider_map() {
bashunit::helper::_resolve_provider_script "$1"
local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

if [ -z "$script" ]; then
# Unreadable path: reset to an empty map keyed to this argument so a
# follow-up lookup returns empty without rescanning.
_BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
return
fi

local data_provider_function
data_provider_function=$(
# shellcheck disable=SC1087
grep -B 2 -E "(function[[:space:]]+)?$function_name[[:space:]]*\(\)" "$script" 2>/dev/null |
sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
)
if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
return
fi

_BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()

local count=0
local fn provider
# Single awk pass emits "<fn>\t<provider>" for every function whose
# definition is at most two lines below a `# @data_provider` (or
# `# data_provider`) annotation, mirroring the previous grep -B2 + sed.
while IFS=$'\t' read -r fn provider; do
[ -z "$fn" ] && continue
_BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
_BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
count=$((count + 1))
done < <(awk '
/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
p = $0
sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
sub(/[[:space:]]+$/, "", p)
pending = p
pending_line = NR
next
}
{
if (pending != "" && NR - pending_line <= 2) {
if (match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)) {
fn = $0
sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
sub(/[[:space:]]*\(\).*/, "", fn)
printf "%s\t%s\n", fn, pending
pending = ""
}
} else if (pending != "" && NR - pending_line > 2) {
pending = ""
}
}
' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached provider map.
# Writes the provider-function name (or empty) into _BASHUNIT_PROVIDER_FN_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::provider_for_function() {
local function_name=$1
local i=0
local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
while [ "$i" -lt "$total" ]; do
if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
_BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
return
fi
i=$((i + 1))
done
_BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"

bashunit::helper::build_provider_map "$script"
bashunit::helper::provider_for_function "$function_name"

if [ -n "$data_provider_function" ]; then
bashunit::helper::execute_function_if_exists "$data_provider_function"
if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
fi
}

Expand DownExpand Up@@ -379,15 +473,19 @@ function bashunit::helper::find_total_tests() {
local -a provider_data=()
local provider_data_count=0
local fn_name line
# Scan once; functions without a provider count as 1 with no fork (#763).
bashunit::helper::build_provider_map "$file"
for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
provider_data=()
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
count=$((count + 1))
continue
fi
provider_data_count=0
while IFS=" " read -r line; do
[ -z "$line" ] && continue
# shellcheck disable=SC2034
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$file")"
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

if [ "$provider_data_count" -eq 0 ]; then
count=$((count + 1))
Expand Down
26 changes: 15 additions & 11 deletions src/runner.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,22 +767,17 @@ function bashunit::runner::call_test_functions() {
local -a parsed_data=()
local parsed_data_count=0

# Scan the file once; per-test provider lookups below are pure-bash (#763).
bashunit::helper::build_provider_map "$script"

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
break
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$script")"

# No data provider found
if [ "$provider_data_count" -eq 0 ]; then
# No data provider found: run once without forking to capture provider output.
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
bashunit::runner::wait_for_job_slot
bashunit::runner::run_test "$script" "$fn_name" &
Expand All@@ -793,6 +788,15 @@ function bashunit::runner::call_test_functions() {
continue
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

# Execute the test function for each line of data
local data
for data in "${provider_data[@]+"${provider_data[@]}"}"; do
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/fixtures/provider_map/sample_providers.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Fixture for provider-map scanner tests. Not run as a suite; scanned as text.

# @data_provider provide_at_form
function test_with_at_annotation() {
return 0
}

# data_provider provide_plain_form
function test_without_at() {
return 0
}

# @data_provider provide_two_lines_up
# shellcheck disable=SC2317
function test_annotation_two_lines_up() {
return 0
}

# @data_provider provide_shared
function test_shares_provider_one() {
return 0
}

# @data_provider provide_shared
test_shares_provider_two() {
return 0
}

function test_without_provider() {
return 0
}
45 changes: 45 additions & 0 deletions tests/unit/helpers_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,51 @@ function test_get_provider_data_should_returns_empty_when_not_exists_provider_fu
"$(bashunit::helper::get_provider_data "fake_function_get_not_existing_provider_data" "${BASH_SOURCE[0]}")"
}

FIXTURE_PROVIDER_MAP="$(dirname "${BASH_SOURCE[0]}")/fixtures/provider_map/sample_providers.sh"

function provider_for() {
bashunit::helper::build_provider_map "$1"
bashunit::helper::provider_for_function "$2"
echo "$_BASHUNIT_PROVIDER_FN_OUT"
}

function test_provider_map_resolves_at_annotation() {
assert_same "provide_at_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_with_at_annotation")"
}

function test_provider_map_resolves_plain_annotation_without_at() {
assert_same "provide_plain_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_at")"
}

function test_provider_map_resolves_annotation_two_lines_above_function() {
assert_same "provide_two_lines_up" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_annotation_two_lines_up")"
}

function test_provider_map_resolves_shared_provider_for_both_functions() {
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_one")"
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_two")"
}

function test_provider_map_returns_empty_when_function_has_no_provider() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_provider")"
}

function test_provider_map_returns_empty_for_unknown_function() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_does_not_exist")"
}

function test_provider_map_returns_empty_for_unreadable_script() {
assert_same "" \
"$(provider_for "/no/such/path/nope_test.sh" "test_with_at_annotation")"
}

function test_left_trim() {
assert_same "foo" "$(bashunit::helper::trim " foo")"
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,9 @@

## Unreleased

### Changed
- Faster test execution: each test file's data-provider annotations are scanned once and cached, replacing a per-test `grep`+`sed` probe with a pure-bash lookup (no behaviour change) (#763)

## [0.41.0](https://github.com/TypedDevs/bashunit/compare/0.40.0...0.41.0) - 2026-07-11

### Added
Expand Down
132 changes: 115 additions & 17 deletions src/helpers.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -292,29 +292,123 @@ function bashunit::helper::normalize_variable_name() {
builtin echo "$normalized_string"
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"
# Provider map for the most recently scanned script. Scanning a file once and
# caching the test-function -> provider-function pairs replaces a per-test
# grep+sed fork with a pure-bash lookup on the hot path (issue #763).
_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""

#
# Resolves a script path, applying the issue #529 working-dir fallback.
# Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable).
#
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
local script=$1
# Handle directory changes in set_up_before_script (issue #529)
# If relative path doesn't exist, try with BASHUNIT_WORKING_DIR
if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
script="$BASHUNIT_WORKING_DIR/$script"
fi

if [ ! -f "$script" ]; then
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
return
fi
_BASHUNIT_PROVIDER_RESOLVED_OUT=$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.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_provider_map() {
bashunit::helper::_resolve_provider_script "$1"
local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

if [ -z "$script" ]; then
# Unreadable path: reset to an empty map keyed to this argument so a
# follow-up lookup returns empty without rescanning.
_BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
return
fi

local data_provider_function
data_provider_function=$(
# shellcheck disable=SC1087
grep -B 2 -E "(function[[:space:]]+)?$function_name[[:space:]]*\(\)" "$script" 2>/dev/null |
sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
)
if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
return
fi

_BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()

local count=0
local fn provider
# Single awk pass emits "<fn>\t<provider>" for every function whose
# definition is at most two lines below a `# @data_provider` (or
# `# data_provider`) annotation, mirroring the previous grep -B2 + sed.
while IFS=$'\t' read -r fn provider; do
[ -z "$fn" ] && continue
_BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
_BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
count=$((count + 1))
done < <(awk '
/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
p = $0
sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
sub(/[[:space:]]+$/, "", p)
pending = p
pending_line = NR
next
}
{
if (pending != "" && NR - pending_line <= 2) {
if (match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)) {
fn = $0
sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
sub(/[[:space:]]*\(\).*/, "", fn)
printf "%s\t%s\n", fn, pending
pending = ""
}
} else if (pending != "" && NR - pending_line > 2) {
pending = ""
}
}
' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached provider map.
# Writes the provider-function name (or empty) into _BASHUNIT_PROVIDER_FN_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::provider_for_function() {
local function_name=$1
local i=0
local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
while [ "$i" -lt "$total" ]; do
if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
_BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
return
fi
i=$((i + 1))
done
_BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"

bashunit::helper::build_provider_map "$script"
bashunit::helper::provider_for_function "$function_name"

if [ -n "$data_provider_function" ]; then
bashunit::helper::execute_function_if_exists "$data_provider_function"
if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
fi
}

Expand DownExpand Up@@ -379,15 +473,19 @@ function bashunit::helper::find_total_tests() {
local -a provider_data=()
local provider_data_count=0
local fn_name line
# Scan once; functions without a provider count as 1 with no fork (#763).
bashunit::helper::build_provider_map "$file"
for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
provider_data=()
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
count=$((count + 1))
continue
fi
provider_data_count=0
while IFS=" " read -r line; do
[ -z "$line" ] && continue
# shellcheck disable=SC2034
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$file")"
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

if [ "$provider_data_count" -eq 0 ]; then
count=$((count + 1))
Expand Down
26 changes: 15 additions & 11 deletions src/runner.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,22 +767,17 @@ function bashunit::runner::call_test_functions() {
local -a parsed_data=()
local parsed_data_count=0

# Scan the file once; per-test provider lookups below are pure-bash (#763).
bashunit::helper::build_provider_map "$script"

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
break
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$script")"

# No data provider found
if [ "$provider_data_count" -eq 0 ]; then
# No data provider found: run once without forking to capture provider output.
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
bashunit::runner::wait_for_job_slot
bashunit::runner::run_test "$script" "$fn_name" &
Expand All@@ -793,6 +788,15 @@ function bashunit::runner::call_test_functions() {
continue
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

# Execute the test function for each line of data
local data
for data in "${provider_data[@]+"${provider_data[@]}"}"; do
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/fixtures/provider_map/sample_providers.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Fixture for provider-map scanner tests. Not run as a suite; scanned as text.

# @data_provider provide_at_form
function test_with_at_annotation() {
return 0
}

# data_provider provide_plain_form
function test_without_at() {
return 0
}

# @data_provider provide_two_lines_up
# shellcheck disable=SC2317
function test_annotation_two_lines_up() {
return 0
}

# @data_provider provide_shared
function test_shares_provider_one() {
return 0
}

# @data_provider provide_shared
test_shares_provider_two() {
return 0
}

function test_without_provider() {
return 0
}
45 changes: 45 additions & 0 deletions tests/unit/helpers_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,51 @@ function test_get_provider_data_should_returns_empty_when_not_exists_provider_fu
"$(bashunit::helper::get_provider_data "fake_function_get_not_existing_provider_data" "${BASH_SOURCE[0]}")"
}

FIXTURE_PROVIDER_MAP="$(dirname "${BASH_SOURCE[0]}")/fixtures/provider_map/sample_providers.sh"

function provider_for() {
bashunit::helper::build_provider_map "$1"
bashunit::helper::provider_for_function "$2"
echo "$_BASHUNIT_PROVIDER_FN_OUT"
}

function test_provider_map_resolves_at_annotation() {
assert_same "provide_at_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_with_at_annotation")"
}

function test_provider_map_resolves_plain_annotation_without_at() {
assert_same "provide_plain_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_at")"
}

function test_provider_map_resolves_annotation_two_lines_above_function() {
assert_same "provide_two_lines_up" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_annotation_two_lines_up")"
}

function test_provider_map_resolves_shared_provider_for_both_functions() {
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_one")"
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_two")"
}

function test_provider_map_returns_empty_when_function_has_no_provider() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_provider")"
}

function test_provider_map_returns_empty_for_unknown_function() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_does_not_exist")"
}

function test_provider_map_returns_empty_for_unreadable_script() {
assert_same "" \
"$(provider_for "/no/such/path/nope_test.sh" "test_with_at_annotation")"
}

function test_left_trim() {
assert_same "foo" "$(bashunit::helper::trim " foo")"
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,9 @@

## Unreleased

### Changed
- Faster test execution: each test file's data-provider annotations are scanned once and cached, replacing a per-test `grep`+`sed` probe with a pure-bash lookup (no behaviour change) (#763)

## [0.41.0](https://github.com/TypedDevs/bashunit/compare/0.40.0...0.41.0) - 2026-07-11

### Added
Expand Down
132 changes: 115 additions & 17 deletions src/helpers.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -292,29 +292,123 @@ function bashunit::helper::normalize_variable_name() {
builtin echo "$normalized_string"
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"
# Provider map for the most recently scanned script. Scanning a file once and
# caching the test-function -> provider-function pairs replaces a per-test
# grep+sed fork with a pure-bash lookup on the hot path (issue #763).
_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""

#
# Resolves a script path, applying the issue #529 working-dir fallback.
# Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable).
#
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
local script=$1
# Handle directory changes in set_up_before_script (issue #529)
# If relative path doesn't exist, try with BASHUNIT_WORKING_DIR
if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
script="$BASHUNIT_WORKING_DIR/$script"
fi

if [ ! -f "$script" ]; then
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
return
fi
_BASHUNIT_PROVIDER_RESOLVED_OUT=$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.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_provider_map() {
bashunit::helper::_resolve_provider_script "$1"
local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

if [ -z "$script" ]; then
# Unreadable path: reset to an empty map keyed to this argument so a
# follow-up lookup returns empty without rescanning.
_BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
return
fi

local data_provider_function
data_provider_function=$(
# shellcheck disable=SC1087
grep -B 2 -E "(function[[:space:]]+)?$function_name[[:space:]]*\(\)" "$script" 2>/dev/null |
sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
)
if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
return
fi

_BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()

local count=0
local fn provider
# Single awk pass emits "<fn>\t<provider>" for every function whose
# definition is at most two lines below a `# @data_provider` (or
# `# data_provider`) annotation, mirroring the previous grep -B2 + sed.
while IFS=$'\t' read -r fn provider; do
[ -z "$fn" ] && continue
_BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
_BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
count=$((count + 1))
done < <(awk '
/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
p = $0
sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
sub(/[[:space:]]+$/, "", p)
pending = p
pending_line = NR
next
}
{
if (pending != "" && NR - pending_line <= 2) {
if (match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)) {
fn = $0
sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
sub(/[[:space:]]*\(\).*/, "", fn)
printf "%s\t%s\n", fn, pending
pending = ""
}
} else if (pending != "" && NR - pending_line > 2) {
pending = ""
}
}
' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached provider map.
# Writes the provider-function name (or empty) into _BASHUNIT_PROVIDER_FN_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::provider_for_function() {
local function_name=$1
local i=0
local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
while [ "$i" -lt "$total" ]; do
if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
_BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
return
fi
i=$((i + 1))
done
_BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"

bashunit::helper::build_provider_map "$script"
bashunit::helper::provider_for_function "$function_name"

if [ -n "$data_provider_function" ]; then
bashunit::helper::execute_function_if_exists "$data_provider_function"
if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
fi
}

Expand DownExpand Up@@ -379,15 +473,19 @@ function bashunit::helper::find_total_tests() {
local -a provider_data=()
local provider_data_count=0
local fn_name line
# Scan once; functions without a provider count as 1 with no fork (#763).
bashunit::helper::build_provider_map "$file"
for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
provider_data=()
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
count=$((count + 1))
continue
fi
provider_data_count=0
while IFS=" " read -r line; do
[ -z "$line" ] && continue
# shellcheck disable=SC2034
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$file")"
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

if [ "$provider_data_count" -eq 0 ]; then
count=$((count + 1))
Expand Down
26 changes: 15 additions & 11 deletions src/runner.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,22 +767,17 @@ function bashunit::runner::call_test_functions() {
local -a parsed_data=()
local parsed_data_count=0

# Scan the file once; per-test provider lookups below are pure-bash (#763).
bashunit::helper::build_provider_map "$script"

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
break
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$script")"

# No data provider found
if [ "$provider_data_count" -eq 0 ]; then
# No data provider found: run once without forking to capture provider output.
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
bashunit::runner::wait_for_job_slot
bashunit::runner::run_test "$script" "$fn_name" &
Expand All@@ -793,6 +788,15 @@ function bashunit::runner::call_test_functions() {
continue
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

# Execute the test function for each line of data
local data
for data in "${provider_data[@]+"${provider_data[@]}"}"; do
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/fixtures/provider_map/sample_providers.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Fixture for provider-map scanner tests. Not run as a suite; scanned as text.

# @data_provider provide_at_form
function test_with_at_annotation() {
return 0
}

# data_provider provide_plain_form
function test_without_at() {
return 0
}

# @data_provider provide_two_lines_up
# shellcheck disable=SC2317
function test_annotation_two_lines_up() {
return 0
}

# @data_provider provide_shared
function test_shares_provider_one() {
return 0
}

# @data_provider provide_shared
test_shares_provider_two() {
return 0
}

function test_without_provider() {
return 0
}
45 changes: 45 additions & 0 deletions tests/unit/helpers_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,51 @@ function test_get_provider_data_should_returns_empty_when_not_exists_provider_fu
"$(bashunit::helper::get_provider_data "fake_function_get_not_existing_provider_data" "${BASH_SOURCE[0]}")"
}

FIXTURE_PROVIDER_MAP="$(dirname "${BASH_SOURCE[0]}")/fixtures/provider_map/sample_providers.sh"

function provider_for() {
bashunit::helper::build_provider_map "$1"
bashunit::helper::provider_for_function "$2"
echo "$_BASHUNIT_PROVIDER_FN_OUT"
}

function test_provider_map_resolves_at_annotation() {
assert_same "provide_at_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_with_at_annotation")"
}

function test_provider_map_resolves_plain_annotation_without_at() {
assert_same "provide_plain_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_at")"
}

function test_provider_map_resolves_annotation_two_lines_above_function() {
assert_same "provide_two_lines_up" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_annotation_two_lines_up")"
}

function test_provider_map_resolves_shared_provider_for_both_functions() {
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_one")"
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_two")"
}

function test_provider_map_returns_empty_when_function_has_no_provider() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_provider")"
}

function test_provider_map_returns_empty_for_unknown_function() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_does_not_exist")"
}

function test_provider_map_returns_empty_for_unreadable_script() {
assert_same "" \
"$(provider_for "/no/such/path/nope_test.sh" "test_with_at_annotation")"
}

function test_left_trim() {
assert_same "foo" "$(bashunit::helper::trim " foo")"
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,9 @@

## Unreleased

### Changed
- Faster test execution: each test file's data-provider annotations are scanned once and cached, replacing a per-test `grep`+`sed` probe with a pure-bash lookup (no behaviour change) (#763)

## [0.41.0](https://github.com/TypedDevs/bashunit/compare/0.40.0...0.41.0) - 2026-07-11

### Added
Expand Down
132 changes: 115 additions & 17 deletions src/helpers.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -292,29 +292,123 @@ function bashunit::helper::normalize_variable_name() {
builtin echo "$normalized_string"
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"
# Provider map for the most recently scanned script. Scanning a file once and
# caching the test-function -> provider-function pairs replaces a per-test
# grep+sed fork with a pure-bash lookup on the hot path (issue #763).
_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""

#
# Resolves a script path, applying the issue #529 working-dir fallback.
# Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable).
#
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
local script=$1
# Handle directory changes in set_up_before_script (issue #529)
# If relative path doesn't exist, try with BASHUNIT_WORKING_DIR
if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
script="$BASHUNIT_WORKING_DIR/$script"
fi

if [ ! -f "$script" ]; then
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
return
fi
_BASHUNIT_PROVIDER_RESOLVED_OUT=$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.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_provider_map() {
bashunit::helper::_resolve_provider_script "$1"
local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

if [ -z "$script" ]; then
# Unreadable path: reset to an empty map keyed to this argument so a
# follow-up lookup returns empty without rescanning.
_BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
return
fi

local data_provider_function
data_provider_function=$(
# shellcheck disable=SC1087
grep -B 2 -E "(function[[:space:]]+)?$function_name[[:space:]]*\(\)" "$script" 2>/dev/null |
sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
)
if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
return
fi

_BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()

local count=0
local fn provider
# Single awk pass emits "<fn>\t<provider>" for every function whose
# definition is at most two lines below a `# @data_provider` (or
# `# data_provider`) annotation, mirroring the previous grep -B2 + sed.
while IFS=$'\t' read -r fn provider; do
[ -z "$fn" ] && continue
_BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
_BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
count=$((count + 1))
done < <(awk '
/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
p = $0
sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
sub(/[[:space:]]+$/, "", p)
pending = p
pending_line = NR
next
}
{
if (pending != "" && NR - pending_line <= 2) {
if (match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)) {
fn = $0
sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
sub(/[[:space:]]*\(\).*/, "", fn)
printf "%s\t%s\n", fn, pending
pending = ""
}
} else if (pending != "" && NR - pending_line > 2) {
pending = ""
}
}
' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached provider map.
# Writes the provider-function name (or empty) into _BASHUNIT_PROVIDER_FN_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::provider_for_function() {
local function_name=$1
local i=0
local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
while [ "$i" -lt "$total" ]; do
if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
_BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
return
fi
i=$((i + 1))
done
_BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"

bashunit::helper::build_provider_map "$script"
bashunit::helper::provider_for_function "$function_name"

if [ -n "$data_provider_function" ]; then
bashunit::helper::execute_function_if_exists "$data_provider_function"
if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
fi
}

Expand DownExpand Up@@ -379,15 +473,19 @@ function bashunit::helper::find_total_tests() {
local -a provider_data=()
local provider_data_count=0
local fn_name line
# Scan once; functions without a provider count as 1 with no fork (#763).
bashunit::helper::build_provider_map "$file"
for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
provider_data=()
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
count=$((count + 1))
continue
fi
provider_data_count=0
while IFS=" " read -r line; do
[ -z "$line" ] && continue
# shellcheck disable=SC2034
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$file")"
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

if [ "$provider_data_count" -eq 0 ]; then
count=$((count + 1))
Expand Down
26 changes: 15 additions & 11 deletions src/runner.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,22 +767,17 @@ function bashunit::runner::call_test_functions() {
local -a parsed_data=()
local parsed_data_count=0

# Scan the file once; per-test provider lookups below are pure-bash (#763).
bashunit::helper::build_provider_map "$script"

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
break
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$script")"

# No data provider found
if [ "$provider_data_count" -eq 0 ]; then
# No data provider found: run once without forking to capture provider output.
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
bashunit::runner::wait_for_job_slot
bashunit::runner::run_test "$script" "$fn_name" &
Expand All@@ -793,6 +788,15 @@ function bashunit::runner::call_test_functions() {
continue
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

# Execute the test function for each line of data
local data
for data in "${provider_data[@]+"${provider_data[@]}"}"; do
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/fixtures/provider_map/sample_providers.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Fixture for provider-map scanner tests. Not run as a suite; scanned as text.

# @data_provider provide_at_form
function test_with_at_annotation() {
return 0
}

# data_provider provide_plain_form
function test_without_at() {
return 0
}

# @data_provider provide_two_lines_up
# shellcheck disable=SC2317
function test_annotation_two_lines_up() {
return 0
}

# @data_provider provide_shared
function test_shares_provider_one() {
return 0
}

# @data_provider provide_shared
test_shares_provider_two() {
return 0
}

function test_without_provider() {
return 0
}
45 changes: 45 additions & 0 deletions tests/unit/helpers_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,51 @@ function test_get_provider_data_should_returns_empty_when_not_exists_provider_fu
"$(bashunit::helper::get_provider_data "fake_function_get_not_existing_provider_data" "${BASH_SOURCE[0]}")"
}

FIXTURE_PROVIDER_MAP="$(dirname "${BASH_SOURCE[0]}")/fixtures/provider_map/sample_providers.sh"

function provider_for() {
bashunit::helper::build_provider_map "$1"
bashunit::helper::provider_for_function "$2"
echo "$_BASHUNIT_PROVIDER_FN_OUT"
}

function test_provider_map_resolves_at_annotation() {
assert_same "provide_at_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_with_at_annotation")"
}

function test_provider_map_resolves_plain_annotation_without_at() {
assert_same "provide_plain_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_at")"
}

function test_provider_map_resolves_annotation_two_lines_above_function() {
assert_same "provide_two_lines_up" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_annotation_two_lines_up")"
}

function test_provider_map_resolves_shared_provider_for_both_functions() {
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_one")"
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_two")"
}

function test_provider_map_returns_empty_when_function_has_no_provider() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_provider")"
}

function test_provider_map_returns_empty_for_unknown_function() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_does_not_exist")"
}

function test_provider_map_returns_empty_for_unreadable_script() {
assert_same "" \
"$(provider_for "/no/such/path/nope_test.sh" "test_with_at_annotation")"
}

function test_left_trim() {
assert_same "foo" "$(bashunit::helper::trim " foo")"
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,9 @@

## Unreleased

### Changed
- Faster test execution: each test file's data-provider annotations are scanned once and cached, replacing a per-test `grep`+`sed` probe with a pure-bash lookup (no behaviour change) (#763)

## [0.41.0](https://github.com/TypedDevs/bashunit/compare/0.40.0...0.41.0) - 2026-07-11

### Added
Expand Down
132 changes: 115 additions & 17 deletions src/helpers.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -292,29 +292,123 @@ function bashunit::helper::normalize_variable_name() {
builtin echo "$normalized_string"
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"
# Provider map for the most recently scanned script. Scanning a file once and
# caching the test-function -> provider-function pairs replaces a per-test
# grep+sed fork with a pure-bash lookup on the hot path (issue #763).
_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""

#
# Resolves a script path, applying the issue #529 working-dir fallback.
# Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable).
#
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
local script=$1
# Handle directory changes in set_up_before_script (issue #529)
# If relative path doesn't exist, try with BASHUNIT_WORKING_DIR
if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
script="$BASHUNIT_WORKING_DIR/$script"
fi

if [ ! -f "$script" ]; then
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
return
fi
_BASHUNIT_PROVIDER_RESOLVED_OUT=$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.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_provider_map() {
bashunit::helper::_resolve_provider_script "$1"
local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

if [ -z "$script" ]; then
# Unreadable path: reset to an empty map keyed to this argument so a
# follow-up lookup returns empty without rescanning.
_BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
return
fi

local data_provider_function
data_provider_function=$(
# shellcheck disable=SC1087
grep -B 2 -E "(function[[:space:]]+)?$function_name[[:space:]]*\(\)" "$script" 2>/dev/null |
sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
)
if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
return
fi

_BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()

local count=0
local fn provider
# Single awk pass emits "<fn>\t<provider>" for every function whose
# definition is at most two lines below a `# @data_provider` (or
# `# data_provider`) annotation, mirroring the previous grep -B2 + sed.
while IFS=$'\t' read -r fn provider; do
[ -z "$fn" ] && continue
_BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
_BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
count=$((count + 1))
done < <(awk '
/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
p = $0
sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
sub(/[[:space:]]+$/, "", p)
pending = p
pending_line = NR
next
}
{
if (pending != "" && NR - pending_line <= 2) {
if (match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)) {
fn = $0
sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
sub(/[[:space:]]*\(\).*/, "", fn)
printf "%s\t%s\n", fn, pending
pending = ""
}
} else if (pending != "" && NR - pending_line > 2) {
pending = ""
}
}
' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached provider map.
# Writes the provider-function name (or empty) into _BASHUNIT_PROVIDER_FN_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::provider_for_function() {
local function_name=$1
local i=0
local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
while [ "$i" -lt "$total" ]; do
if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
_BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
return
fi
i=$((i + 1))
done
_BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"

bashunit::helper::build_provider_map "$script"
bashunit::helper::provider_for_function "$function_name"

if [ -n "$data_provider_function" ]; then
bashunit::helper::execute_function_if_exists "$data_provider_function"
if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
fi
}

Expand DownExpand Up@@ -379,15 +473,19 @@ function bashunit::helper::find_total_tests() {
local -a provider_data=()
local provider_data_count=0
local fn_name line
# Scan once; functions without a provider count as 1 with no fork (#763).
bashunit::helper::build_provider_map "$file"
for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
provider_data=()
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
count=$((count + 1))
continue
fi
provider_data_count=0
while IFS=" " read -r line; do
[ -z "$line" ] && continue
# shellcheck disable=SC2034
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$file")"
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

if [ "$provider_data_count" -eq 0 ]; then
count=$((count + 1))
Expand Down
26 changes: 15 additions & 11 deletions src/runner.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,22 +767,17 @@ function bashunit::runner::call_test_functions() {
local -a parsed_data=()
local parsed_data_count=0

# Scan the file once; per-test provider lookups below are pure-bash (#763).
bashunit::helper::build_provider_map "$script"

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
break
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$script")"

# No data provider found
if [ "$provider_data_count" -eq 0 ]; then
# No data provider found: run once without forking to capture provider output.
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
bashunit::runner::wait_for_job_slot
bashunit::runner::run_test "$script" "$fn_name" &
Expand All@@ -793,6 +788,15 @@ function bashunit::runner::call_test_functions() {
continue
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

# Execute the test function for each line of data
local data
for data in "${provider_data[@]+"${provider_data[@]}"}"; do
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/fixtures/provider_map/sample_providers.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Fixture for provider-map scanner tests. Not run as a suite; scanned as text.

# @data_provider provide_at_form
function test_with_at_annotation() {
return 0
}

# data_provider provide_plain_form
function test_without_at() {
return 0
}

# @data_provider provide_two_lines_up
# shellcheck disable=SC2317
function test_annotation_two_lines_up() {
return 0
}

# @data_provider provide_shared
function test_shares_provider_one() {
return 0
}

# @data_provider provide_shared
test_shares_provider_two() {
return 0
}

function test_without_provider() {
return 0
}
45 changes: 45 additions & 0 deletions tests/unit/helpers_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,51 @@ function test_get_provider_data_should_returns_empty_when_not_exists_provider_fu
"$(bashunit::helper::get_provider_data "fake_function_get_not_existing_provider_data" "${BASH_SOURCE[0]}")"
}

FIXTURE_PROVIDER_MAP="$(dirname "${BASH_SOURCE[0]}")/fixtures/provider_map/sample_providers.sh"

function provider_for() {
bashunit::helper::build_provider_map "$1"
bashunit::helper::provider_for_function "$2"
echo "$_BASHUNIT_PROVIDER_FN_OUT"
}

function test_provider_map_resolves_at_annotation() {
assert_same "provide_at_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_with_at_annotation")"
}

function test_provider_map_resolves_plain_annotation_without_at() {
assert_same "provide_plain_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_at")"
}

function test_provider_map_resolves_annotation_two_lines_above_function() {
assert_same "provide_two_lines_up" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_annotation_two_lines_up")"
}

function test_provider_map_resolves_shared_provider_for_both_functions() {
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_one")"
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_two")"
}

function test_provider_map_returns_empty_when_function_has_no_provider() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_provider")"
}

function test_provider_map_returns_empty_for_unknown_function() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_does_not_exist")"
}

function test_provider_map_returns_empty_for_unreadable_script() {
assert_same "" \
"$(provider_for "/no/such/path/nope_test.sh" "test_with_at_annotation")"
}

function test_left_trim() {
assert_same "foo" "$(bashunit::helper::trim " foo")"
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,9 @@

## Unreleased

### Changed
- Faster test execution: each test file's data-provider annotations are scanned once and cached, replacing a per-test `grep`+`sed` probe with a pure-bash lookup (no behaviour change) (#763)

## [0.41.0](https://github.com/TypedDevs/bashunit/compare/0.40.0...0.41.0) - 2026-07-11

### Added
Expand Down
132 changes: 115 additions & 17 deletions src/helpers.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -292,29 +292,123 @@ function bashunit::helper::normalize_variable_name() {
builtin echo "$normalized_string"
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"
# Provider map for the most recently scanned script. Scanning a file once and
# caching the test-function -> provider-function pairs replaces a per-test
# grep+sed fork with a pure-bash lookup on the hot path (issue #763).
_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""

#
# Resolves a script path, applying the issue #529 working-dir fallback.
# Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable).
#
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
local script=$1
# Handle directory changes in set_up_before_script (issue #529)
# If relative path doesn't exist, try with BASHUNIT_WORKING_DIR
if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
script="$BASHUNIT_WORKING_DIR/$script"
fi

if [ ! -f "$script" ]; then
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
return
fi
_BASHUNIT_PROVIDER_RESOLVED_OUT=$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.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_provider_map() {
bashunit::helper::_resolve_provider_script "$1"
local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

if [ -z "$script" ]; then
# Unreadable path: reset to an empty map keyed to this argument so a
# follow-up lookup returns empty without rescanning.
_BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
return
fi

local data_provider_function
data_provider_function=$(
# shellcheck disable=SC1087
grep -B 2 -E "(function[[:space:]]+)?$function_name[[:space:]]*\(\)" "$script" 2>/dev/null |
sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
)
if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
return
fi

_BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()

local count=0
local fn provider
# Single awk pass emits "<fn>\t<provider>" for every function whose
# definition is at most two lines below a `# @data_provider` (or
# `# data_provider`) annotation, mirroring the previous grep -B2 + sed.
while IFS=$'\t' read -r fn provider; do
[ -z "$fn" ] && continue
_BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
_BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
count=$((count + 1))
done < <(awk '
/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
p = $0
sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
sub(/[[:space:]]+$/, "", p)
pending = p
pending_line = NR
next
}
{
if (pending != "" && NR - pending_line <= 2) {
if (match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)) {
fn = $0
sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
sub(/[[:space:]]*\(\).*/, "", fn)
printf "%s\t%s\n", fn, pending
pending = ""
}
} else if (pending != "" && NR - pending_line > 2) {
pending = ""
}
}
' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached provider map.
# Writes the provider-function name (or empty) into _BASHUNIT_PROVIDER_FN_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::provider_for_function() {
local function_name=$1
local i=0
local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
while [ "$i" -lt "$total" ]; do
if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
_BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
return
fi
i=$((i + 1))
done
_BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
local function_name="$1"
local script="$2"

bashunit::helper::build_provider_map "$script"
bashunit::helper::provider_for_function "$function_name"

if [ -n "$data_provider_function" ]; then
bashunit::helper::execute_function_if_exists "$data_provider_function"
if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
fi
}

Expand DownExpand Up@@ -379,15 +473,19 @@ function bashunit::helper::find_total_tests() {
local -a provider_data=()
local provider_data_count=0
local fn_name line
# Scan once; functions without a provider count as 1 with no fork (#763).
bashunit::helper::build_provider_map "$file"
for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
provider_data=()
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
count=$((count + 1))
continue
fi
provider_data_count=0
while IFS=" " read -r line; do
[ -z "$line" ] && continue
# shellcheck disable=SC2034
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$file")"
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

if [ "$provider_data_count" -eq 0 ]; then
count=$((count + 1))
Expand Down
26 changes: 15 additions & 11 deletions src/runner.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,22 +767,17 @@ function bashunit::runner::call_test_functions() {
local -a parsed_data=()
local parsed_data_count=0

# Scan the file once; per-test provider lookups below are pure-bash (#763).
bashunit::helper::build_provider_map "$script"

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
break
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$script")"

# No data provider found
if [ "$provider_data_count" -eq 0 ]; then
# No data provider found: run once without forking to capture provider output.
bashunit::helper::provider_for_function "$fn_name"
if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
bashunit::runner::wait_for_job_slot
bashunit::runner::run_test "$script" "$fn_name" &
Expand All@@ -793,6 +788,15 @@ function bashunit::runner::call_test_functions() {
continue
fi

provider_data=()
provider_data_count=0
local line
while IFS=" " read -r line; do
[ -z "$line" ] && continue
provider_data[provider_data_count]="$line"
provider_data_count=$((provider_data_count + 1))
done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

# Execute the test function for each line of data
local data
for data in "${provider_data[@]+"${provider_data[@]}"}"; do
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/fixtures/provider_map/sample_providers.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Fixture for provider-map scanner tests. Not run as a suite; scanned as text.

# @data_provider provide_at_form
function test_with_at_annotation() {
return 0
}

# data_provider provide_plain_form
function test_without_at() {
return 0
}

# @data_provider provide_two_lines_up
# shellcheck disable=SC2317
function test_annotation_two_lines_up() {
return 0
}

# @data_provider provide_shared
function test_shares_provider_one() {
return 0
}

# @data_provider provide_shared
test_shares_provider_two() {
return 0
}

function test_without_provider() {
return 0
}
45 changes: 45 additions & 0 deletions tests/unit/helpers_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,51 @@ function test_get_provider_data_should_returns_empty_when_not_exists_provider_fu
"$(bashunit::helper::get_provider_data "fake_function_get_not_existing_provider_data" "${BASH_SOURCE[0]}")"
}

FIXTURE_PROVIDER_MAP="$(dirname "${BASH_SOURCE[0]}")/fixtures/provider_map/sample_providers.sh"

function provider_for() {
bashunit::helper::build_provider_map "$1"
bashunit::helper::provider_for_function "$2"
echo "$_BASHUNIT_PROVIDER_FN_OUT"
}

function test_provider_map_resolves_at_annotation() {
assert_same "provide_at_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_with_at_annotation")"
}

function test_provider_map_resolves_plain_annotation_without_at() {
assert_same "provide_plain_form" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_at")"
}

function test_provider_map_resolves_annotation_two_lines_above_function() {
assert_same "provide_two_lines_up" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_annotation_two_lines_up")"
}

function test_provider_map_resolves_shared_provider_for_both_functions() {
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_one")"
assert_same "provide_shared" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_shares_provider_two")"
}

function test_provider_map_returns_empty_when_function_has_no_provider() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_without_provider")"
}

function test_provider_map_returns_empty_for_unknown_function() {
assert_same "" \
"$(provider_for "$FIXTURE_PROVIDER_MAP" "test_does_not_exist")"
}

function test_provider_map_returns_empty_for_unreadable_script() {
assert_same "" \
"$(provider_for "/no/such/path/nope_test.sh" "test_with_at_annotation")"
}

function test_left_trim() {
assert_same "foo" "$(bashunit::helper::trim " foo")"
}
Expand Down
Loading