Skip to content

fix(coverage): count a branch-junction line as covered when either arm runs#35858

Merged
bartlomieju merged 1 commit into
denoland:mainfrom
Hixie:fix/lcov-else-junction-line-coverage
Jul 8, 2026
Merged

fix(coverage): count a branch-junction line as covered when either arm runs#35858
bartlomieju merged 1 commit into
denoland:mainfrom
Hixie:fix/lcov-else-junction-line-coverage

Conversation

@Hixie

@Hixie Hixie commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

(This PR was written by Opus 4.8 and reviewed by me, a human. I continue to have no real familiarity with this repository and so could not evaluate the fix for correctness, however it seems reasonable to me, though some of the details of the test expectation changes seemed a little strange. If there's anything I can do to help this land please let me know; this reduces some confusion we're seeing in our CI so I am motivated to address any feedback promptly.)

The LCOV line-hit count for a source line is reset to zero when a zero-count V8 coverage range overlaps the line and reaches one of its edges. On a line that holds a branch junction the two edges belong to different arms: the } that closes an if consequent and the else's opening { on a } else { line, and likewise } catch { and } finally {. When only one arm runs, the arm that did not run leaves a zero-count range that clips the opposite edge of the line, and the reset then zeroes the whole line even though the covered side genuinely ran.

The visible effect is that a } else { line counts as covered only when both arms run within the same coverage process. Run the two arms in separate test files, so separate processes, and merge the resulting LCOV per line, and the junction drops out of coverage. Its coverage then depends on how the test suite happens to be split across processes.

Reset a line to zero only when a zero-count range genuinely covers its code, judged three ways. The range spans the whole line content: a fully uncovered line, or one in the middle of a multi-line uncovered block. The range is an uncovered statement confined to the line that runs to the end of its code: the never-taken throw in if (!x) throw .... Or the range is a whole never-called function (its ranges[0]), which keeps the function's signature line uncovered like its body even though the range starts a few characters into that line after an export or async keyword. A multi-line block whose brace only clips a junction line extends past the line end and matches none of the three, so the covered half of the junction survives. Branch (BRDA) counts are unchanged.

A new spec runs a single arm of an if/else and asserts the } else { line is covered by the arm that ran. Existing coverage expectations that exercised one arm of a branch are updated: the junction line, and an if (...) { line whose condition ran but whose body did not, now count as covered.

Fixes #35857.

…m runs

The LCOV line-hit count for a source line is reset to zero when a
zero-count V8 coverage range overlaps the line and reaches one of its
edges. On a line that holds a branch junction the two edges belong to
different arms: the `}` that closes an `if` consequent and the `else`'s
opening `{` on a `} else {` line, and likewise `} catch {` and
`} finally {`. When only one arm runs, the arm that did not run leaves a
zero-count range that clips the opposite edge of the line, and the reset
then zeroes the whole line even though the covered side genuinely ran.

The visible effect is that a `} else {` line counts as covered only when
both arms run within the same coverage process. Run the two arms in
separate test files, so separate processes, and merge the resulting LCOV
per line, and the junction drops out of coverage. Its coverage then
depends on how the test suite happens to be split across processes.

Reset a line to zero only when a zero-count range genuinely covers its
code, judged three ways. The range spans the whole line content: a fully
uncovered line, or one in the middle of a multi-line uncovered block. The
range is an uncovered statement confined to the line that runs to the end
of its code: the never-taken `throw` in `if (!x) throw ...`. Or the range
is a whole never-called function (its ranges[0]), which keeps the
function's signature line uncovered like its body even though the range
starts a few characters into that line after an `export` or `async`
keyword. A multi-line block whose brace only clips a junction line extends
past the line end and matches none of the three, so the covered half of
the junction survives. Branch (BRDA) counts are unchanged.

A new spec runs a single arm of an `if`/`else` and asserts the `} else {`
line is covered by the arm that ran. Existing coverage expectations that
exercised one arm of a branch are updated: the junction line, and an
`if (...) {` line whose condition ran but whose body did not, now count as
covered.

Fixed denoland#35857

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this works

@bartlomieju
bartlomieju merged commit 95407f0 into denoland:main Jul 8, 2026
136 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deno coverage: a } else { line is reported uncovered unless both branches run in the same process

2 participants