From cf9e001415088415f801693b130743d69e1d37e3 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Wed, 3 Jun 2026 12:09:04 +0200 Subject: [PATCH 1/3] ci(bash-3.0): add --strict job to the Bash 3.0 matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The strict job ran only on Bash 5 (Ubuntu), so set -e regressions specific to Bash 3.x went undetected — e.g. a bare non-zero call aborting a test body under set -euo pipefail. Add a '--simple --parallel --strict' variant to the Bash 3.0 matrix. --- .github/workflows/tests-bash-3.0.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests-bash-3.0.yml b/.github/workflows/tests-bash-3.0.yml index 3a86efdc..e8097764 100644 --- a/.github/workflows/tests-bash-3.0.yml +++ b/.github/workflows/tests-bash-3.0.yml @@ -84,6 +84,8 @@ jobs: flags: "--simple" - name: "Simple Parallel" flags: "--simple --parallel" + - name: "Strict" + flags: "--simple --parallel --strict" steps: - name: Checkout uses: actions/checkout@v4 From f178d980c956cb00da57a45c4de93a7b5a0a5b12 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Wed, 3 Jun 2026 12:17:01 +0200 Subject: [PATCH 2/3] ci(bash-3.0): scope strict job to unit + functional The acceptance suite drives ./bashunit as a subprocess and has many pre-existing Bash 3.0 strict-mode failures; the full strict run is covered on Bash 5 (Ubuntu - strict). On Bash 3.0, run the strict variant over unit + functional, which guard the set -e correctness of the framework code. --- .github/workflows/tests-bash-3.0.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-bash-3.0.yml b/.github/workflows/tests-bash-3.0.yml index e8097764..28be4f96 100644 --- a/.github/workflows/tests-bash-3.0.yml +++ b/.github/workflows/tests-bash-3.0.yml @@ -78,14 +78,22 @@ jobs: include: - name: "Sequential" flags: "" + paths: "tests/" - name: "Parallel" flags: "--parallel" + paths: "tests/" - name: "Simple" flags: "--simple" + paths: "tests/" - name: "Simple Parallel" flags: "--simple --parallel" + paths: "tests/" + # Strict covers unit + functional on Bash 3.0; the acceptance suite + # drives ./bashunit as a subprocess and is exercised strict on Bash 5 + # (the "Ubuntu - strict" job runs the full tests/). - name: "Strict" flags: "--simple --parallel --strict" + paths: "tests/unit tests/functional" steps: - name: Checkout uses: actions/checkout@v4 @@ -113,4 +121,4 @@ jobs: -v "$(pwd)":/bashunit \ -w /bashunit \ bashunit-bash3 \ - /opt/bash-3.0/bin/bash ./bashunit ${{ matrix.flags }} tests/ + /opt/bash-3.0/bin/bash ./bashunit ${{ matrix.flags }} ${{ matrix.paths }} From 5ea9120a449a6172d8ca7d17d798d55de2b423f7 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Wed, 3 Jun 2026 12:53:25 +0200 Subject: [PATCH 3/3] fix(runner): don't enable Bash 3.0 broken pipefail under --strict Bash 3.0 ships a broken 'set -o pipefail' that reports failing pipelines as successful, which made the framework (snapshot matcher, etc.) and ~59 tests misbehave under --strict on Bash 3.0 only. Apply 'set -eu' always and enable pipefail only on Bash >= 3.1 via _supports_reliable_pipefail. Also guard the snapshot git-diff capture with '|| true'. With this, the full suite passes --strict on Bash 3.0, so the Bash 3.0 matrix gains a '--simple --parallel --strict tests/' job to catch strict regressions on the oldest supported Bash. --- .github/workflows/tests-bash-3.0.yml | 10 +--------- CHANGELOG.md | 1 + src/console_results.sh | 4 +++- src/runner.sh | 21 ++++++++++++++++++++- tests/unit/runner_test.sh | 12 ++++++++++++ 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests-bash-3.0.yml b/.github/workflows/tests-bash-3.0.yml index 28be4f96..e8097764 100644 --- a/.github/workflows/tests-bash-3.0.yml +++ b/.github/workflows/tests-bash-3.0.yml @@ -78,22 +78,14 @@ jobs: include: - name: "Sequential" flags: "" - paths: "tests/" - name: "Parallel" flags: "--parallel" - paths: "tests/" - name: "Simple" flags: "--simple" - paths: "tests/" - name: "Simple Parallel" flags: "--simple --parallel" - paths: "tests/" - # Strict covers unit + functional on Bash 3.0; the acceptance suite - # drives ./bashunit as a subprocess and is exercised strict on Bash 5 - # (the "Ubuntu - strict" job runs the full tests/). - name: "Strict" flags: "--simple --parallel --strict" - paths: "tests/unit tests/functional" steps: - name: Checkout uses: actions/checkout@v4 @@ -121,4 +113,4 @@ jobs: -v "$(pwd)":/bashunit \ -w /bashunit \ bashunit-bash3 \ - /opt/bash-3.0/bin/bash ./bashunit ${{ matrix.flags }} ${{ matrix.paths }} + /opt/bash-3.0/bin/bash ./bashunit ${{ matrix.flags }} tests/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d92ae2d..a47ee9a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Killed tests report the cause: timeout, SIGINT, SIGKILL/OOM, SIGTERM (#683) ### Fixed +- `--strict` no longer enables Bash 3.0's broken `pipefail` (which wrongly reports failing pipelines as successful); `set -eu` is still applied, `pipefail` only on Bash >= 3.1 - `bashunit watch` forwards `--filter` and other flags correctly (#682) - `learn` and coverage use `mktemp -d` for temp directories - `parallel::cleanup` refuses to `rm -rf` outside `*/bashunit/parallel/*` diff --git a/src/console_results.sh b/src/console_results.sh index f835657b..2d4a8c7a 100644 --- a/src/console_results.sh +++ b/src/console_results.sh @@ -337,10 +337,12 @@ function bashunit::console_results::print_failed_snapshot_test() { local actual_file="${snapshot_file}.tmp" echo "$actual_content" >"$actual_file" + # `git diff` exits non-zero when the files differ; guard with `|| true` so + # the assignment does not trip `set -e`/`pipefail` under --strict. local git_diff_output git_diff_output="$(git diff --no-index --word-diff --color=always \ "$snapshot_file" "$actual_file" 2>/dev/null | - tail -n +6 | sed "s/^/ /")" + tail -n +6 | sed "s/^/ /")" || true line="$line$git_diff_output" rm "$actual_file" diff --git a/src/runner.sh b/src/runner.sh index 7bc201d5..fd71e0bf 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -12,6 +12,19 @@ function bashunit::runner::restore_workdir() { cd "$BASHUNIT_WORKING_DIR" 2>/dev/null || true } +## +# Whether the running Bash has a reliable `set -o pipefail`. Bash 3.0 shipped a +# broken pipefail (a failing pipeline can wrongly report success), which makes +# `--strict` unsound; on 3.0 we fall back to `set -eu` without pipefail. +# Returns: 0 when pipefail is reliable (Bash >= 3.1), 1 otherwise. +## +function bashunit::runner::_supports_reliable_pipefail() { + if [ "${BASH_VERSINFO[0]:-0}" -gt 3 ]; then + return 0 + fi + [ "${BASH_VERSINFO[0]:-0}" -eq 3 ] && [ "${BASH_VERSINFO[1]:-0}" -ge 1 ] +} + # Caches BASHUNIT_COVERAGE into _BASHUNIT_COVERAGE_ON ("1"|"0") so hot-path checks # avoid a function dispatch per call. Call once after arg parsing; tests that # toggle BASHUNIT_COVERAGE mid-run must call this again to refresh. @@ -887,7 +900,13 @@ function bashunit::runner::run_test() { # Apply shell mode setting for test execution if bashunit::env::is_strict_mode_enabled; then - set -euo pipefail + set -eu + # Bash 3.0 ships a broken pipefail; only enable it where it is reliable. + if bashunit::runner::_supports_reliable_pipefail; then + set -o pipefail + else + set +o pipefail + fi else set +euo pipefail fi diff --git a/tests/unit/runner_test.sh b/tests/unit/runner_test.sh index b9d7a58a..62734c26 100644 --- a/tests/unit/runner_test.sh +++ b/tests/unit/runner_test.sh @@ -232,3 +232,15 @@ function test_classify_kill_signal_generic_signal() { function test_classify_kill_signal_empty_for_normal_exit() { assert_empty "$(bashunit::runner::classify_kill_signal 1)" } + +function test_supports_reliable_pipefail_matches_bash_version() { + # Reliable on Bash >= 3.1; Bash 3.0 ships a broken pipefail. + local expected_rc=0 + if [ "${BASH_VERSINFO[0]}" -eq 3 ] && [ "${BASH_VERSINFO[1]}" -eq 0 ]; then + expected_rc=1 + fi + + local actual_rc=0 + bashunit::runner::_supports_reliable_pipefail || actual_rc=$? + assert_same "$expected_rc" "$actual_rc" +}