From da37b868278f92656bc0bce4984ce794531d99e9 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 2 Oct 2026 20:15:26 +0200 Subject: [PATCH 1/3] fix(runner): retain synchronous provider results Closes #1377 --- CHANGELOG.md | 1 + src/runner/exec.sh | 10 +- .../bashunit_provider_transport_test.sh | 110 ++++++++++++++++++ 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d2278f..619290c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - JSON reports write ordinary filenames, test names and empty messages without per-field processes, preserving existing escaping (#1369) ### Fixed +- Synchronous provider rows retain separate results in parallel runs, so later passing rows cannot hide failures (#1377) - A late timeout signal no longer interrupts `tear_down`; hanging cleanup remains bounded by the watchdog (#1376) - Piped parallel output no longer waits for an inactive spinner's sleep process (#1368) diff --git a/src/runner/exec.sh b/src/runner/exec.sh index 4a9962cc..c9e9a11c 100644 --- a/src/runner/exec.sh +++ b/src/runner/exec.sh @@ -135,7 +135,7 @@ function bashunit::runner::call_test_functions() { local -a parsed_data=() local parsed_data_count=0 local provider_arg_file="" - # Monotonic within this file; names each parallel worker's .result file. + # Monotonic within this file, including tests executed synchronously. local _test_ordinal=0 # Scan the file once; per-test provider lookups below are pure-bash (#763). @@ -178,10 +178,10 @@ function bashunit::runner::call_test_functions() { # 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 + _test_ordinal=$((_test_ordinal + 1)) + _BASHUNIT_RUNNER_RESULT_ORDINAL=$_test_ordinal if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then bashunit::runner::wait_for_job_slot - _test_ordinal=$((_test_ordinal + 1)) - _BASHUNIT_RUNNER_RESULT_ORDINAL=$_test_ordinal bashunit::runner::run_test_parallel "$script" "$fn_name" & _BASHUNIT_WORKER_TEST_PIDS="$_BASHUNIT_WORKER_TEST_PIDS $!" else @@ -256,10 +256,10 @@ function bashunit::runner::call_test_functions() { "$script" "$fn_name" "data provider '$_BASHUNIT_PROVIDER_FN_OUT' $transport_error" break fi + _test_ordinal=$((_test_ordinal + 1)) + _BASHUNIT_RUNNER_RESULT_ORDINAL=$_test_ordinal if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then bashunit::runner::wait_for_job_slot - _test_ordinal=$((_test_ordinal + 1)) - _BASHUNIT_RUNNER_RESULT_ORDINAL=$_test_ordinal bashunit::runner::run_test_parallel "$script" "$fn_name" ${parsed_data+"${parsed_data[@]}"} & _BASHUNIT_WORKER_TEST_PIDS="$_BASHUNIT_WORKER_TEST_PIDS $!" else diff --git a/tests/acceptance/bashunit_provider_transport_test.sh b/tests/acceptance/bashunit_provider_transport_test.sh index a997dcbf..7602b98c 100644 --- a/tests/acceptance/bashunit_provider_transport_test.sh +++ b/tests/acceptance/bashunit_provider_transport_test.sh @@ -135,3 +135,113 @@ function test_row_after_scratch_loss() { assert_empty "$(<"$dir/public_files")" assert_not_contains "No such file or directory" "$output" } + +function provide_synchronous_modes() { + bashunit::data_set --no-parallel false + bashunit::data_set --parallel false + bashunit::data_set --no-parallel true + bashunit::data_set --parallel true +} + +# @data_provider provide_synchronous_modes +function test_synchronous_provider_failures_survive_a_later_passing_row() { + local mode="$1" strict="$2" + local dir="$PROVIDER_TRANSPORT_FIXTURES/ordinal_${mode#--}_$strict" + mkdir -p "$dir" + printf '%s\n' '# bashunit: no-parallel-tests' >"$dir/ordinal_test.sh" + cat >>"$dir/ordinal_test.sh" <<'TEST' +function provide_ordinal_rows() { bashunit::data_set bad; bashunit::data_set good; } +# @data_provider provide_ordinal_rows +function test_ordinal_row() { assert_same good "$1"; } +TEST + + local -a options=("$mode") + if [ "$strict" = true ]; then + options[1]=--strict + fi + local output code=0 + output="$(./bashunit "${options[@]}" --skip-env-file --no-color \ + --report-json "$dir/report.json" "$dir/ordinal_test.sh" 2>&1)" || code=$? + + assert_same 1 "$code" + assert_contains "1 passed" "$output" + assert_contains "1 failed" "$output" + assert_contains "2 total" "$output" + assert_file_contains "$dir/report.json" '"total": 2, "passed": 1, "failed": 1' +} + +# @data_provider provide_synchronous_modes +function test_synchronous_provider_dispatch_preserves_retries_repeats_reports_and_cleanup() { + local mode="$1" strict="$2" + local dir="$PROVIDER_TRANSPORT_FIXTURES/lifecycle_${mode#--}_$strict" + mkdir -p "$dir/tmp" + printf '%s\n' '# bashunit: no-parallel-tests' >"$dir/lifecycle_test.sh" + cat >>"$dir/lifecycle_test.sh" <<'TEST' +function set_up_before_script() { + SYNC_SCRIPT_TEMP=$(bashunit::temp_file script) + printf '%s\n' "$SYNC_SCRIPT_TEMP" >>"$SYNC_TEMP_PATHS" +} +function set_up() { + if [ -f "$SYNC_ACTIVE" ]; then + printf 'overlap\n' >>"$SYNC_EVENTS" + fi + : >"$SYNC_ACTIVE" + printf 'setup\n' >>"$SYNC_HOOKS" +} +function tear_down() { + rm -f "$SYNC_ACTIVE" + printf 'teardown\n' >>"$SYNC_HOOKS" +} +function tear_down_after_script() { + assert_file_exists "$SYNC_SCRIPT_TEMP" + printf 'script teardown\n' >>"$SYNC_EVENTS" +} +function test_before_rows() { assert_true true; printf 'before\n' >>"$SYNC_EVENTS"; } +function provide_sync_rows() { bashunit::data_set bad; bashunit::data_set good; } +# @data_provider provide_sync_rows +function test_sync_row() { + local temp + temp=$(bashunit::temp_file row) + printf '%s\n' "$temp" >>"$SYNC_TEMP_PATHS" + printf '%s\n' "$1" >>"$SYNC_EVENTS" + assert_same good "$1" +} +function test_after_rows() { assert_true true; printf 'after\n' >>"$SYNC_EVENTS"; } +TEST + + local -a options=("$mode") + if [ "$strict" = true ]; then + options[1]=--strict + fi + local output code=0 + output="$(TMPDIR="$dir/tmp/" SYNC_EVENTS="$dir/events" SYNC_HOOKS="$dir/hooks" \ + SYNC_ACTIVE="$dir/active" SYNC_TEMP_PATHS="$dir/paths" \ + ./bashunit "${options[@]}" --skip-env-file --no-color --repeat 3 --retry 1 \ + --report-json "$dir/report.json" --report-junit "$dir/report.xml" --report-tap "$dir/report.tap" \ + "$dir/lifecycle_test.sh" 2>&1)" || code=$? + + assert_same 1 "$code" + assert_contains "3 passed" "$output" + assert_contains "1 failed" "$output" + assert_contains "4 total" "$output" + assert_file_contains "$dir/report.json" '"total": 4, "passed": 3, "failed": 1' + assert_same 4 "$("$GREP" -c ' Date: Fri, 2 Oct 2026 21:30:46 +0200 Subject: [PATCH 2/3] test(runner): ignore exited cancellation bodies Closes #1385 --- CHANGELOG.md | 1 + tests/acceptance/bashunit_timeout_test.sh | 30 +++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb82cd0..4b04dad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - JSON reports write ordinary filenames, test names and empty messages without per-field processes, preserving existing escaping (#1369) ### Fixed +- Cancellation regression checks distinguish exited bodies awaiting reaping from running cleanup (#1385) - Synchronous provider rows retain separate results in parallel runs, so later passing rows cannot hide failures (#1377) - Linux test jobs install the standalone build optimizer required by beta installer acceptance tests (#1383) - A late timeout signal no longer interrupts `tear_down`; hanging cleanup remains bounded by the watchdog (#1376) diff --git a/tests/acceptance/bashunit_timeout_test.sh b/tests/acceptance/bashunit_timeout_test.sh index deee66a0..7ae69943 100644 --- a/tests/acceptance/bashunit_timeout_test.sh +++ b/tests/acceptance/bashunit_timeout_test.sh @@ -244,6 +244,24 @@ TEST assert_file_not_exists "$marker.completed" } +function _timeout_body_is_running() { + local body_pid="$1" + kill -0 "$body_pid" 2>/dev/null || return 1 + local state + if [ -r "/proc/$body_pid/stat" ]; then + IFS= read -r state 2>/dev/null <"/proc/$body_pid/stat" || return 1 + state=${state##*) } + state=${state%% *} + else + state=$(ps -p "$body_pid" -o stat= 2>/dev/null) || return 1 + fi + # A cancelled runner can leave an exited body awaiting reaping by its new parent. + case "$state" in + *Z* | "") return 1 ;; + esac + return 0 +} + function _assert_cancellation_stops_timeout_teardown() { if bashunit::check_os::is_windows; then bashunit::skip "Unix process-group signals" @@ -298,17 +316,25 @@ TEST if [ -f "$marker.body" ]; then IFS= read -r body_pid <"$marker.body" ticks=0 - while kill -0 "$body_pid" 2>/dev/null && [ "$ticks" -lt 60 ]; do + while _timeout_body_is_running "$body_pid" && [ "$ticks" -lt 60 ]; do sleep 0.05 ticks=$((ticks + 1)) done - if kill -0 "$body_pid" 2>/dev/null; then + if _timeout_body_is_running "$body_pid"; then + if [ -r "/proc/$body_pid/stat" ]; then + cat "/proc/$body_pid/stat" 2>/dev/null || true + else + ps -p "$body_pid" -o pid=,ppid=,pgid=,stat= 2>/dev/null || true + fi : >"$marker.forced" kill -KILL -"$body_pid" 2>/dev/null || true fi fi ) || true + if [ ! -f "$marker.started" ] || [ -f "$marker.forced" ]; then + printf '%s\n' "$output" + fi assert_file_exists "$marker.started" assert_contains "Caught Ctrl-C, killing all child processes" "$output" assert_file_not_exists "$marker.forced" From 3d9ae436d85d582c31103585ba43842599a29594 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 2 Oct 2026 21:30:51 +0200 Subject: [PATCH 3/3] fix(ci): name skipped acceptance jobs clearly --- .github/workflows/tests.yml | 8 ++++---- CHANGELOG.md | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60fbb7d2..3d03a77f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -146,7 +146,7 @@ jobs: run: ./bashunit --parallel --jobs 4 ${{ matrix.test_path }} macos-acceptance: - name: "macOS - acceptance (${{ matrix.name }})" + name: "macOS - acceptance" if: github.event_name == 'push' runs-on: macos-latest timeout-minutes: 10 @@ -169,7 +169,7 @@ jobs: - name: Setup Config run: cp .env.example .env - - name: Run Tests + - name: "Run acceptance shard ${{ matrix.name }}" run: ./bashunit --parallel --jobs 4 ${{ matrix.test_path }} windows: @@ -207,7 +207,7 @@ jobs: run: ./bashunit --parallel --jobs 4 ${{ matrix.test_path }} windows-acceptance: - name: "windows-acceptance (${{ matrix.name }})" + name: "Windows - acceptance" if: github.event_name == 'push' timeout-minutes: 10 runs-on: windows-latest @@ -231,7 +231,7 @@ jobs: shell: bash run: cp .env.example .env - - name: Run tests + - name: "Run acceptance shard ${{ matrix.name }}" shell: bash run: ./bashunit --parallel --jobs 4 ${{ matrix.test_path }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b04dad4..b5e66d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - JSON reports write ordinary filenames, test names and empty messages without per-field processes, preserving existing escaping (#1369) ### Fixed +- Skipped macOS and Windows acceptance CI jobs have readable names, with shard labels in their test steps - Cancellation regression checks distinguish exited bodies awaiting reaping from running cleanup (#1385) - Synchronous provider rows retain separate results in parallel runs, so later passing rows cannot hide failures (#1377) - Linux test jobs install the standalone build optimizer required by beta installer acceptance tests (#1383)