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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Changed
- A `--tag` matching nothing now names the tags the run saw, or says no test carries one — tags live only in `# @tag` comments and nothing lists them, so a typo had no way back (#1265)

- Performance: per-test cleanup no longer reads the whole of `BASHUNIT_TEMP_DIR`. That directory is shared and survives between runs, so leftovers from an interrupted run taxed every test of every later run — a 100-test file took 978ms against 5000 leftovers and 542ms after, and runtime no longer grows with the directory. A file a test writes there by hand, rather than via `temp_file`/`temp_dir`, is no longer removed for it (#1269)

### Fixed
- A test that both fails an assertion and hits a shell error no longer reports the failure text with the diagnostic glued onto the end; the error message is the diagnostic alone (#1267)

Expand Down
3 changes: 2 additions & 1 deletion docs/ai-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ actually make against this API:
the prefix: `bashunit::temp_dir`, `bashunit::temp_file`, `bashunit::mock`,
`bashunit::spy`. The unprefixed form is `command not found`, not an alias.
- Use `$(bashunit::temp_file)` / `$(bashunit::temp_dir)` for scratch files — they are
cleaned up automatically and are safe under `--parallel`.
cleaned up automatically and are safe under `--parallel`. Only paths these hand out
are cleaned; a path you build yourself is yours to remove.
- Never call the network in a test. Use `bashunit::mock` / `bashunit::spy` instead.
- Doubles are torn down after every test — do not add `bashunit::unmock` to `tear_down`.
Use it inside a test to get the real command back, or to suspend a
Expand Down
29 changes: 29 additions & 0 deletions src/api/globals.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ function bashunit::is_command_available() {
command -v "$1" >/dev/null 2>&1
}

##
# Records that the owning test created something under BASHUNIT_TEMP_DIR, so
# the EXIT trap can decide whether to clean up without reading the directory.
# A redirect, not a fork. Nothing to mark when neither id is set: the file is
# then not owned by a test and the trap never looks for it.
# Arguments: $1 - "<id>_" prefix, possibly empty
##
function bashunit::_mark_temp_owner() {
local test_prefix=$1
if [ -n "$test_prefix" ]; then
: >"$BASHUNIT_TEMP_DIR/${test_prefix}.mark" 2>/dev/null || true
fi
}

function bashunit::temp_file() {
local prefix=${1:-bashunit}
local test_prefix=""
Expand All @@ -33,6 +47,7 @@ function bashunit::temp_file() {
# We're at script level (e.g., in set_up_before_script) - use script ID
test_prefix="${BASHUNIT_CURRENT_SCRIPT_ID}_"
fi
bashunit::_mark_temp_owner "$test_prefix"
"$MKTEMP" "$BASHUNIT_TEMP_DIR/${test_prefix}${prefix}.XXXXXXX"
}

Expand All @@ -46,12 +61,26 @@ function bashunit::temp_dir() {
# We're at script level (e.g., in set_up_before_script) - use script ID
test_prefix="${BASHUNIT_CURRENT_SCRIPT_ID}_"
fi
bashunit::_mark_temp_owner "$test_prefix"
"$MKTEMP" -d "$BASHUNIT_TEMP_DIR/${test_prefix}${prefix}.XXXXXXX"
}

function bashunit::cleanup_testcase_temp_files() {
bashunit::internal_log "cleanup_testcase_temp_files"
if [ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]; then
# Stat one known path before globbing. Expanding the glob makes bash read
# the whole of BASHUNIT_TEMP_DIR, which is shared and persists between runs
# -- every file an interrupted run left behind is then re-examined by every
# test of every later run. It can never match one: the id carries this
# run's $$, so the scan is pure overhead. Measured on a 100-test file, a
# directory holding 5000 leftovers took the run from 498ms to 978ms (#1269).
#
# The marker is written by temp_file/temp_dir, so its absence means this
# test created nothing and there is nothing to remove. It is named with the
# same "<id>_" prefix, so the rm below takes it along with the rest.
if [ ! -e "$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_TEST_ID}_.mark" ]; then
return 0
fi
# Probe the glob in pure bash first: most tests create no temp file, so
# skipping the rm avoids a fork per test (#764). A non-matching glob either
# stays literal (nullglob off) or yields an empty array (nullglob on);
Expand Down
31 changes: 31 additions & 0 deletions tests/acceptance/bashunit_script_temp_file_cleanup_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,34 @@ function execution_modes() {
echo "sequential"
echo "parallel"
}

# Cleanup stats one marker instead of expanding a glob over BASHUNIT_TEMP_DIR.
# That directory is shared and survives between runs, so anything an
# interrupted run left there used to be re-examined by every test of every
# later run -- and could never match, since the id carries the run's own $$.
#
# Deterministic guard rather than a timing one: a file the test wrote itself,
# named with its own id, is reachable *only* by scanning the directory. It
# surviving proves no scan happened. A test that used the helper still has its
# file removed, which is the behaviour that must not regress.
function test_cleanup_does_not_read_the_shared_temp_directory() {
local fixture="tests/acceptance/fixtures/temp_marker_cleanup.sh"
local spy_planted spy_handed
spy_planted="$(bashunit::temp_file)"
spy_handed="$(bashunit::temp_file)"

local output
output=$(MARKER_SPY_PLANTED="$spy_planted" MARKER_SPY_HANDED="$spy_handed" \
./bashunit --no-parallel "$fixture" 2>&1)

assert_contains "2 passed" "$output"

local planted handed
planted="$(cat "$spy_planted")"
handed="$(cat "$spy_handed")"

assert_file_exists "$planted"
assert_file_not_exists "$handed"

rm -f "$planted"
}
16 changes: 16 additions & 0 deletions tests/acceptance/fixtures/temp_marker_cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash

# Writes a file the framework never handed out, named with this test's own id
# so it matches the cleanup glob. Only a directory scan could find it.
function test_plants_a_file_it_created_itself() {
printf 'planted\n' >"$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_TEST_ID}_planted"
printf '%s\n' "$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_TEST_ID}_planted" >"$MARKER_SPY_PLANTED"
assert_same 1 1
}

function test_uses_the_temp_file_helper() {
local f
f=$(bashunit::temp_file)
printf '%s\n' "$f" >"$MARKER_SPY_HANDED"
assert_file_exists "$f"
}
Loading