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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- `--jobs auto` / `-j auto` caps parallel concurrency at the CPU core count (portable across Linux/macOS/BSD); the default stays unlimited (#766)

### Changed
- Faster test execution: removed the remaining round-2 fork leftovers — `wait_for_job_slot` counts running background jobs without forking `wc` on every poll (145 -> 0 `wc` forks across a small parallel run), and `check_duplicate_functions` folds its per-file grep+awk+sort+uniq scan into a single awk pass. No behaviour change (#761)
- `assert_equals`/`assert_same` failures with multiline values now render a git word-diff below the header (requires git, opt out with `BASHUNIT_NO_DIFF=true`, respects `--no-color`); machine reports keep the raw values (#777)
- Faster `assert_match_snapshot`: fork-free success path, byte-compatible snapshots; 500 snapshot assertions ~7.5s -> ~3.0s on bash 3.2 (#775)
- Faster `--tag`/`--exclude-tag`: per-file `# @tag` annotations scanned once and cached; 100 tagged tests ~2.92s -> ~0.68s on bash 3.2 (#773)
Expand Down
35 changes: 21 additions & 14 deletions src/helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -213,22 +213,29 @@ function bashunit::helper::check_duplicate_functions() {
script="$BASHUNIT_WORKING_DIR/$script"
fi

local filtered_lines
filtered_lines=$(grep -E '^[[:space:]]*(function[[:space:]]+)?test[a-zA-Z_][a-zA-Z0-9_]*\s*\(\)\s*\{' "$script")

local function_names
function_names=$(echo "$filtered_lines" | awk '{
for (i=1; i<=NF; i++) {
if ($i ~ /^test[a-zA-Z_][a-zA-Z0-9_]*\(\)$/) {
gsub(/\(\)/, "", $i)
print $i
break
# One awk pass over the file finds each test-function definition and emits only
# the names seen more than once, folding the former grep + awk + sort + uniq
# chain into a single awk + sort (#761).
local duplicates
duplicates=$(awk '
/^[[:space:]]*(function[[:space:]]+)?test[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(\)[[:space:]]*\{/ {
for (i = 1; i <= NF; i++) {
if ($i ~ /^test[a-zA-Z_][a-zA-Z0-9_]*\(\)$/) {
name = $i
gsub(/\(\)/, "", name)
if (++seen[name] == 2) {
dup[name] = 1
}
break
}
}
}
}')

local duplicates
duplicates=$(echo "$function_names" | sort | uniq -d)
END {
for (name in dup) {
print name
}
}
' "$script" | sort)
if [ -n "$duplicates" ]; then
bashunit::state::set_duplicated_functions_merged "$script" "$duplicates"
return 1
Expand Down
25 changes: 21 additions & 4 deletions src/runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,22 @@ function bashunit::runner::_supports_wait_n() {
return 1
}

_BASHUNIT_RUNNER_RUNNING_JOBS_OUT=0

# Counts running background jobs into _BASHUNIT_RUNNER_RUNNING_JOBS_OUT. `jobs -pr`
# still needs one command substitution, but the line count is pure-bash, so this
# drops the extra `wc` fork per poll iteration on the parallel hot path (#761).
function bashunit::runner::_count_running_jobs() {
local running
running=$(jobs -pr)
if [ -z "$running" ]; then
_BASHUNIT_RUNNER_RUNNING_JOBS_OUT=0
return
fi
local newlines="${running//[!$'\n']/}"
_BASHUNIT_RUNNER_RUNNING_JOBS_OUT=$((${#newlines} + 1))
}

function bashunit::runner::wait_for_job_slot() {
local max_jobs="${BASHUNIT_PARALLEL_JOBS:-0}"
if [ "$max_jobs" -le 0 ]; then
Expand All @@ -302,8 +318,10 @@ function bashunit::runner::wait_for_job_slot() {

if bashunit::runner::_supports_wait_n; then
# Bash 4.3+: block until any child exits. No polling, no sleep latency.
while [ "$(jobs -r | wc -l)" -ge "$max_jobs" ]; do
bashunit::runner::_count_running_jobs
while [ "$_BASHUNIT_RUNNER_RUNNING_JOBS_OUT" -ge "$max_jobs" ]; do
wait -n 2>/dev/null || break
bashunit::runner::_count_running_jobs
done
return 0
fi
Expand All @@ -313,9 +331,8 @@ function bashunit::runner::wait_for_job_slot() {
local delay="0.05"
local iterations=0
while true; do
local running_jobs
running_jobs=$(jobs -r | wc -l)
if [ "$running_jobs" -lt "$max_jobs" ]; then
bashunit::runner::_count_running_jobs
if [ "$_BASHUNIT_RUNNER_RUNNING_JOBS_OUT" -lt "$max_jobs" ]; then
break
fi
sleep "$delay"
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/fixtures/multiple_duplicate_functions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# shellcheck disable=SC2317
# shellcheck disable=SC2329

function test_beta() {
echo "beta"
}

function test_alpha() {
echo "alpha"
}

function test_beta() {
echo "beta duplicate"
}

test_alpha() {
echo "alpha duplicate without function keyword"
}

function test_gamma() {
echo "gamma appears once"
}
14 changes: 14 additions & 0 deletions tests/unit/helpers_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@ function test_check_duplicate_functions_without_function_keyword() {
assert_general_error "$(bashunit::helper::check_duplicate_functions "$file")"
}

function test_check_duplicate_functions_reports_each_duplicate_name_sorted() {
local file names
file="$(bashunit::current_dir)/fixtures/multiple_duplicate_functions.sh"

# Isolate the state mutation in a subshell; report the stored duplicate names.
names=$(
bashunit::helper::check_duplicate_functions "$file" >/dev/null 2>&1 || true
bashunit::state::get_duplicated_function_names
)

assert_same "test_alpha
test_beta" "$names"
}

function test_generate_id_uses_pid_suffix_when_not_parallel() {
local _orig="${BASHUNIT_PARALLEL_RUN-}"
export BASHUNIT_PARALLEL_RUN=false
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/parallel_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,32 @@ function test_wait_for_job_slot_releases_when_background_job_finishes() {
assert_successful_code "$?"
}

function test_count_running_jobs_reflects_started_jobs() {
bashunit::runner::_count_running_jobs
local before=$_BASHUNIT_RUNNER_RUNNING_JOBS_OUT

sleep 30 &
local p1=$!
sleep 30 &
local p2=$!

bashunit::runner::_count_running_jobs
local after=$_BASHUNIT_RUNNER_RUNNING_JOBS_OUT

kill "$p1" "$p2" 2>/dev/null || true
wait "$p1" "$p2" 2>/dev/null || true

assert_same "2" "$((after - before))"
}

function test_count_running_jobs_is_zero_with_no_background_jobs() {
# A fresh subshell has no running jobs, so the count must be exactly 0.
local out
out=$(bashunit::runner::_count_running_jobs && echo "$_BASHUNIT_RUNNER_RUNNING_JOBS_OUT")

assert_same "0" "$out"
}

# === is_enabled tests ===

function test_parallel_enabled_on_windows() {
Expand Down
Loading