Skip to content

Commit 23216f1

Browse files
philnashUlisesGascon
authored andcommitted
test_runner: report covered lines, functions and branches to reporters
This is a breaking change for the format of test:coverage events. But the test coverage is still experimental, so I don't believe it requires a semver-major bump. Fixes #49303 PR-URL: #49320 Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
1 parent e845155 commit 23216f1

File tree

5 files changed

+125
-9
lines changed

5 files changed

+125
-9
lines changed

doc/api/test.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -2026,8 +2026,18 @@ object, streaming a series of events representing the execution of the tests.
20262026
* `coveredLinePercent` {number} The percentage of lines covered.
20272027
* `coveredBranchPercent` {number} The percentage of branches covered.
20282028
* `coveredFunctionPercent` {number} The percentage of functions covered.
2029-
* `uncoveredLineNumbers` {Array} An array of integers representing line
2030-
numbers that are uncovered.
2029+
* `functions` {Array} An array of functions representing function
2030+
coverage.
2031+
* `name` {string} The name of the function.
2032+
* `line` {number} The line number where the function is defined.
2033+
* `count` {number} The number of times the function was called.
2034+
* `branches` {Array} An array of branches representing branch coverage.
2035+
* `line` {number} The line number where the branch is defined.
2036+
* `count` {number} The number of times the branch was taken.
2037+
* `lines` {Array} An array of lines representing line
2038+
numbers and the number of times they were covered.
2039+
* `line` {number} The line number.
2040+
* `count` {number} The number of times the line was covered.
20312041
* `totals` {Object} An object containing a summary of coverage for all
20322042
files.
20332043
* `totalLineCount` {number} The total number of lines.

lib/internal/test_runner/coverage.js

