From cd300ceeba29bb275e88ac5a1c5c5a4af768ad09 Mon Sep 17 00:00:00 2001 From: SauronBot Date: Sun, 29 Mar 2026 21:03:59 +0200 Subject: [PATCH 1/2] fix(test-doubles): use builtin echo/printf in spy to prevent recursion when spying on echo or printf Spying on 'echo' or 'printf' caused bashunit to hang indefinitely because the generated spy function called back into itself through bashunit's internal use of these builtins. Changes: - Use 'builtin echo' and 'builtin printf' inside the eval'd spy function body in bashunit::spy to prevent recursive calls when spying on echo or printf - Use 'builtin echo' in bashunit::mock's no-arg form for the same reason - Use 'builtin echo' in bashunit::helper::normalize_variable_name so that spy assertions remain functional even when echo is spied upon - Add functional tests (with fixtures) verifying that spying on echo and printf completes without hanging Fixes #607 --- src/helpers.sh | 4 ++-- src/test_doubles.sh | 16 ++++++++-------- tests/functional/doubles_test.sh | 18 ++++++++++++++++++ tests/functional/fixtures/echo_function.sh | 5 +++++ tests/functional/fixtures/printf_function.sh | 5 +++++ tests/unit/test_doubles_test.sh | 2 ++ 6 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 tests/functional/fixtures/echo_function.sh create mode 100644 tests/functional/fixtures/printf_function.sh diff --git a/src/helpers.sh b/src/helpers.sh index d8ced397..c8d18311 100755 --- a/src/helpers.sh +++ b/src/helpers.sh @@ -273,11 +273,11 @@ function bashunit::helper::normalize_variable_name() { normalized_string="${input_string//[^a-zA-Z0-9_]/_}" local _re='^[a-zA-Z_]' - if [ "$(echo "$normalized_string" | "$GREP" -cE "$_re" || true)" -eq 0 ]; then + if [ "$(builtin echo "$normalized_string" | "$GREP" -cE "$_re" || true)" -eq 0 ]; then normalized_string="_$normalized_string" fi - echo "$normalized_string" + builtin echo "$normalized_string" } function bashunit::helper::get_provider_data() { diff --git a/src/test_doubles.sh b/src/test_doubles.sh index c0dbe94a..df4f60a3 100644 --- a/src/test_doubles.sh +++ b/src/test_doubles.sh @@ -34,7 +34,7 @@ function bashunit::mock() { if [ $# -gt 0 ]; then eval "function $command() { $* \"\$@\"; }" else - eval "function $command() { echo \"$($CAT)\" ; }" + eval "function $command() { builtin echo \"$($CAT)\" ; }" fi export -f "${command?}" @@ -61,14 +61,14 @@ function bashunit::spy() { local serialized=\"\" local arg for arg in \"\$@\"; do - serialized=\"\$serialized\$(printf '%q' \"\$arg\")$'\\x1f'\" + serialized=\"\$serialized\$(builtin printf '%q' \"\$arg\")$'\\x1f'\" done serialized=\${serialized%$'\\x1f'} - printf '%s\x1e%s\\n' \"\$raw\" \"\$serialized\" >> '$params_file' + builtin printf '%s\x1e%s\\n' \"\$raw\" \"\$serialized\" >> '$params_file' local _c - _c=\$(cat '$times_file' 2>/dev/null || echo 0) + _c=\$(cat '$times_file' 2>/dev/null || builtin echo 0) _c=\$((_c+1)) - echo \"\$_c\" > '$times_file' + builtin echo \"\$_c\" > '$times_file' }" export -f "${command?}" @@ -83,7 +83,7 @@ function assert_have_been_called() { local file_var="${variable}_times_file" local times=0 if [ -f "${!file_var-}" ]; then - times=$(cat "${!file_var}" 2>/dev/null || echo 0) + times=$(cat "${!file_var}" 2>/dev/null || builtin echo 0) fi local label="${2:-$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")}" @@ -141,7 +141,7 @@ function assert_have_been_called_times() { local file_var="${variable}_times_file" local times=0 if [ -f "${!file_var-}" ]; then - times=$(cat "${!file_var}" 2>/dev/null || echo 0) + times=$(cat "${!file_var}" 2>/dev/null || builtin echo 0) fi local label="${3:-$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [ "$times" -ne "$expected_count" ]; then @@ -170,7 +170,7 @@ function assert_have_been_called_nth_with() { local times=0 if [ -f "${!times_file_var-}" ]; then - times=$(cat "${!times_file_var}" 2>/dev/null || echo 0) + times=$(cat "${!times_file_var}" 2>/dev/null || builtin echo 0) fi if [ "$nth" -gt "$times" ]; then diff --git a/tests/functional/doubles_test.sh b/tests/functional/doubles_test.sh index b3ba9bd2..798c7bb0 100644 --- a/tests/functional/doubles_test.sh +++ b/tests/functional/doubles_test.sh @@ -106,3 +106,21 @@ function test_mock_mktemp_does_not_break_spy_creation() { assert_have_been_called_times 1 rm assert_have_been_called_with rm "-f" "/tmp/mocked_temp_file" } + +function test_spy_on_echo_does_not_hang() { + source ./tests/functional/fixtures/echo_function.sh + bashunit::spy echo + + write_message "hello world" + + assert_have_been_called echo +} + +function test_spy_on_printf_does_not_hang() { + source ./tests/functional/fixtures/printf_function.sh + bashunit::spy printf + + format_message "hello world" + + assert_have_been_called printf +} diff --git a/tests/functional/fixtures/echo_function.sh b/tests/functional/fixtures/echo_function.sh new file mode 100644 index 00000000..fc7fd1e2 --- /dev/null +++ b/tests/functional/fixtures/echo_function.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +function write_message() { + echo "message: $*" +} diff --git a/tests/functional/fixtures/printf_function.sh b/tests/functional/fixtures/printf_function.sh new file mode 100644 index 00000000..f8e298ad --- /dev/null +++ b/tests/functional/fixtures/printf_function.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +function format_message() { + printf "formatted: %s\n" "$*" +} diff --git a/tests/unit/test_doubles_test.sh b/tests/unit/test_doubles_test.sh index 169bf6b2..9a6e6530 100644 --- a/tests/unit/test_doubles_test.sh +++ b/tests/unit/test_doubles_test.sh @@ -214,3 +214,5 @@ function test_unsuccessful_spy_nth_called_with_invalid_index() { "expected call" "at index 5 but" "only called 1 times")" \ "$(assert_have_been_called_nth_with 5 ps "first")" } + + From bfcbfef9ee6c532715ad1ebb890c1c770c97a775 Mon Sep 17 00:00:00 2001 From: SauronBot Date: Sun, 29 Mar 2026 21:21:29 +0200 Subject: [PATCH 2/2] docs(changelog): add entry for fix #607 spy on echo/printf causing hang --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f0e0dc8..d24d347f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Fixed +- Fix spying on `echo` or `printf` causing bashunit to hang due to infinite recursion (#607) + ## [0.34.1](https://github.com/TypedDevs/bashunit/compare/0.34.0...0.34.1) - 2026-03-20 ### Added