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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
### Fixed
- A failing `set_up_before_script`/`set_up` is now reported consistently: previously the same failure could be attributed (plain failing command), silently ignored (failing `cmd && var=x` guard on Bash 3.2 — the hook's real exit status was discarded), or silently counted with an off-by-one total and no message (Bash ≥ 4, where the ERR trap re-fired in the runner's scope). Every test in the affected file is now marked failed with the hook message, totals match the declared count, and the suite continues — a strict test file can no longer abort the whole run mid-suite via its top-level `set -euo pipefail` (#836)
- `bashunit::env::is_diff_enabled` and `bashunit::cleanup_testcase_temp_files` no longer break callers under `set -u`/`set -e` (unbound read after `unset BASHUNIT_NO_DIFF`; skip path returned 1) (#836)
- `--stop-on-failure`, `--log-junit`, `--report-html`, `--report-tap` and `--report-json` no longer leak into nested bashunit runs via the environment: a script under test that itself calls bashunit used to inherit the parent's stop-on-failure mode (aborting before the rerun cache was written) and overwrite the parent's report files (#834)
- No run-mode flag leaks into nested bashunit runs via the environment anymore: after #834 fixed five flags, the remaining ~40 (`--parallel`, `--simple`, `--strict`, `--filter`, `--retry`, `--seed`, `--watch`, …) are now also this-process-only, so a script under test that itself calls bashunit gets default behavior instead of silently inheriting the parent's output mode, strictness, parallelism, timeouts or report paths. Env-var configuration (`BASHUNIT_*=… bashunit`) is unchanged (#834, #837)
- `./bashunit bench` works again when running from a repository checkout: the dev entrypoint never sourced `src/benchmark.sh`, so every bench run crashed with `command not found` (the built binary was unaffected) (#834)
- `./build.sh --verify` now exits non-zero when the built binary fails the test suite — previously a red verification run still reported success to CI. The gate exposed that verification had been silently crashing mid-suite since 2025-06: six test files resolved repo paths through the running binary's root dir, which has no `src/` in a build folder; they now resolve paths relative to the test file or repo cwd (#834)
- Snapshot placeholders (`::ignore::`) now work on systems without perl; multi-line placeholders still need perl (#823)
Expand Down
7 changes: 5 additions & 2 deletions bashunit
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ export BASHUNIT_WORKING_DIR
for arg in "$@"; do
case "$arg" in
--skip-env-file)
export BASHUNIT_SKIP_ENV_FILE=true
# this-process-only (see the flag-parse loop in src/main.sh) (#837)
BASHUNIT_SKIP_ENV_FILE=true
export -n BASHUNIT_SKIP_ENV_FILE
;;
-l | --login)
export BASHUNIT_LOGIN_SHELL=true
BASHUNIT_LOGIN_SHELL=true
export -n BASHUNIT_LOGIN_SHELL
;;
--no-color)
# shellcheck disable=SC2034
Expand Down
136 changes: 94 additions & 42 deletions src/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ function bashunit::main::set_shard_or_exit() {
exit 1
fi

export BASHUNIT_SHARD_INDEX="$index"
export BASHUNIT_SHARD_TOTAL="$total"
BASHUNIT_SHARD_INDEX="$index"
export -n BASHUNIT_SHARD_INDEX
BASHUNIT_SHARD_TOTAL="$total"
export -n BASHUNIT_SHARD_TOTAL
}

#############################
Expand All @@ -46,7 +48,18 @@ function bashunit::main::cmd_test() {
local assert_fn=""
local _bashunit_coverage_opt_set=false

# Parse test-specific options
# Parse test-specific options.
#
# Flag branches assign WITHOUT export and strip the export attribute with
# `export -n`: run-mode flags are this-process-only. Everything that reads
# them (runner, reporters, parallel workers) runs in this shell or its
# subshells, which inherit unexported variables — while exec'd children
# (nested bashunit runs: bashunit's own acceptance suite under
# `build.sh --verify`, or a user's script under test that calls bashunit)
# must NOT inherit the parent's flags (#834, #837). The explicit `export -n`
# also clears an export attribute stamped by an allexport .env load.
# Pair any newly exported-by-necessity flag with a comment naming the exec'd
# consumer, and extend tests/acceptance/fixtures/flag_env_leak/leak_probe.sh.
while [ $# -gt 0 ]; do
case "$1" in
-a | --assert)
Expand Down Expand Up @@ -74,13 +87,16 @@ function bashunit::main::cmd_test() {
shift
;;
-s | --simple)
export BASHUNIT_SIMPLE_OUTPUT=true
BASHUNIT_SIMPLE_OUTPUT=true
export -n BASHUNIT_SIMPLE_OUTPUT
;;
--detailed)
export BASHUNIT_SIMPLE_OUTPUT=false
BASHUNIT_SIMPLE_OUTPUT=false
export -n BASHUNIT_SIMPLE_OUTPUT
;;
--output)
export BASHUNIT_OUTPUT_FORMAT="$2"
BASHUNIT_OUTPUT_FORMAT="$2"
export -n BASHUNIT_OUTPUT_FORMAT
shift
;;
--debug)
Expand All @@ -101,53 +117,65 @@ function bashunit::main::cmd_test() {
export -n BASHUNIT_STOP_ON_FAILURE
;;
-p | --parallel)
export BASHUNIT_PARALLEL_RUN=true
BASHUNIT_PARALLEL_RUN=true
export -n BASHUNIT_PARALLEL_RUN
;;
-j | --jobs)
export BASHUNIT_PARALLEL_RUN=true
BASHUNIT_PARALLEL_RUN=true
export -n BASHUNIT_PARALLEL_RUN
# "auto" caps at the detected core count; wait_for_job_slot needs an
# integer, so resolve it here rather than leaking the string downstream.
if [ "$2" = "auto" ]; then
export BASHUNIT_PARALLEL_JOBS="$(bashunit::check_os::nproc)"
BASHUNIT_PARALLEL_JOBS="$(bashunit::check_os::nproc)"
export -n BASHUNIT_PARALLEL_JOBS
else
export BASHUNIT_PARALLEL_JOBS="$2"
BASHUNIT_PARALLEL_JOBS="$2"
export -n BASHUNIT_PARALLEL_JOBS
fi
shift
;;
--no-parallel)
export BASHUNIT_PARALLEL_RUN=false
BASHUNIT_PARALLEL_RUN=false
export -n BASHUNIT_PARALLEL_RUN
;;
--test-timeout)
export BASHUNIT_TEST_TIMEOUT="$2"
BASHUNIT_TEST_TIMEOUT="$2"
export -n BASHUNIT_TEST_TIMEOUT
shift
;;
--retry)
export BASHUNIT_RETRY="$2"
BASHUNIT_RETRY="$2"
export -n BASHUNIT_RETRY
shift
;;
--random-order)
export BASHUNIT_RANDOM_ORDER=true
BASHUNIT_RANDOM_ORDER=true
export -n BASHUNIT_RANDOM_ORDER
;;
--seed)
export BASHUNIT_SEED="$2"
BASHUNIT_SEED="$2"
export -n BASHUNIT_SEED
shift
;;
--shard)
bashunit::main::set_shard_or_exit "$2"
shift
;;
--rerun-failed)
export BASHUNIT_RERUN_FAILED=true
BASHUNIT_RERUN_FAILED=true
export -n BASHUNIT_RERUN_FAILED
;;
-w | --watch)
export BASHUNIT_WATCH_MODE=true
BASHUNIT_WATCH_MODE=true
export -n BASHUNIT_WATCH_MODE
;;
-e | --env | --boot)
# Support: --env "bootstrap.sh arg1 arg2"
local boot_file="${2%% *}"
local boot_args="${2#* }"
if [ "$boot_args" != "$2" ]; then
export BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
export -n BASHUNIT_BOOTSTRAP_ARGS
fi
# Export all variables from the env file so they're available in subshells
# (e.g., process substitution used in load_test_files)
Expand All @@ -169,7 +197,8 @@ function bashunit::main::cmd_test() {
shift
;;
--log-gha)
export BASHUNIT_LOG_GHA="$2"
BASHUNIT_LOG_GHA="$2"
export -n BASHUNIT_LOG_GHA
shift
;;
-r | --report-html)
Expand All @@ -188,50 +217,64 @@ function bashunit::main::cmd_test() {
shift
;;
--no-output)
export BASHUNIT_NO_OUTPUT=true
BASHUNIT_NO_OUTPUT=true
export -n BASHUNIT_NO_OUTPUT
;;
-vvv | --verbose)
export BASHUNIT_VERBOSE=true
BASHUNIT_VERBOSE=true
export -n BASHUNIT_VERBOSE
;;
-h | --help)
bashunit::console_header::print_test_help
exit 0
;;
--show-skipped)
export BASHUNIT_SHOW_SKIPPED=true
BASHUNIT_SHOW_SKIPPED=true
export -n BASHUNIT_SHOW_SKIPPED
;;
--show-incomplete)
export BASHUNIT_SHOW_INCOMPLETE=true
BASHUNIT_SHOW_INCOMPLETE=true
export -n BASHUNIT_SHOW_INCOMPLETE
;;
--failures-only)
export BASHUNIT_FAILURES_ONLY=true
BASHUNIT_FAILURES_ONLY=true
export -n BASHUNIT_FAILURES_ONLY
;;
--fail-on-risky)
export BASHUNIT_FAIL_ON_RISKY=true
BASHUNIT_FAIL_ON_RISKY=true
export -n BASHUNIT_FAIL_ON_RISKY
;;
--profile)
export BASHUNIT_PROFILE=true
BASHUNIT_PROFILE=true
export -n BASHUNIT_PROFILE
;;
--show-output)
export BASHUNIT_SHOW_OUTPUT_ON_FAILURE=true
BASHUNIT_SHOW_OUTPUT_ON_FAILURE=true
export -n BASHUNIT_SHOW_OUTPUT_ON_FAILURE
;;
--no-output-on-failure)
export BASHUNIT_SHOW_OUTPUT_ON_FAILURE=false
BASHUNIT_SHOW_OUTPUT_ON_FAILURE=false
export -n BASHUNIT_SHOW_OUTPUT_ON_FAILURE
;;
--no-progress)
export BASHUNIT_NO_PROGRESS=true
BASHUNIT_NO_PROGRESS=true
export -n BASHUNIT_NO_PROGRESS
;;
--strict)
export BASHUNIT_STRICT_MODE=true
BASHUNIT_STRICT_MODE=true
export -n BASHUNIT_STRICT_MODE
;;
-R | --run-all)
export BASHUNIT_STOP_ON_ASSERTION_FAILURE=false
BASHUNIT_STOP_ON_ASSERTION_FAILURE=false
export -n BASHUNIT_STOP_ON_ASSERTION_FAILURE
;;
--skip-env-file)
export BASHUNIT_SKIP_ENV_FILE=true
BASHUNIT_SKIP_ENV_FILE=true
export -n BASHUNIT_SKIP_ENV_FILE
;;
-l | --login)
export BASHUNIT_LOGIN_SHELL=true
BASHUNIT_LOGIN_SHELL=true
export -n BASHUNIT_LOGIN_SHELL
;;
--no-color)
# shellcheck disable=SC2034
Expand Down Expand Up @@ -400,7 +443,8 @@ function bashunit::main::cmd_test() {
# Disable coverage for assert mode - it's meant for running single assertions,
# not tracking code coverage. This also prevents issues when parent bashunit
# runs with coverage and calls subprocess bashunit with -a flag.
export BASHUNIT_COVERAGE=false
BASHUNIT_COVERAGE=false
export -n BASHUNIT_COVERAGE
bashunit::main::exec_assert "$assert_fn" ${args+"${args[@]}"}
else
if [ "${BASHUNIT_WATCH_MODE:-false}" = true ]; then
Expand Down Expand Up @@ -431,7 +475,8 @@ function bashunit::main::cmd_bench() {
local -a args=()
local args_count=0

export BASHUNIT_BENCH_MODE=true
BASHUNIT_BENCH_MODE=true
export -n BASHUNIT_BENCH_MODE

# Parse bench-specific options
while [ $# -gt 0 ]; do
Expand All @@ -441,17 +486,20 @@ function bashunit::main::cmd_bench() {
shift
;;
-s | --simple)
export BASHUNIT_SIMPLE_OUTPUT=true
BASHUNIT_SIMPLE_OUTPUT=true
export -n BASHUNIT_SIMPLE_OUTPUT
;;
--detailed)
export BASHUNIT_SIMPLE_OUTPUT=false
BASHUNIT_SIMPLE_OUTPUT=false
export -n BASHUNIT_SIMPLE_OUTPUT
;;
-e | --env | --boot)
# Support: --env "bootstrap.sh arg1 arg2"
local boot_file="${2%% *}"
local boot_args="${2#* }"
if [ "$boot_args" != "$2" ]; then
export BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
export -n BASHUNIT_BOOTSTRAP_ARGS
fi
# Export all variables from the env file so they're available in subshells
# (e.g., process substitution used in load_test_files)
Expand All @@ -462,13 +510,16 @@ function bashunit::main::cmd_bench() {
shift
;;
-vvv | --verbose)
export BASHUNIT_VERBOSE=true
BASHUNIT_VERBOSE=true
export -n BASHUNIT_VERBOSE
;;
--skip-env-file)
export BASHUNIT_SKIP_ENV_FILE=true
BASHUNIT_SKIP_ENV_FILE=true
export -n BASHUNIT_SKIP_ENV_FILE
;;
-l | --login)
export BASHUNIT_LOGIN_SHELL=true
BASHUNIT_LOGIN_SHELL=true
export -n BASHUNIT_LOGIN_SHELL
;;
--no-color)
# shellcheck disable=SC2034
Expand Down Expand Up @@ -819,7 +870,8 @@ function bashunit::main::exec_tests() {
# for replay and inherited by parallel test-file subshells.
if bashunit::env::is_random_order_enabled; then
if [ -z "${BASHUNIT_SEED:-}" ]; then
export BASHUNIT_SEED=$RANDOM
BASHUNIT_SEED=$RANDOM
export -n BASHUNIT_SEED
fi
if ! bashunit::env::is_tap_output_enabled; then
bashunit::console_header::print_random_order_seed "$BASHUNIT_SEED"
Expand Down
20 changes: 13 additions & 7 deletions tests/acceptance/bashunit_flag_env_leak_test.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail

# Regression guard for #834. Run-mode flags (--stop-on-failure and the report
# writers) used to be exported, so nested bashunit runs — bashunit's own
# acceptance tests under `build.sh --verify`, or a user's scripts under test
# that call bashunit — inherited them: nested runs aborted before persisting
# the rerun cache, wrote their own reports over the parent's files, and blew
# the per-run fork budget.
# Regression guard for #834/#837. Run-mode flags used to be exported, so nested
# bashunit runs — bashunit's own acceptance tests under `build.sh --verify`, or
# a user's scripts under test that call bashunit — inherited them: nested runs
# aborted before persisting the rerun cache, wrote their own reports over the
# parent's files, silently switched output/strict/parallel modes, and blew the
# per-run fork budget. Every flag the parent sets here must be paired with a
# name in leak_probe.sh's grep list (and vice versa).
function test_run_mode_flags_do_not_leak_into_nested_runs() {
local dir
dir=$(bashunit::temp_dir)

local output
local exit_code=0
output=$(./bashunit --no-parallel --skip-env-file \
output=$(./bashunit --no-parallel --simple --strict --skip-env-file \
--stop-on-failure \
--retry 1 \
--test-timeout 60 \
--random-order --seed 7 \
--no-progress \
--fail-on-risky \
--log-junit "$dir/log-junit.xml" \
--report-html "$dir/report.html" \
--report-tap "$dir/report.tap" \
Expand Down
9 changes: 8 additions & 1 deletion tests/acceptance/fixtures/flag_env_leak/leak_probe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

# Probe used by bashunit_flag_env_leak_test.sh: fails if any run-mode flag of
# the parent bashunit process leaked into this (nested) run's environment.
# Every name listed here must be paired with the matching flag in the parent
# invocation: the flag branch's `export -n` is also what clears an export
# attribute stamped by an allexport .env load further up the process tree.
function test_run_mode_flags_are_not_in_the_environment() {
local flags='STOP_ON_FAILURE|LOG_JUNIT|REPORT_HTML|REPORT_TAP|REPORT_JSON'
flags="$flags|PARALLEL_RUN|SIMPLE_OUTPUT|STRICT_MODE|RETRY|TEST_TIMEOUT"
flags="$flags|RANDOM_ORDER|SEED|NO_PROGRESS|FAIL_ON_RISKY|SKIP_ENV_FILE"

local leaked
leaked="$(env | grep -E '^BASHUNIT_(STOP_ON_FAILURE|LOG_JUNIT|REPORT_HTML|REPORT_TAP|REPORT_JSON)=' || true)"
leaked="$(env | grep -E "^BASHUNIT_($flags)=" || true)"

assert_empty "$leaked"
}
Loading