Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions lib/internal/test_runner/coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,24 @@ class TestCoverage {
ObjectAssign(range, mapRangeToLines(range, lines));

if (isBlockCoverage) {
// Skip branches where all lines are ignored (similar to line handling)
if (range.ignoredLines === range.lines.length) {
continue;
}

// If the branch is uncovered but contains ignored lines, treat it as
// covered. This matches the behavior of tools like c8 and ensures that
// ignored code doesn't penalize branch coverage.
const branchCount = (range.count === 0 && range.ignoredLines > 0) ?
1 : range.count;

ArrayPrototypePush(branchReports, {
__proto__: null,
line: range.lines[0]?.line,
count: range.count,
count: branchCount,
});

if (range.count !== 0 ||
range.ignoredLines === range.lines.length) {
if (branchCount !== 0) {
branchesCovered++;
}

Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/test-runner/coverage-ignore-branch/source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';
// Source file for testing that branch coverage respects ignore comments

function getValue(condition) {
if (condition) {
return 'truthy';
}
/* node:coverage ignore next */
return 'falsy';
}

module.exports = { getValue };
9 changes: 9 additions & 0 deletions test/fixtures/test-runner/coverage-ignore-branch/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';
const { test } = require('node:test');
const assert = require('node:assert');
const { getValue } = require('./source.js');

// Only call with true, so the false branch is "uncovered" but ignored
test('getValue returns truthy for true', () => {
assert.strictEqual(getValue(true), 'truthy');
});
41 changes: 41 additions & 0 deletions test/parallel/test-runner-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,44 @@ test('coverage with directory and file named "file"', skipIfNoInspector, () => {
assert.strictEqual(result.status, 0);
assert(result.stdout.toString().includes('start of coverage report'));
});

// Regression test for https://github.com/nodejs/node/issues/61586
test('coverage ignore comments exclude branches in LCOV output', skipIfNoInspector, () => {
const fixture = fixtures.path('test-runner', 'coverage-ignore-branch', 'test.js');
const args = [
'--experimental-test-coverage',
'--test-reporter', 'lcov',
'--test-coverage-exclude=!test/fixtures/test-runner/coverage-ignore-branch/**',
fixture,
];
const result = spawnSync(process.execPath, args);
const lcov = result.stdout.toString();

assert.strictEqual(result.stderr.toString(), '');
assert.strictEqual(result.status, 0);

// Extract the source.js section from LCOV output
const sourceSection = lcov.split('end_of_record')
.find((s) => s.includes('source.js'));
assert(sourceSection, 'LCOV should contain source.js coverage');

// Verify that all branches are reported as covered (BRH should equal BRF)
// The ignored branch should not penalize coverage
const brfMatch = sourceSection.match(/BRF:(\d+)/);
const brhMatch = sourceSection.match(/BRH:(\d+)/);
assert(brfMatch, 'LCOV should contain BRF');
assert(brhMatch, 'LCOV should contain BRH');
assert.strictEqual(brfMatch[1], brhMatch[1],
`All branches should be covered when ignored code is not executed. ` +
`BRF=${brfMatch[1]}, BRH=${brhMatch[1]}`);

// Verify no BRDA entries show 0 (uncovered) for the ignored branch
// The branch at the if statement should be covered, not penalized by the ignored return
const brdaEntries = sourceSection.match(/BRDA:\d+,\d+,\d+,(\d+)/g) || [];
for (const entry of brdaEntries) {
const count = entry.match(/BRDA:\d+,\d+,\d+,(\d+)/)[1];
assert.notStrictEqual(count, '0',
`No branch should show 0 coverage when the ` +
`uncovered path is ignored: ${entry}`);
}
});