diff --git a/CHANGELOG.md b/CHANGELOG.md index fc2fa7a4..11958d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## Unreleased ### Fixed +- `--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) +- `./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) - Runs no longer leak a scratch directory under `$TMPDIR/bashunit/run/` — it is removed on exit, including `--version`/`--help`, subcommands and Ctrl-C (#811) - `bashunit::helper::get_function_line_number` no longer disables `extdebug` for its caller (#808) @@ -16,6 +19,7 @@ - `--jobs auto` / `-j auto` caps parallel concurrency at the CPU core count (portable across Linux/macOS/BSD); the default stays unlimited (#766) ### Changed +- `build.sh` hardened: runs under `set -euo pipefail`, derives the embed list from the entrypoint's `source` order (single source of truth), guards against duplicate embeds and missing doc markers, drops `eval`, and gates every build behind `bash -n` (#834) - `bashunit doc` no longer forks an `echo | sed` pipe per line of the assertion docs: a single awk pass prints the same bytes in ~50ms instead of ~5s (#832) - Multi-file runs are no longer quadratic in file count: each file's test functions are unset once the file has been processed, so test subshells stop forking an ever-growing shell. bashunit's own 63-file unit suite: ~64s -> ~22s sequential, ~26s -> ~7s parallel (#829) - Major performance work with no behaviour change: assertions, per-test execution, per-file discovery, cold start and parallel result publishing are now (near) fork-free, snapshots and `--tag` scans are cached, and quadratic failure rendering is single-pass. Benchmarks on bash 3.2: 100x10 `assert_equals` ~1.50s -> ~0.76s, 500 snapshot assertions ~7.5s -> ~3.0s, 100 tagged tests ~2.92s -> ~0.68s, and bashunit's own acceptance suite ~61s -> ~17s (#761-#764, #772-#775, #798, #801-#807, #809, #810, #813, #817) diff --git a/bashunit b/bashunit index 0b00c177..08763508 100755 --- a/bashunit +++ b/bashunit @@ -78,6 +78,7 @@ source "$BASHUNIT_ROOT_DIR/src/doc.sh" source "$BASHUNIT_ROOT_DIR/src/reports.sh" source "$BASHUNIT_ROOT_DIR/src/rerun.sh" source "$BASHUNIT_ROOT_DIR/src/runner.sh" +source "$BASHUNIT_ROOT_DIR/src/benchmark.sh" source "$BASHUNIT_ROOT_DIR/src/bashunit.sh" source "$BASHUNIT_ROOT_DIR/src/init.sh" source "$BASHUNIT_ROOT_DIR/src/learn.sh" diff --git a/build.sh b/build.sh index 296f7eb5..0b317bb2 100755 --- a/build.sh +++ b/build.sh @@ -1,10 +1,17 @@ #!/usr/bin/env bash +set -euo pipefail source src/check_os.sh +bashunit::check_os::init BASHUNIT_ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export BASHUNIT_ROOT_DIR +# Files already embedded by build::process_file. The source graph is a tree +# today, but one added cross-source would otherwise silently bundle a file +# twice (duplicate function definitions and double top-level execution). +_BUILD_EMBEDDED_FILES="" + function build() { local out=$1 @@ -21,38 +28,41 @@ function build::verify() { echo "Verifying build ⏱️" - BASHUNIT_BUILD_DIR="$out_dir" "$out" tests \ + if ! BASHUNIT_BUILD_DIR="$out_dir" "$out" tests \ --simple \ --parallel \ --log-junit "$out_dir/log-junit.xml" \ --report-html "$out_dir/report.html" \ - --stop-on-failure - - # shellcheck disable=SC2181 - if [[ $? -eq 0 ]]; then - echo "✅ Build verified ✅" + --stop-on-failure; then + echo "❌ Build verification failed" >&2 + exit 1 fi + + echo "✅ Build verified ✅" } function build::generate_bin() { local out=$1 local temp - temp="$(dirname "$out")/temp.sh" + temp="$(dirname "$out")/temp.$$.sh" echo '#!/usr/bin/env bash' >"$temp" echo "Generating bashunit in the '$(dirname "$out")' folder..." + local file for file in $(build::dependencies); do build::process_file "$file" "$temp" done cat bashunit >>"$temp" - grep -v '^source' "$temp" >"$out" + grep -v '^source ' "$temp" >"$out" rm "$temp" chmod u+x "$out" # Embed the assertions.md docs into the binary build::embed_docs "$out" + + build::assert_valid_syntax "$out" } # Recursive function to process each file and any files it sources @@ -60,20 +70,26 @@ function build::process_file() { local file=$1 local temp=$2 + case " $_BUILD_EMBEDDED_FILES " in + *" $file "*) return ;; + esac + _BUILD_EMBEDDED_FILES="$_BUILD_EMBEDDED_FILES $file" + { echo "# $(basename "$file")" - tail -n +2 "$file" >>"$temp" + tail -n +2 "$file" echo "" } >>"$temp" - # Search for any 'source' lines in the current file - grep '^source ' "$file" | while read -r line; do - # Extract the path from the 'source' command + # Recurse into any 'source' lines of the current file. Process substitution + # (not a pipe) keeps the loop in this shell so _BUILD_EMBEDDED_FILES persists. + local line + while read -r line; do local sourced_file sourced_file=$(echo "$line" | awk '{print $2}' | sed 's/^"//;s/"$//') # Remove any quotes - # Handle cases where the path uses $BASHUNIT_ROOT_DIR or other variables - sourced_file=$(eval echo "$sourced_file") + # Expand the literal $BASHUNIT_ROOT_DIR prefix without eval + sourced_file="${sourced_file/\$BASHUNIT_ROOT_DIR/$BASHUNIT_ROOT_DIR}" # Handle relative paths if necessary local _absolute_path_pattern='^/' @@ -85,42 +101,16 @@ function build::process_file() { if [[ -f "$sourced_file" ]]; then build::process_file "$sourced_file" "$temp" fi - done + done < <(grep '^source ' "$file" || true) } +# The embed list is derived from the entrypoint's own source order: a single +# source of truth, so a src file added to the entrypoint can never be missing +# from the distributable (regressions: bench #0.31.0, watch #735). function build::dependencies() { - deps=( - "src/check_os.sh" - "src/str.sh" - "src/globals.sh" - "src/dependencies.sh" - "src/io.sh" - "src/math.sh" - "src/parallel.sh" - "src/env.sh" - "src/coverage.sh" - "src/clock.sh" - "src/state.sh" - "src/colors.sh" - "src/console_header.sh" - "src/console_results.sh" - "src/helpers.sh" - "src/test_title.sh" - "src/upgrade.sh" - "src/watch.sh" - "src/assertions.sh" - "src/reports.sh" - "src/rerun.sh" - "src/runner.sh" - "src/benchmark.sh" - "src/init.sh" - "src/learn.sh" - "src/doc.sh" - "src/bashunit.sh" - "src/main.sh" - ) - - echo "${deps[@]}" + grep '^source ' bashunit \ + | sed -e 's|^source "\$BASHUNIT_ROOT_DIR/||' -e 's|"$||' \ + | grep -v '^src/dev/' } function build::embed_docs() { @@ -128,12 +118,21 @@ function build::embed_docs() { local docs_file="docs/assertions.md" local temp_file="${file}.tmp" + local start_line + start_line=$(grep -n "# __BASHUNIT_EMBEDDED_DOCS_START__" "$file" | cut -d: -f1 | head -n 1) || true + if [[ -z "$start_line" ]]; then + echo "❌ Embed marker __BASHUNIT_EMBEDDED_DOCS_START__ not found in $file" >&2 + exit 1 + fi + if ! grep -q "# __BASHUNIT_EMBEDDED_DOCS_END__" "$file"; then + echo "❌ Embed marker __BASHUNIT_EMBEDDED_DOCS_END__ not found in $file" >&2 + exit 1 + fi + # Build the replacement content { # Print everything before the start marker (excluding the marker line) - local line_num - line_num=$(grep -n "# __BASHUNIT_EMBEDDED_DOCS_START__" "$file" | cut -d: -f1) - head -n "$((line_num - 1))" "$file" + head -n "$((start_line - 1))" "$file" # Print the heredoc with embedded docs echo " cat <<'__BASHUNIT_DOCS_EOF__'" @@ -148,10 +147,19 @@ function build::embed_docs() { chmod u+x "$file" } +function build::assert_valid_syntax() { + local file=$1 + + if ! bash -n "$file"; then + echo "❌ Generated artifact failed bash -n syntax check: $file" >&2 + exit 1 + fi +} + function build::generate_checksum() { local out=$1 - if [[ "$_OS" == "Windows" ]]; then + if [[ "$_BASHUNIT_OS" == "Windows" ]]; then return fi @@ -170,34 +178,37 @@ function build::generate_checksum() { ######### MAIN ######### ######################## -DIR="bin" -SHOULD_VERIFY_BUILD=false -SHOULD_CLEANUP=false - -for arg in "$@"; do - case $arg in - -v | --verify) - SHOULD_VERIFY_BUILD=true - ;; - -c | --cleanup) - SHOULD_CLEANUP=true - ;; - *) - DIR=$arg - ;; - esac -done +# Skip when sourced (tests source this file to exercise the functions above) +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + DIR="bin" + SHOULD_VERIFY_BUILD=false + SHOULD_CLEANUP=false + + for arg in "$@"; do + case $arg in + -v | --verify) + SHOULD_VERIFY_BUILD=true + ;; + -c | --cleanup) + SHOULD_CLEANUP=true + ;; + *) + DIR=$arg + ;; + esac + done -mkdir -p "$DIR" -OUT="$DIR/bashunit" + mkdir -p "$DIR" + OUT="$DIR/bashunit" -build "$OUT" + build "$OUT" -if [[ $SHOULD_VERIFY_BUILD == true ]]; then - build::verify "$OUT" -fi + if [[ $SHOULD_VERIFY_BUILD == true ]]; then + build::verify "$OUT" + fi -if [[ $SHOULD_CLEANUP == true ]]; then - echo "🧹 Cleaning up build directory: $DIR" - rm -rf "$DIR" + if [[ $SHOULD_CLEANUP == true ]]; then + echo "🧹 Cleaning up build directory: $DIR" + rm -rf "$DIR" + fi fi diff --git a/src/main.sh b/src/main.sh index 0b031c14..35de23cb 100644 --- a/src/main.sh +++ b/src/main.sh @@ -92,7 +92,13 @@ function bashunit::main::cmd_test() { set -x ;; -S | --stop-on-failure) - export BASHUNIT_STOP_ON_FAILURE=true + # This-process-only: parallel stop uses a flag file and sync stop uses + # exit codes, so no child process needs it — exported (including via an + # allexport .env load, hence export -n) it leaked into nested bashunit + # runs, aborting them before rerun::persist could write + # .bashunit/last-failed (broke verify's acceptance tests, #834). + BASHUNIT_STOP_ON_FAILURE=true + export -n BASHUNIT_STOP_ON_FAILURE ;; -p | --parallel) export BASHUNIT_PARALLEL_RUN=true @@ -151,8 +157,15 @@ function bashunit::main::cmd_test() { set +o allexport shift ;; + # Report flags are this-process-only: reports are generated by the main + # shell after aggregation, no child process reads these. `export -n` also + # strips an export attribute inherited from an allexport .env load — + # otherwise nested bashunit runs (bashunit's own acceptance tests, or a + # user's scripts under test that call bashunit) silently write their own + # reports over the parent's files and blow the per-run fork budget (#834). --log-junit | --report-junit) - export BASHUNIT_LOG_JUNIT="$2" + BASHUNIT_LOG_JUNIT="$2" + export -n BASHUNIT_LOG_JUNIT shift ;; --log-gha) @@ -160,15 +173,18 @@ function bashunit::main::cmd_test() { shift ;; -r | --report-html) - export BASHUNIT_REPORT_HTML="$2" + BASHUNIT_REPORT_HTML="$2" + export -n BASHUNIT_REPORT_HTML shift ;; --report-tap) - export BASHUNIT_REPORT_TAP="$2" + BASHUNIT_REPORT_TAP="$2" + export -n BASHUNIT_REPORT_TAP shift ;; --report-json) - export BASHUNIT_REPORT_JSON="$2" + BASHUNIT_REPORT_JSON="$2" + export -n BASHUNIT_REPORT_JSON shift ;; --no-output) diff --git a/tests/acceptance/bashunit_bench_test.sh b/tests/acceptance/bashunit_bench_test.sh new file mode 100644 index 00000000..89e95094 --- /dev/null +++ b/tests/acceptance/bashunit_bench_test.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Regression guard for #834. The dev entrypoint never sourced src/benchmark.sh +# (only the hand-maintained build list bundled it), so `./bashunit bench` worked +# in the built binary but crashed with `command not found` in dev mode — and no +# *_test.sh exercised the bench CLI path. +function test_bench_command_runs_in_dev_mode() { + local fixture_dir + fixture_dir=$(bashunit::temp_dir) + printf '#!/usr/bin/env bash\nfunction bench_sample() { :; }\n' >"$fixture_dir/sample_bench.sh" + + local output + local exit_code=0 + output=$(./bashunit bench "$fixture_dir/sample_bench.sh" 2>&1) || exit_code=$? + + assert_equals 0 "$exit_code" + assert_not_contains "command not found" "$output" + assert_contains "bench_sample" "$output" +} diff --git a/tests/acceptance/bashunit_flag_env_leak_test.sh b/tests/acceptance/bashunit_flag_env_leak_test.sh new file mode 100644 index 00000000..480984a2 --- /dev/null +++ b/tests/acceptance/bashunit_flag_env_leak_test.sh @@ -0,0 +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. +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 \ + --stop-on-failure \ + --log-junit "$dir/log-junit.xml" \ + --report-html "$dir/report.html" \ + --report-tap "$dir/report.tap" \ + --report-json "$dir/report.json" \ + tests/acceptance/fixtures/flag_env_leak/leak_probe.sh 2>&1) || exit_code=$? + + assert_equals 0 "$exit_code" + assert_contains "1 passed" "$output" +} diff --git a/tests/acceptance/fixtures/flag_env_leak/leak_probe.sh b/tests/acceptance/fixtures/flag_env_leak/leak_probe.sh new file mode 100644 index 00000000..baaadbb3 --- /dev/null +++ b/tests/acceptance/fixtures/flag_env_leak/leak_probe.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# 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. +function test_run_mode_flags_are_not_in_the_environment() { + local leaked + leaked="$(env | grep -E '^BASHUNIT_(STOP_ON_FAILURE|LOG_JUNIT|REPORT_HTML|REPORT_TAP|REPORT_JSON)=' || true)" + + assert_empty "$leaked" +} diff --git a/tests/unit/benchmark_test.sh b/tests/unit/benchmark_test.sh index 19ab528e..30758da0 100755 --- a/tests/unit/benchmark_test.sh +++ b/tests/unit/benchmark_test.sh @@ -1,10 +1,6 @@ #!/usr/bin/env bash # shellcheck disable=SC2329 # Mock functions are invoked indirectly by tested code -function set_up_before_script() { - source "$BASHUNIT_ROOT_DIR/src/benchmark.sh" -} - function set_up() { SCRIPT="tests/benchmark/fixtures/bashunit_sleep_bench.sh" } diff --git a/tests/unit/build_test.sh b/tests/unit/build_test.sh index 8c52416e..cabff116 100644 --- a/tests/unit/build_test.sh +++ b/tests/unit/build_test.sh @@ -11,8 +11,8 @@ function src_files_sourced_by_entrypoint() { grep -oE 'src/[a-zA-Z0-9_/]+\.sh' "$ROOT_DIR/bashunit" | grep -v '^src/dev/' | sort -u } -function src_files_in_build() { - grep -oE 'src/[a-zA-Z0-9_/]+\.sh' "$ROOT_DIR/build.sh" | sort -u +function build_dependencies() { + (cd "$ROOT_DIR" && bash -c 'source ./build.sh && build::dependencies') } # Every src file the dev entrypoint sources (except dev-only helpers) must also be @@ -20,11 +20,101 @@ function src_files_in_build() { # single-file binary (regressions: bench #0.31.0, watch #735). function test_build_bundles_every_src_file_sourced_by_entrypoint() { local missing - missing=$(comm -23 <(src_files_sourced_by_entrypoint) <(src_files_in_build)) + missing=$(comm -23 <(src_files_sourced_by_entrypoint) <(build_dependencies | sort -u)) assert_empty "$missing" } +# The reverse direction: the build must not bundle files the entrypoint does not +# source (a stale hand-maintained list shipped benchmark.sh while dev mode lacked +# it — the two must stay a single source of truth). +function test_build_bundles_only_files_sourced_by_entrypoint() { + local extra + extra=$(comm -13 <(src_files_sourced_by_entrypoint) <(build_dependencies | sort -u)) + + assert_empty "$extra" +} + +function test_build_dependencies_include_benchmark() { + assert_contains "src/benchmark.sh" "$(build_dependencies)" +} + +function test_build_script_is_sourceable_without_running_a_build() { + local output + output=$(cd "$ROOT_DIR" && bash -c 'source ./build.sh && declare -F build::verify' 2>&1) + + assert_not_contains "Generating" "$output" + assert_contains "build::verify" "$output" +} + +function test_build_verify_exits_nonzero_when_suite_fails() { + local fake_dir + fake_dir=$(bashunit::temp_dir) + printf '#!/usr/bin/env bash\nexit 1\n' >"$fake_dir/bashunit" + chmod +x "$fake_dir/bashunit" + + local exit_code=0 + (cd "$ROOT_DIR" && bash -c 'source ./build.sh && build::verify "$1"' _ "$fake_dir/bashunit") \ + >/dev/null 2>&1 || exit_code=$? + + assert_not_equals 0 "$exit_code" +} + +function test_build_verify_succeeds_when_suite_passes() { + local fake_dir + fake_dir=$(bashunit::temp_dir) + printf '#!/usr/bin/env bash\nexit 0\n' >"$fake_dir/bashunit" + chmod +x "$fake_dir/bashunit" + + local output + local exit_code=0 + output=$(cd "$ROOT_DIR" && bash -c 'source ./build.sh && build::verify "$1"' _ "$fake_dir/bashunit" 2>&1) \ + || exit_code=$? + + assert_equals 0 "$exit_code" + assert_contains "verified" "$output" +} + +function test_build_embed_docs_fails_on_missing_markers() { + local file + file=$(bashunit::temp_file) + printf '#!/usr/bin/env bash\necho hi\n' >"$file" + + local exit_code=0 + (cd "$ROOT_DIR" && bash -c 'source ./build.sh && build::embed_docs "$1"' _ "$file") \ + >/dev/null 2>&1 || exit_code=$? + + assert_not_equals 0 "$exit_code" + # The artifact must not be replaced with a truncated file on failure. + assert_contains "echo hi" "$(cat "$file")" +} + +function test_build_process_file_embeds_a_file_only_once() { + local dir + dir=$(bashunit::temp_dir) + printf '#!/usr/bin/env bash\nsource ./a.sh\nsource ./b.sh\n' >"$dir/root.sh" + printf '#!/usr/bin/env bash\nsource ./common.sh\n' >"$dir/a.sh" + printf '#!/usr/bin/env bash\nsource ./common.sh\n' >"$dir/b.sh" + printf '#!/usr/bin/env bash\nfunction common_fn() { :; }\n' >"$dir/common.sh" + + (cd "$ROOT_DIR" && bash -c 'source ./build.sh && build::process_file "$1" "$2"' _ "$dir/root.sh" "$dir/out.tmp") \ + >/dev/null 2>&1 + + assert_equals "1" "$(grep -c '^# common.sh$' "$dir/out.tmp")" +} + +function test_build_assert_valid_syntax_rejects_broken_file() { + local file + file=$(bashunit::temp_file) + printf '#!/usr/bin/env bash\nif then fi (\n' >"$file" + + local exit_code=0 + (cd "$ROOT_DIR" && bash -c 'source ./build.sh && build::assert_valid_syntax "$1"' _ "$file") \ + >/dev/null 2>&1 || exit_code=$? + + assert_not_equals 0 "$exit_code" +} + function test_built_binary_defines_watch_run() { local build_dir build_dir=$(bashunit::temp_dir) @@ -34,3 +124,15 @@ function test_built_binary_defines_watch_run() { assert_file_exists "$build_dir/bashunit" assert_equals "1" "$(grep -c 'function bashunit::watch::run()' "$build_dir/bashunit")" } + +function test_built_binary_embeds_each_src_file_exactly_once() { + local build_dir + build_dir=$(bashunit::temp_dir) + + (cd "$ROOT_DIR" && bash build.sh "$build_dir") >/dev/null 2>&1 + + local duplicated + duplicated=$(grep -E '^# [a-z_]+\.sh$' "$build_dir/bashunit" | sort | uniq -d) + + assert_empty "$duplicated" +} diff --git a/tests/unit/check_os_test.sh b/tests/unit/check_os_test.sh index ab488eed..8cd9c5c3 100644 --- a/tests/unit/check_os_test.sh +++ b/tests/unit/check_os_test.sh @@ -81,7 +81,11 @@ function test_not_alpine_is_not_busybox() { function test_module_load_detects_os_with_a_single_uname_call() { bashunit::spy uname - source "$BASHUNIT_ROOT_DIR/src/check_os.sh" + # Resolve src relative to this test file, not the running binary: + # under `build.sh --verify` the built bashunit lives in a folder + # without src/, and sourcing through $BASHUNIT_ROOT_DIR crashed + # the whole verification run (silently, until #834 gated it). + source "$(dirname "${BASH_SOURCE[0]}")/../../src/check_os.sh" assert_have_been_called_times 1 uname } diff --git a/tests/unit/coverage_percent_test.sh b/tests/unit/coverage_percent_test.sh index 148299b1..6b0f8839 100644 --- a/tests/unit/coverage_percent_test.sh +++ b/tests/unit/coverage_percent_test.sh @@ -3,7 +3,9 @@ # Tests for tools/coverage_percent.sh: extract a single rounded coverage # percentage from an LCOV report by summing the per-section LH/LF records. -COVERAGE_PERCENT_SCRIPT="${BASHUNIT_ROOT_DIR:-.}/tools/coverage_percent.sh" +# cwd-relative (repo root), not $BASHUNIT_ROOT_DIR: under `build.sh --verify` +# the running binary's root dir has no tools/ (#834). +COVERAGE_PERCENT_SCRIPT="tools/coverage_percent.sh" function test_coverage_percent_sums_lh_over_lf() { local lcov diff --git a/tests/unit/globals_test.sh b/tests/unit/globals_test.sh index bbcbcbbc..966c9e4d 100644 --- a/tests/unit/globals_test.sh +++ b/tests/unit/globals_test.sh @@ -30,8 +30,15 @@ function test_globals_current_filename() { assert_same "globals_test.sh" "$(bashunit::current_filename)" } +# Indirection keeps the observed caller frame inside this file: called directly +# from a test, caller_filename reports the framework's own source layout, which +# differs between a repo checkout (./src) and the built single-file binary (#834). +function globals_test::call_caller_filename() { + bashunit::caller_filename +} + function test_globals_caller_filename() { - assert_same "./src" "$(bashunit::caller_filename)" + assert_same "tests/unit" "$(globals_test::call_caller_filename)" } function test_globals_caller_line() { diff --git a/tests/unit/helpers_test.sh b/tests/unit/helpers_test.sh index 01da1734..ea3cd8fb 100644 --- a/tests/unit/helpers_test.sh +++ b/tests/unit/helpers_test.sh @@ -435,9 +435,11 @@ function test_normalize_test_function_name_with_interpolation() { } function helpers_test::find_total_in_subshell() { - # "bashunit::helper::find_total_tests" needs the "data_set" function, so we have to source globals.sh first + # "bashunit::helper::find_total_tests" needs the "data_set" function, so we have to source globals.sh first. + # Paths are cwd-relative (repo root), not $BASHUNIT_ROOT_DIR: under `build.sh --verify` + # the running binary's root dir has no src/ (#834). bash -c 'source src/globals.sh; source "$1"; shift; bashunit::helper::find_total_tests "$@"' \ - bash "$BASHUNIT_ROOT_DIR/src/helpers.sh" "$@" + bash "src/helpers.sh" "$@" } function test_find_total_tests_no_files() { diff --git a/tests/unit/learn_test.sh b/tests/unit/learn_test.sh index 80d470bb..ac14b5ac 100644 --- a/tests/unit/learn_test.sh +++ b/tests/unit/learn_test.sh @@ -11,8 +11,9 @@ set -euo pipefail function _learn_in_sandbox() { local sandbox root_dir sandbox="$(bashunit::temp_dir)" - # BASHUNIT_ROOT_DIR may be relative (e.g. "."); resolve it before cd-ing away. - root_dir="$(cd "$BASHUNIT_ROOT_DIR" && pwd)" + # Resolve the repo root from this test file, not $BASHUNIT_ROOT_DIR: under + # `build.sh --verify` the running binary's root dir has no src/ (#834). + root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" ( cd "$sandbox" && HOME="$sandbox" bash -c '