diff --git a/CHANGELOG.md b/CHANGELOG.md index cb1aae1d..42fc3524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x — a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117) ### Fixed +- A report path that is a directory (`--report-junit build/` with the filename forgotten) fails fast with `is a directory, not a file` instead of passing validation, failing inside the writer with a raw bash message naming a bashunit source file, and exiting 0 with no report written. Affects every report flag and the `bench` equivalents (#1177) - `bashunit init` no longer adds a dead `BASHUNIT_BOOTSTRAP` line to `.env` on every run: it commented out the existing setting and appended a fresh copy even when the value was unchanged, so a third run left three lines, two of them inert. It also now reports what it did to `.env`, the one file it wrote without saying so (#1175) - A coverage run that tracked no executable line at all no longer reports it as `Coverage 0% is below minimum N%`: that 0% is arithmetic rather than a measurement, and the message sent the reader to their tests when the cause is almost always a `--coverage-paths` that matched nothing. The gate still fails, now naming the real problem (#1171) - A run whose scratch directory goes missing now says so once, on stderr, naming the directory, instead of recovering silently — the recovery added for that case left no trace at all, which is worse for diagnosis than the misleading error it replaced (#1167) diff --git a/src/main/validate.sh b/src/main/validate.sh index 27997bc7..df3d0bcd 100644 --- a/src/main/validate.sh +++ b/src/main/validate.sh @@ -33,12 +33,32 @@ function bashunit::main::require_non_negative_int_or_exit() { # raw redirect error on a run that had already reported success (#875). # Arguments: $1 - path, $2 - the setting name to quote in the error ## +## +# Exits with the same shape as the writability errors, but says which mistake +# it was: "cannot be written" sends the reader to permissions, and a directory +# is almost always a forgotten filename. +# Arguments: $1 - path, $2 - setting name +## +function bashunit::main::report_path_is_a_directory() { + printf "%sError: %s is a directory, not a file: '%s'.%s\n" \ + "$_BASHUNIT_COLOR_FAILED" "${2:-path}" "$1" "$_BASHUNIT_COLOR_DEFAULT" >&2 + exit 1 +} + + function bashunit::main::require_writable_path_or_exit() { local path=$1 local parent=${1%/*} [ "$parent" = "$1" ] && parent="." [ -z "$parent" ] && parent="/" + # A directory satisfies both -e and -w, so it used to pass here and fail much + # later inside the writer -- with a raw bash message naming a bashunit source + # file, and an exit status of 0 (#1177). + if [ -d "$path" ]; then + bashunit::main::report_path_is_a_directory "$path" "${2:-}" + fi + if [ -e "$path" ]; then [ -w "$path" ] && return 0 elif [ -d "$parent" ] && [ -w "$parent" ]; then @@ -72,6 +92,10 @@ function bashunit::main::require_creatable_path_or_exit() { done [ -z "$ancestor" ] && ancestor="/" + if [ -d "$path" ]; then + bashunit::main::report_path_is_a_directory "$path" "${2:-}" + fi + if [ -d "$ancestor" ] && [ -w "$ancestor" ] && { [ ! -e "$path" ] || [ -w "$path" ]; }; then return 0 diff --git a/tests/acceptance/bashunit_bench_reports_test.sh b/tests/acceptance/bashunit_bench_reports_test.sh index 4fc96887..e2d43e53 100644 --- a/tests/acceptance/bashunit_bench_reports_test.sh +++ b/tests/acceptance/bashunit_bench_reports_test.sh @@ -123,6 +123,23 @@ function test_an_unwritable_report_path_fails_fast() { assert_contains "cannot be written" "$output" } +# A directory satisfies both `-e` and `-w`, so it passed the writability check +# and the run continued to the writer, which failed with a raw bash message +# naming a bashunit source file -- and the run still exited 0. A CI job +# publishing that report would find no file and nothing red to explain it +# (#1177). +function test_a_report_path_that_is_a_directory_fails_fast() { + local dir + dir="$(bashunit::temp_dir)" + + local ec=0 + local output + output=$(./bashunit bench --report-json "$dir" "$FIXTURE" 2>&1) || ec=$? + + assert_general_error "" "" "$ec" + assert_contains "is a directory" "$output" +} + function test_the_flags_are_advertised_by_bench_help() { local output output=$(./bashunit bench --help 2>&1)