diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b2f5927..6f4553b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Fix spying on `echo` or `printf` causing bashunit to hang due to infinite recursion (#607) - Fix invalid `.env.example` coverage threshold entry and copy `.env.example` to `.env` in CI test workflows so configuration parse errors are caught during automated test runs - Fix `clock::now` shell-time parsing when `EPOCHREALTIME` uses a comma decimal separator +- Fix LCOV and HTML coverage reports generating incomplete/empty output due to post-increment operator causing silent exit under `set -e` (#618) ## [0.34.1](https://github.com/TypedDevs/bashunit/compare/0.34.0...0.34.1) - 2026-03-20 diff --git a/src/coverage.sh b/src/coverage.sh index cb8a7612..794ff346 100644 --- a/src/coverage.sh +++ b/src/coverage.sh @@ -465,8 +465,8 @@ function bashunit::coverage::get_executable_lines() { local line while IFS= read -r line || [ -n "$line" ]; do - ((lineno++)) - bashunit::coverage::is_executable_line "$line" "$lineno" && ((count++)) + ((++lineno)) + bashunit::coverage::is_executable_line "$line" "$lineno" && ((++count)) done <"$file" echo "$count" @@ -498,7 +498,7 @@ function bashunit::coverage::get_hit_lines() { local line_content line_content=$(sed -n "${line_num}p" "$file" 2>/dev/null) || continue if bashunit::coverage::is_executable_line "$line_content" "$line_num"; then - ((count++)) + ((++count)) fi done @@ -565,7 +565,7 @@ function bashunit::coverage::extract_functions() { local line while IFS= read -r line || [ -n "$line" ]; do - ((lineno++)) + ((++lineno)) # Check for function definition patterns # Pattern 1: function name() { or function name { @@ -644,10 +644,10 @@ function bashunit::coverage::get_function_coverage() { line_content=$(sed -n "${lineno}p" "$file" 2>/dev/null) || continue if bashunit::coverage::is_executable_line "$line_content" "$lineno"; then - ((executable++)) + ((++executable)) local line_hits=${_hits_ref[$lineno]:-0} if [ "$line_hits" -gt 0 ]; then - ((hit++)) + ((++hit)) fi fi done @@ -778,7 +778,7 @@ function bashunit::coverage::report_lcov() { local line # shellcheck disable=SC2094 while IFS= read -r line || [ -n "$line" ]; do - ((lineno++)) + ((++lineno)) bashunit::coverage::is_executable_line "$line" "$lineno" || continue echo "DA:${lineno},$(bashunit::coverage::get_line_hits "$file" "$lineno")" done <"$file" @@ -1543,10 +1543,10 @@ EOF local ln_content ln_content=$(sed -n "${ln}p" "$file" 2>/dev/null) || continue if bashunit::coverage::is_executable_line "$ln_content" "$ln"; then - ((fn_executable++)) + ((++fn_executable)) local ln_hits=${hits_by_line[$ln]:-0} if [ "$ln_hits" -gt 0 ]; then - ((fn_hit++)) + ((++fn_hit)) fi fi done @@ -1597,7 +1597,7 @@ EOF local lineno=0 local line while IFS= read -r line || [ -n "$line" ]; do - ((lineno++)) + ((++lineno)) local escaped_line escaped_line=$(bashunit::coverage::html_escape "$line") diff --git a/src/learn.sh b/src/learn.sh index adb5d3c9..c57c18a9 100644 --- a/src/learn.sh +++ b/src/learn.sh @@ -127,7 +127,7 @@ function bashunit::learn::show_progress() { for i in $(seq 1 $total_lessons); do if bashunit::learn::is_completed "lesson_$i"; then echo " ${_BASHUNIT_COLOR_PASSED}✓${_BASHUNIT_COLOR_DEFAULT} Lesson $i completed" - ((completed++)) || true + ((++completed)) || true else echo " ${_BASHUNIT_COLOR_INCOMPLETE}○${_BASHUNIT_COLOR_DEFAULT} Lesson $i" fi diff --git a/src/str.sh b/src/str.sh index 8ca614dd..b63232eb 100644 --- a/src/str.sh +++ b/src/str.sh @@ -40,17 +40,17 @@ function bashunit::str::rpad() { if [ "$original_char" = $'\x1b' ]; then while [ "${left_text:$j:1}" != "m" ] && [ $j -lt ${#left_text} ]; do result_left_text="$result_left_text${left_text:$j:1}" - ((j++)) + ((++j)) done result_left_text="$result_left_text${left_text:$j:1}" # Append the final 'm' - ((j++)) + ((++j)) elif [ "$char" = "$original_char" ]; then # Match the actual character result_left_text="$result_left_text$char" - ((i++)) - ((j++)) + ((++i)) + ((++j)) else - ((j++)) + ((++j)) fi done diff --git a/tests/unit/coverage_core_test.sh b/tests/unit/coverage_core_test.sh index 73183465..3a58b155 100644 --- a/tests/unit/coverage_core_test.sh +++ b/tests/unit/coverage_core_test.sh @@ -174,6 +174,29 @@ EOF rm -f "$temp_file" } +function test_coverage_get_executable_lines_does_not_exit_under_set_e() { + local temp_file + temp_file=$(mktemp) + + cat >"$temp_file" <<'EOF' +#!/usr/bin/env bash +echo "line 1" +echo "line 2" +EOF + + # ((var++)) when var=0 evaluates to 0 (falsy) causing exit code 1; + # under set -e this silently terminates the function (#618) + local result + result=$( + set -e + bashunit::coverage::get_executable_lines "$temp_file" + ) + + assert_equals "2" "$result" + + rm -f "$temp_file" +} + function test_coverage_record_line_writes_to_file() { BASHUNIT_COVERAGE="true" BASHUNIT_COVERAGE_PATHS="/" diff --git a/tests/unit/coverage_reporting_test.sh b/tests/unit/coverage_reporting_test.sh index d2af35d8..c0ecb5bd 100644 --- a/tests/unit/coverage_reporting_test.sh +++ b/tests/unit/coverage_reporting_test.sh @@ -146,6 +146,40 @@ EOF rm -f "$temp_file" "$report_file" } +function test_coverage_report_lcov_completes_under_set_e() { + BASHUNIT_COVERAGE="true" + bashunit::coverage::init + + local temp_file + temp_file=$(mktemp) + cat >"$temp_file" <<'EOF' +#!/usr/bin/env bash +echo "line 1" +echo "line 2" +EOF + + echo "$temp_file" >"$_BASHUNIT_COVERAGE_TRACKED_FILES" + + local report_file + report_file=$(mktemp) + + # ((lineno++)) when lineno=0 returns exit code 1 under set -e + # causing incomplete LCOV output (#618) + ( + set -e + bashunit::coverage::report_lcov "$report_file" + ) + + local content + content=$(cat "$report_file") + + assert_contains "end_of_record" "$content" + assert_contains "DA:2," "$content" + assert_contains "DA:3," "$content" + + rm -f "$temp_file" "$report_file" +} + function test_coverage_report_text_shows_no_files_message() { BASHUNIT_COVERAGE="true" bashunit::coverage::init diff --git a/tests/unit/str_test.sh b/tests/unit/str_test.sh index 33d8097b..54e71a58 100644 --- a/tests/unit/str_test.sh +++ b/tests/unit/str_test.sh @@ -51,6 +51,18 @@ function test_rpad_custom_width_padding_text_too_long_and_special_chars() { "$actual" } +function test_rpad_does_not_exit_under_set_e() { + # ((i++)) when i=0 evaluates to 0 (falsy) causing exit code 1; + # under set -e this silently terminates the function (#618) + local actual + actual=$( + set -e + bashunit::str::rpad "input" "1" 20 + ) + + assert_same "input 1" "$actual" +} + function test_rpad_width_smaller_than_right_word() { local actual=$(bashunit::str::rpad "foo" "verylongword" 5)