Repository navigation
Description
Problem
bashunit::unmock on a spied or mocked shell function removes the function instead of restoring it. The docs promise the opposite: "drops the function override, restoring the real command".
Reproduced on main 776997e9, macOS arm64, Bash 3.2.57 and Bash 5.3:
function test_unmock_restores_a_spied_function() {
function greet() { echo real; }
bashunit::spy greet
greet
bashunit::unmock greet
assert_same "real" "$(greet 2>&1)"
}
function test_unmock_restores_a_mocked_function() {
function wave() { echo real; }
bashunit::mock wave echo mocked
bashunit::unmock wave
assert_same "real" "$(wave 2>&1)"
}Both fail with greet: command not found / wave: command not found, and the test is also reported as an Error because of the shell diagnostic.
Where
src/doubles/spy.sh:224 and src/doubles/mock.sh:97-101 redefine the function with eval "function $command() { ... }" without saving the previous definition. src/doubles/mock.sh:18 (bashunit::unmock) runs unset -f "$command", which is right for an external command (the binary on PATH becomes visible again) and wrong for a function the test file or the code under test defined.
Why it matters
Spying on a function from the code under test, then releasing the spy to call the real implementation later in the same test, is the documented reason to call bashunit::unmock. Today that leaves the test with a missing function.
Suggested change
Before redefining, save the existing definition when declare -F "$command" succeeds (declare -f "$command" output, in a _BASHUNIT_DOUBLE_${variable}_ORIGINAL slot or a temp file). In unmock, eval the saved definition back when one exists, else unset -f. The same restore belongs in the per-test unwind only if it is cheap; the per-test subshell already discards redefinitions.
Acceptance criteria
- After
bashunit::unmock, a function that existed beforebashunit::spyorbashunit::mockruns its original body. - A name that was an external command, or undefined, keeps today's behaviour: the function is removed.
- The docs line about restoring stays true, and a test covers both the function and the command case and fails on the current code.
Proposed fix
Save the previous function body when a double replaces a shell function, and restore it in bashunit::unmock. Add one helper in src/doubles/mock.sh used by bashunit::spy (src/doubles/spy.sh:224), bashunit::mock (mock.sh:97-101) and bashunit::mock_sequence (mock.sh:177) just before their eval "function $command() ...":
# Keeps the definition a double is about to replace, so unmock can put it back.
function bashunit::doubles::save_original() {
local command=$1 variable
bashunit::helper::normalize_variable_name_to_slot "$command"
variable=$_BASHUNIT_HELPER_VARNAME_OUT
local saved_var="_BASHUNIT_DOUBLE_${variable}_ORIGINAL"
[ -n "${!saved_var-}" ] && return 0
if declare -F -- "$command" >/dev/null 2>&1; then
export "$saved_var"="$(declare -f -- "$command")"
fi
}In bashunit::unmock (mock.sh:18), after unset -f "$command": if _BASHUNIT_DOUBLE_${variable}_ORIGINAL is set, eval "${!saved_var}" and unset "$saved_var". Public behaviour that must not change: unmocking an external command or an undefined name still just removes the function; the sandbox shim restore stays; a second spy or mock on the same name still replaces the first (the [ -n ] && return keeps the first original). Bash 3.0: declare -f name and ${!var-} exist on 3.0; no declare -n, no +=. Docs: docs/test-doubles.md under bashunit::unmock already promises the restore; add one sentence that this includes shell functions. CHANGELOG: one line under Unreleased, Fixed.
Agent brief
- Branch:
fix/1401-unmock-restores-functionfrom freshorigin/main. Commit:fix(doubles): restore a spied or mocked shell function on unmock, bodyCloses #1401. - Files:
src/doubles/mock.sh,src/doubles/spy.sh; tests intests/functional/doubles_lifecycle_test.sh, next to the existing unmock tests, copying their define-double-unmock-assert shape. - RED first:
function test_unmock_restores_a_spied_shell_function() {
function greet_1401() { echo real; }
bashunit::spy greet_1401
greet_1401
bashunit::unmock greet_1401
assert_same "real" "$(greet_1401 2>&1)"
}
function test_unmock_restores_a_mocked_shell_function() {
function wave_1401() { echo real; }
bashunit::mock wave_1401 echo mocked
bashunit::unmock wave_1401
assert_same "real" "$(wave_1401 2>&1)"
}Run only that file: ./bashunit tests/functional/doubles_lifecycle_test.sh.
- Non-goals: per-test cleanup in
runner/hooks.sh(the test subshell already discards redefinitions);bashunit::spystill overrides the behaviour as documented; A spy records a multi-line argument as two calls #1399 and Call assertions pass against a recorded call that does not exist #1400 are separate. - Gate, once, when the change is complete:
./bashunit tests/,./bashunit --parallel --simple --strict tests/,make sa,make lint.make lintis the formatting authority; nevershfmt -w; 2-space indent. - Done when: every acceptance box is checked, the gate is green, CHANGELOG and docs are updated as listed above, and a PR is open with the
prskill.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
fix(doubles): restore original functions on unmock