From f4a6095a08c08bc6539d48dee932cee887a9b54c Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Tue, 16 Jun 2026 08:46:34 +0200 Subject: [PATCH 1/2] fix(coverage): use builtin printf so printf spy cannot drop coverage data Spying or mocking the printf builtin in a test shadowed the bare printf calls in coverage flush_buffer, redirecting the buffered coverage write to the spy instead of the data file. All coverage for that test was silently lost. Flush with `builtin printf` to bypass the shadow. Closes #724 --- CHANGELOG.md | 1 + src/coverage.sh | 8 +++++--- tests/unit/coverage_core_test.sh | 30 ++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76bd7dcb..e8de0bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed - Coverage report now counts backslash line-continuation lines as covered: a multi-line statement's hit is propagated forward across its continuation chain, so the lines after a trailing `\` are no longer reported as uncovered (#722) +- Spying or mocking the `printf` builtin no longer breaks coverage collection: the coverage buffer is now flushed with `builtin printf`, so a test double can no longer shadow the write and silently drop all coverage data for that test (#724) ### Changed - Documentation and project URLs now point to the new primary domain `bashunit.com` (old `bashunit.typeddevs.com` continues to work as a redirect) diff --git a/src/coverage.sh b/src/coverage.sh index 4807aa09..097766ce 100644 --- a/src/coverage.sh +++ b/src/coverage.sh @@ -330,11 +330,13 @@ function bashunit::coverage::flush_buffer() { test_hits_file="${_BASHUNIT_COVERAGE_TEST_HITS_FILE}.$$" fi - # Write buffered data in a single I/O operation - printf '%s' "$_BASHUNIT_COVERAGE_BUFFER" >>"$data_file" + # Write buffered data in a single I/O operation. + # Use `builtin printf` so a user test spying/mocking the printf builtin + # cannot shadow the coverage write and silently drop data (see issue #724). + builtin printf '%s' "$_BASHUNIT_COVERAGE_BUFFER" >>"$data_file" if [ -n "$_BASHUNIT_COVERAGE_HITS_BUFFER" ]; then - printf '%s' "$_BASHUNIT_COVERAGE_HITS_BUFFER" >>"$test_hits_file" + builtin printf '%s' "$_BASHUNIT_COVERAGE_HITS_BUFFER" >>"$test_hits_file" fi # Reset buffer diff --git a/tests/unit/coverage_core_test.sh b/tests/unit/coverage_core_test.sh index fdf0fc34..1351d66e 100644 --- a/tests/unit/coverage_core_test.sh +++ b/tests/unit/coverage_core_test.sh @@ -268,6 +268,36 @@ function test_coverage_record_line_writes_to_file() { assert_contains "$test_file:20" "$content" } +function test_coverage_flush_buffer_writes_even_when_printf_is_spied() { + BASHUNIT_COVERAGE="true" + BASHUNIT_COVERAGE_PATHS="/" + BASHUNIT_COVERAGE_EXCLUDE="" + bashunit::coverage::init + + local test_file="/some/path/script.sh" + bashunit::coverage::record_line "$test_file" "10" + bashunit::coverage::record_line "$test_file" "20" + + # A user test spying on the printf builtin must not shadow the coverage + # internals: buffered data must still be flushed to disk (see issue #724). + bashunit::spy printf + + bashunit::coverage::flush_buffer + + bashunit::unmock printf + + local data_file="$_BASHUNIT_COVERAGE_DATA_FILE" + if bashunit::parallel::is_enabled; then + data_file="${_BASHUNIT_COVERAGE_DATA_FILE}.$$" + fi + + local content + content=$(cat "$data_file") + + assert_contains "$test_file:10" "$content" + assert_contains "$test_file:20" "$content" +} + function test_coverage_cleanup_removes_temp_files() { BASHUNIT_COVERAGE="true" bashunit::coverage::init From 15e30ae88ff58a22a8baf6f6deef80e1864d5de6 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Tue, 16 Jun 2026 08:49:42 +0200 Subject: [PATCH 2/2] test(coverage): tidy printf-spy regression test --- tests/unit/coverage_core_test.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/unit/coverage_core_test.sh b/tests/unit/coverage_core_test.sh index 1351d66e..5af83d72 100644 --- a/tests/unit/coverage_core_test.sh +++ b/tests/unit/coverage_core_test.sh @@ -278,12 +278,9 @@ function test_coverage_flush_buffer_writes_even_when_printf_is_spied() { bashunit::coverage::record_line "$test_file" "10" bashunit::coverage::record_line "$test_file" "20" - # A user test spying on the printf builtin must not shadow the coverage - # internals: buffered data must still be flushed to disk (see issue #724). + # Spying printf must not shadow the coverage write (issue #724) bashunit::spy printf - bashunit::coverage::flush_buffer - bashunit::unmock printf local data_file="$_BASHUNIT_COVERAGE_DATA_FILE"