Skip to content

Commit 15aa158

Browse files
authored
fix(coverage): ignore case comments and done redirections (#634) (#635)
1 parent 0d1a6d3 commit 15aa158

4 files changed

Lines changed: 110 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- LCOV and HTML coverage reports no longer produce empty output under `set -e` (#618)
2121
- `clock::now` handles `EPOCHREALTIME` values that use a comma decimal separator
2222
- Invalid `.env.example` coverage threshold entry; CI now copies `.env.example` to `.env` so config parse errors are caught
23+
- Coverage no longer counts case patterns with trailing comments (e.g. `*thing) # note`) or loop terminators with redirections/pipes (e.g. `done < file`, `done <<<"$var"`, `done | sort`) as executable lines (#634)
2324

2425
## [0.34.1](https://github.com/TypedDevs/bashunit/compare/0.34.0...0.34.1) - 2026-03-20
2526

src/coverage.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,11 @@ function bashunit::coverage::is_executable_line() {
449449
# Skip control flow keywords (then, else, fi, do, done, esac, in, ;;, ;&, ;;&)
450450
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*(then|else|fi|do|done|esac|in|;;|;;&|;&)[[:space:]]*(#.*)?$' || true)" -gt 0 ] && return 1
451451

452-
# Skip case patterns like "--option)" or "*)"
453-
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*[^\)]+\)[[:space:]]*$' || true)" -gt 0 ] && return 1
452+
# Skip loop terminator with trailing redirection/pipe/fd (e.g. "done < file", "done | sort", "done 2>&1", "done &")
453+
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*done[[:space:]]+[^[:space:]#].*$' || true)" -gt 0 ] && return 1
454+
455+
# Skip case patterns like "--option)" or "*) # comment"
456+
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*[^\)]+\)[[:space:]]*(#.*)?$' || true)" -gt 0 ] && return 1
454457

455458
# Skip standalone ) for arrays/subshells
456459
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*\)[[:space:]]*(#.*)?$' || true)" -gt 0 ] && return 1

tests/unit/coverage_core_test.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,49 @@ EOF
174174
rm -f "$temp_file"
175175
}
176176

177+
function test_coverage_get_executable_lines_ignores_case_comments_and_done_redirects() {
178+
# Regression for #634: case patterns with trailing comments and loop terminators
179+
# with redirections or pipes must be ignored when counting executable lines.
180+
local temp_file
181+
temp_file=$(mktemp)
182+
183+
cat >"$temp_file" <<'EOF'
184+
#!/usr/bin/env bash
185+
function demo() {
186+
case "$1" in
187+
*thing) # Looks for thing at end of text
188+
echo "thing"
189+
;;
190+
*) # fallback branch
191+
echo "other"
192+
;;
193+
esac
194+
195+
while read -r line; do
196+
echo "$line"
197+
done < /path/to/file
198+
199+
while read -r item; do
200+
echo "$item"
201+
done <<<"$some_var"
202+
203+
while read -r x; do
204+
echo "$x"
205+
done | sort
206+
}
207+
EOF
208+
209+
# Executable lines: case "$1" in, echo "thing", echo "other",
210+
# while read -r line, echo "$line", while read -r item, echo "$item",
211+
# while read -r x, echo "$x" -> 9 total.
212+
local count
213+
count=$(bashunit::coverage::get_executable_lines "$temp_file")
214+
215+
assert_equals "9" "$count"
216+
217+
rm -f "$temp_file"
218+
}
219+
177220
function test_coverage_get_executable_lines_does_not_exit_under_set_e() {
178221
local temp_file
179222
temp_file=$(mktemp)

tests/unit/coverage_executable_test.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,64 @@ function test_coverage_is_executable_line_returns_false_for_standalone_paren() {
219219
result=$(bashunit::coverage::is_executable_line ' )' 2 && echo "yes" || echo "no")
220220
assert_equals "no" "$result"
221221
}
222+
223+
function test_coverage_is_executable_line_returns_false_for_case_pattern_with_comment() {
224+
local input=' *thing) # Looks for thing at end of text'
225+
local result
226+
result=$(bashunit::coverage::is_executable_line "$input" 2 && echo "yes" || echo "no")
227+
assert_equals "no" "$result"
228+
}
229+
230+
function test_coverage_is_executable_line_returns_false_for_wildcard_case_with_comment() {
231+
local result
232+
result=$(bashunit::coverage::is_executable_line ' *) # fallback' 2 && echo "yes" || echo "no")
233+
assert_equals "no" "$result"
234+
}
235+
236+
function test_coverage_is_executable_line_returns_false_for_done_with_file_redirect() {
237+
local result
238+
result=$(bashunit::coverage::is_executable_line ' done < /path/to/file' 2 && echo "yes" || echo "no")
239+
assert_equals "no" "$result"
240+
}
241+
242+
function test_coverage_is_executable_line_returns_false_for_done_with_herestring() {
243+
local result
244+
result=$(bashunit::coverage::is_executable_line ' done <<<"$var"' 2 && echo "yes" || echo "no")
245+
assert_equals "no" "$result"
246+
}
247+
248+
function test_coverage_is_executable_line_returns_false_for_done_with_process_sub() {
249+
local result
250+
result=$(bashunit::coverage::is_executable_line ' done < <(some_cmd)' 2 && echo "yes" || echo "no")
251+
assert_equals "no" "$result"
252+
}
253+
254+
function test_coverage_is_executable_line_returns_false_for_done_with_redirect_and_comment() {
255+
local result
256+
result=$(bashunit::coverage::is_executable_line ' done < "$file" # read input' 2 && echo "yes" || echo "no")
257+
assert_equals "no" "$result"
258+
}
259+
260+
function test_coverage_is_executable_line_returns_false_for_done_with_pipe() {
261+
local result
262+
result=$(bashunit::coverage::is_executable_line ' done | sort' 2 && echo "yes" || echo "no")
263+
assert_equals "no" "$result"
264+
}
265+
266+
function test_coverage_is_executable_line_returns_false_for_done_with_fd_redirect() {
267+
local result
268+
result=$(bashunit::coverage::is_executable_line ' done 2>&1' 2 && echo "yes" || echo "no")
269+
assert_equals "no" "$result"
270+
}
271+
272+
function test_coverage_is_executable_line_returns_false_for_done_with_background() {
273+
local result
274+
result=$(bashunit::coverage::is_executable_line ' done &' 2 && echo "yes" || echo "no")
275+
assert_equals "no" "$result"
276+
}
277+
278+
function test_coverage_is_executable_line_returns_false_for_done_with_append_redirect() {
279+
local result
280+
result=$(bashunit::coverage::is_executable_line ' done >> /tmp/out.log' 2 && echo "yes" || echo "no")
281+
assert_equals "no" "$result"
282+
}

0 commit comments

Comments
 (0)