diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d8aa2bc..9a907078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/runner/discovery.sh b/src/runner/discovery.sh index 5ce200fd..2020a603 100644 --- a/src/runner/discovery.sh +++ b/src/runner/discovery.sh @@ -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 diff --git a/src/runner/parallel.sh b/src/runner/parallel.sh index 149af487..5575f0fd 100644 --- a/src/runner/parallel.sh +++ b/src/runner/parallel.sh @@ -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 diff --git a/tests/acceptance/bashunit_run_forks_test.sh b/tests/acceptance/bashunit_run_forks_test.sh index 8d9c2db2..67d7fc2b 100644 --- a/tests/acceptance/bashunit_run_forks_test.sh +++ b/tests/acceptance/bashunit_run_forks_test.sh @@ -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