diff --git a/CHANGELOG.md b/CHANGELOG.md index 50950b63..ed7cc00c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed - A test that exited non-zero no longer poisons the exit code of subsequent tests in the same file (the per-test exit code was accumulated instead of reset) - 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..5af83d72 100644 --- a/tests/unit/coverage_core_test.sh +++ b/tests/unit/coverage_core_test.sh @@ -268,6 +268,33 @@ 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" + + # 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" + 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