+35-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
StringPrototypeIncludes,
1414
StringPrototypeLocaleCompare,
1515
StringPrototypeStartsWith,
16+
MathMax,
1617
} = primordials;
1718
const {
1819
copyFileSync,
@@ -43,6 +44,7 @@ class CoverageLine {
4344
this.startOffset = startOffset;
4445
this.endOffset = startOffset + src.length - newlineLength;
4546
this.ignore = false;
47+
this.count = 0;
4648
this.#covered = true;
4749
}
4850

@@ -118,6 +120,8 @@ class TestCoverage {
118120
let totalFunctions = 0;
119121
let branchesCovered = 0;
120122
let functionsCovered = 0;
123+
const functionReports = [];
124+
const branchReports = [];
121125

122126
const lines = ArrayPrototypeMap(linesWithBreaks, (line, i) => {
123127
const startOffset = offset;
@@ -159,12 +163,20 @@ class TestCoverage {
159163
for (let j = 0; j < functions.length; ++j) {
160164
const { isBlockCoverage, ranges } = functions[j];
161165

166+
let maxCountPerFunction = 0;
162167
for (let k = 0; k < ranges.length; ++k) {
163168
const range = ranges[k];
169+
maxCountPerFunction = MathMax(maxCountPerFunction, range.count);
164170

165171
mapRangeToLines(range, lines);
166172

167173
if (isBlockCoverage) {
174+
ArrayPrototypePush(branchReports, {
175+
__proto__: null,
176+
line: range.lines[0].line,
177+
count: range.count,
178+
});
179+
168180
if (range.count !== 0 ||
169181
range.ignoredLines === range.lines.length) {
170182
branchesCovered++;
@@ -177,6 +189,13 @@ class TestCoverage {
177189
if (j > 0 && ranges.length > 0) {
178190
const range = ranges[0];
179191

192+
ArrayPrototypePush(functionReports, {
193+
__proto__: null,
194+
name: functions[j].functionName,
195+
count: maxCountPerFunction,
196+
line: range.lines[0].line,
197+
});
198+
180199
if (range.count !== 0 || range.ignoredLines === range.lines.length) {
181200
functionsCovered++;
182201
}
@@ -186,15 +205,19 @@ class TestCoverage {
186205
}
187206

188207
let coveredCnt = 0;
189-
const uncoveredLineNums = [];
208+
const lineReports = [];
190209

191210
for (let j = 0; j < lines.length; ++j) {
192211
const line = lines[j];
193-
212+
if (!line.ignore) {
213+
ArrayPrototypePush(lineReports, {
214+
__proto__: null,
215+
line: line.line,
216+
count: line.count,
217+
});
218+
}
194219
if (line.covered || line.ignore) {
195220
coveredCnt++;
196-
} else {
197-
ArrayPrototypePush(uncoveredLineNums, line.line);
198221
}
199222
}
200223

@@ -210,7 +233,9 @@ class TestCoverage {
210233
coveredLinePercent: toPercentage(coveredCnt, lines.length),
211234
coveredBranchPercent: toPercentage(branchesCovered, totalBranches),
212235
coveredFunctionPercent: toPercentage(functionsCovered, totalFunctions),
213-
uncoveredLineNumbers: uncoveredLineNums,
236+
functions: functionReports,
237+
branches: branchReports,
238+
lines: lineReports,
214239
});
215240

216241
coverageSummary.totals.totalLineCount += lines.length;
@@ -320,6 +345,11 @@ function mapRangeToLines(range, lines) {
320345
if (count === 0 && startOffset <= line.startOffset &&
321346
endOffset >= line.endOffset) {
322347
line.covered = false;
348+
line.count = 0;
349+
}
350+
if (count > 0 && startOffset <= line.startOffset &&
351+
endOffset >= line.endOffset) {
352+
line.count = count;
323353
}
324354

325355
ArrayPrototypePush(mappedLines, line);

lib/internal/test_runner/utils.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const {
33
ArrayPrototypeJoin,
44
ArrayPrototypeMap,
5+
ArrayPrototypeFlatMap,
56
ArrayPrototypePush,
67
ArrayPrototypeReduce,
78
ObjectGetOwnPropertyDescriptor,
@@ -303,6 +304,10 @@ function formatLinesToRanges(values) {
303304
}, []), (range) => ArrayPrototypeJoin(range, '-'));
304305
}
305306

307+
function getUncoveredLines(lines) {
308+
return ArrayPrototypeFlatMap(lines, (line) => (line.count === 0 ? line.line : []));
309+
}
310+
306311
function formatUncoveredLines(lines, table) {
307312
if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' ');
308313
return ArrayPrototypeJoin(lines, ', ');
@@ -332,7 +337,7 @@ function getCoverageReport(pad, summary, symbol, color, table) {
332337
const columnsWidth = ArrayPrototypeReduce(columnPadLengths, (acc, columnPadLength) => acc + columnPadLength + 3, 0);
333338

334339
uncoveredLinesPadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) =>
335-
MathMax(acc, formatUncoveredLines(file.uncoveredLineNumbers, table).length), 0);
340+
MathMax(acc, formatUncoveredLines(getUncoveredLines(file.lines), table).length), 0);
336341
uncoveredLinesPadLength = MathMax(uncoveredLinesPadLength, 'uncovered lines'.length);
337342
const uncoveredLinesWidth = uncoveredLinesPadLength + 2;
338343

@@ -394,7 +399,7 @@ function getCoverageReport(pad, summary, symbol, color, table) {
394399

395400
report += `${prefix}${getCell(relativePath, filePadLength, StringPrototypePadEnd, truncateStart, fileCoverage)}${kSeparator}` +
396401
`${ArrayPrototypeJoin(ArrayPrototypeMap(coverages, (coverage, j) => getCell(NumberPrototypeToFixed(coverage, 2), columnPadLengths[j], StringPrototypePadStart, false, coverage)), kSeparator)}${kSeparator}` +
397-
`${getCell(formatUncoveredLines(file.uncoveredLineNumbers, table), uncoveredLinesPadLength, false, truncateEnd)}\n`;
402+
`${getCell(formatUncoveredLines(getUncoveredLines(file.lines), table), uncoveredLinesPadLength, false, truncateEnd)}\n`;
398403
}
399404

400405
// Foot
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Transform } from 'node:stream';
2+
3+
export default class CoverageReporter extends Transform {
4+
constructor(options) {
5+
super({ ...options, writableObjectMode: true });
6+
}
7+
8+
_transform(event, _encoding, callback) {
9+
if (event.type === 'test:coverage') {
10+
callback(null, JSON.stringify(event.data, null, 2));
11+
} else {
12+
callback(null);
13+
}
14+
}
15+
}

test/parallel/test-runner-coverage.js

+56
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,59 @@ test('coverage is combined for multiple processes', skipIfNoInspector, () => {
181181
assert(result.stdout.toString().includes(report));
182182
assert.strictEqual(result.status, 0);
183183
});
184+
185+
test('coverage reports on lines, functions, and branches', skipIfNoInspector, async (t) => {
186+
const fixture = fixtures.path('test-runner', 'coverage.js');
187+
const child = spawnSync(process.execPath,
188+
['--test', '--experimental-test-coverage', '--test-reporter',
189+
fixtures.fileURL('test-runner/custom_reporters/coverage.mjs'),
190+
fixture]);
191+
assert.strictEqual(child.stderr.toString(), '');
192+
const stdout = child.stdout.toString();
193+
const coverage = JSON.parse(stdout);
194+
195+
await t.test('does not include node_modules', () => {
196+
assert.strictEqual(coverage.summary.files.length, 3);
197+
const files = ['coverage.js', 'invalid-tap.js', 'throw.js'];
198+
coverage.summary.files.forEach((file, index) => {
199+
assert.ok(file.path.endsWith(files[index]));
200+
});
201+
});
202+
203+
const file = coverage.summary.files[0];
204+
205+
await t.test('reports on function coverage', () => {
206+
const uncalledFunction = file.functions.find((f) => f.name === 'uncalledTopLevelFunction');
207+
assert.strictEqual(uncalledFunction.count, 0);
208+
assert.strictEqual(uncalledFunction.line, 16);
209+
210+
const calledTwice = file.functions.find((f) => f.name === 'fnWithControlFlow');
211+
assert.strictEqual(calledTwice.count, 2);
212+
assert.strictEqual(calledTwice.line, 35);
213+
});
214+
215+
await t.test('reports on branch coverage', () => {
216+
const uncalledBranch = file.branches.find((b) => b.line === 6);
217+
assert.strictEqual(uncalledBranch.count, 0);
218+
219+
const calledTwice = file.branches.find((b) => b.line === 35);
220+
assert.strictEqual(calledTwice.count, 2);
221+
});
222+
223+
await t.test('reports on line coverage', () => {
224+
[
225+
{ line: 36, count: 2 },
226+
{ line: 37, count: 1 },
227+
{ line: 38, count: 1 },
228+
{ line: 39, count: 0 },
229+
{ line: 40, count: 1 },
230+
{ line: 41, count: 1 },
231+
{ line: 42, count: 1 },
232+
{ line: 43, count: 0 },
233+
{ line: 44, count: 0 },
234+
].forEach((line) => {
235+
const testLine = file.lines.find((l) => l.line === line.line);
236+
assert.strictEqual(testLine.count, line.count);
237+
});
238+
});
239+
});

0 commit comments

Comments
 (0)