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 number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Fixed
- Captured parallel output no longer waits for an inactive spinner's sleep child to close its pipes (#1368)

## [0.51.0](https://github.com/TypedDevs/bashunit/compare/0.50.1...0.51.0) - 2026-09-18

### Changed
Expand Down
21 changes: 8 additions & 13 deletions src/runner/discovery.sh
Original file line number Diff line number Diff line change
Expand Up @@ -287,22 +287,17 @@ function bashunit::runner::load_test_files() {
# "No tests found" into what must be a clean list of ids (#1007).
if bashunit::parallel::is_enabled && ! bashunit::env::is_list_enabled; then
wait
bashunit::runner::spinner &
local spinner_pid=$!
bashunit::state::aggregate_parallel_results "$TEMP_DIR_PARALLEL_TEST_SUITE"
# Kill the spinner once the aggregation finishes
disown "$spinner_pid" 2>/dev/null || true
kill "$spinner_pid" 2>/dev/null || true
# Clear the spinner output, but only where it was drawn. The spinner draws
# nothing when stdout is not a terminal, under --no-progress, or under a
# machine --output format; erasing regardless emitted a literal "\r \r"
# into every piped run, which is every CI log -- and under a machine format
# those bytes landed in front of the report, where an XML declaration must
# start the document. The conditions have to match the ones the spinner
# itself checks, or the two drift apart again.
local spinner_pid=""
if [ -t 1 ] &&
! bashunit::env::is_no_progress_enabled &&
! bashunit::env::is_machine_output_enabled; then
bashunit::runner::spinner &
spinner_pid=$!
fi
bashunit::state::aggregate_parallel_results "$TEMP_DIR_PARALLEL_TEST_SUITE"
if [ -n "$spinner_pid" ]; then
disown "$spinner_pid" 2>/dev/null || true
kill "$spinner_pid" 2>/dev/null || true
printf "\r \r"
fi

Expand Down
12 changes: 1 addition & 11 deletions src/runner/parallel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,7 @@ function bashunit::runner::wait_for_job_slot() {
}

function bashunit::runner::spinner() {
# Only show spinner when output is to a terminal
if [ ! -t 1 ]; then
# Not a terminal, just wait silently
while true; do sleep 1; done
return
fi

# Don't show spinner in no-progress mode, nor when stdout carries a machine
# format the frames would corrupt.
if bashunit::env::is_no_progress_enabled || bashunit::env::is_machine_output_enabled; then
while true; do sleep 1; done
if [ ! -t 1 ] || bashunit::env::is_no_progress_enabled || bashunit::env::is_machine_output_enabled; then
return
fi

Expand Down
45 changes: 45 additions & 0 deletions tests/acceptance/bashunit_run_forks_test.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail

function test_parallel_runs_do_not_start_a_spinner_without_visible_progress() {
local dir
dir="$(bashunit::temp_dir)"
local fixture="$dir/inactive_spinner_test.sh"
printf 'function test_without_spinner() { assert_same 1 1; }\n' >"$fixture"

local bootstrap="$dir/bootstrap.sh"
cat >"$bootstrap" <<'BOOTSTRAP'
function bashunit::runner::spinner() {
printf 'started\n' >"$BASHUNIT_SPINNER_MARKER"
}
# Bash 3.0 cannot target names containing :: with declare -f.
definition=$(declare -f | awk '
/^bashunit::state::aggregate_parallel_results \(\)/ { copy = 1 }
copy { print }
copy && /^}$/ { copy = 0 }
')
definition=${definition/bashunit::state::aggregate_parallel_results/bashunit_probe_aggregate}
eval "$definition"
function bashunit::state::aggregate_parallel_results() {
wait
bashunit_probe_aggregate "$@"
}
BOOTSTRAP

local mode output marker
local flags
for mode in normal no_progress json; do
flags=()
case "$mode" in
no_progress) flags=(--no-progress) ;;
json) flags=(--output json) ;;
esac
marker="$dir/$mode"
output=$(BASHUNIT_BOOTSTRAP="$bootstrap" BASHUNIT_SPINNER_MARKER="$marker" \
./bashunit --parallel --simple --skip-env-file ${flags+"${flags[@]}"} "$fixture" 2>&1)
assert_successful_code "$?"
assert_file_not_exists "$marker"
case "$mode" in
json) assert_contains '"passed": 1' "$output" ;;
*) assert_contains '1 passed' "$output" ;;
esac
done
}

# Regression guard for the per-file run path. Running a test file used to fork
# `grep` twice: once in the runner to scan sourcing stderr for "syntax error"/
# "unexpected EOF", and once in discovery to decide whether to also match the
Expand Down
Loading