mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
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 https://github.com/nodejs/node/issues/49303 PR-URL: https://github.com/nodejs/node/pull/49320 Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
This commit is contained in:
1 parent
fcf5de008a
commit
3a6a80a4e1
5 files changed
+125
-9
No files matched your search
+12
-2
@@ -2011,8 +2011,18 @@ object, streaming a series of events representing the execution of the tests.
|
||||
* `coveredLinePercent` {number} The percentage of lines covered.
|
||||
* `coveredBranchPercent` {number} The percentage of branches covered.
|
||||
* `coveredFunctionPercent` {number} The percentage of functions covered.
|
||||
* `uncoveredLineNumbers` {Array} An array of integers representing line
|
||||
numbers that are uncovered.
|
||||
* `functions` {Array} An array of functions representing function
|
||||
coverage.
|
||||
* `name` {string} The name of the function.
|
||||
* `line` {number} The line number where the function is defined.
|
||||
* `count` {number} The number of times the function was called.
|
||||
* `branches` {Array} An array of branches representing branch coverage.
|
||||
* `line` {number} The line number where the branch is defined.
|
||||
* `count` {number} The number of times the branch was taken.
|
||||
* `lines` {Array} An array of lines representing line
|
||||
numbers and the number of times they were covered.
|
||||
* `line` {number} The line number.
|
||||
* `count` {number} The number of times the line was covered.
|
||||
* `totals` {Object} An object containing a summary of coverage for all
|
||||
files.
|
||||
* `totalLineCount` {number} The total number of lines.
|
||||
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
StringPrototypeIncludes,
|
||||
StringPrototypeLocaleCompare,
|
||||
StringPrototypeStartsWith,
|
||||
MathMax,
|
||||
} = primordials;
|
||||
const {
|
||||
copyFileSync,
|
||||
@@ -43,6 +44,7 @@ class CoverageLine {
|
||||
this.startOffset = startOffset;
|
||||
this.endOffset = startOffset + src.length - newlineLength;
|
||||
this.ignore = false;
|
||||
this.count = 0;
|
||||
this.#covered = true;
|
||||
}
|
||||
|
||||
@@ -118,6 +120,8 @@ class TestCoverage {
|
||||
let totalFunctions = 0;
|
||||
let branchesCovered = 0;
|
||||
let functionsCovered = 0;
|
||||
const functionReports = [];
|
||||
const branchReports = [];
|
||||
|
||||
const lines = ArrayPrototypeMap(linesWithBreaks, (line, i) => {
|
||||
const startOffset = offset;
|
||||
@@ -159,12 +163,20 @@ class TestCoverage {
|
||||
for (let j = 0; j < functions.length; ++j) {
|
||||
const { isBlockCoverage, ranges } = functions[j];
|
||||
|
||||
let maxCountPerFunction = 0;
|
||||
for (let k = 0; k < ranges.length; ++k) {
|
||||
const range = ranges[k];
|
||||
maxCountPerFunction = MathMax(maxCountPerFunction, range.count);
|
||||
|
||||
mapRangeToLines(range, lines);
|
||||
|
||||
if (isBlockCoverage) {
|
||||
ArrayPrototypePush(branchReports, {
|
||||
__proto__: null,
|
||||
line: range.lines[0].line,
|
||||
count: range.count,
|
||||
});
|
||||
|
||||
if (range.count !== 0 ||
|
||||
range.ignoredLines === range.lines.length) {
|
||||
branchesCovered++;
|
||||
@@ -177,6 +189,13 @@ class TestCoverage {
|
||||
if (j > 0 && ranges.length > 0) {
|
||||
const range = ranges[0];
|
||||
|
||||
ArrayPrototypePush(functionReports, {
|
||||
__proto__: null,
|
||||
name: functions[j].functionName,
|
||||
count: maxCountPerFunction,
|
||||
line: range.lines[0].line,
|
||||
});
|
||||
|
||||
if (range.count !== 0 || range.ignoredLines === range.lines.length) {
|
||||
functionsCovered++;
|
||||
}
|
||||
@@ -186,15 +205,19 @@ class TestCoverage {
|
||||
}
|
||||
|
||||
let coveredCnt = 0;
|
||||
const uncoveredLineNums = [];
|
||||
const lineReports = [];
|
||||
|
||||
for (let j = 0; j < lines.length; ++j) {
|
||||
const line = lines[j];
|
||||
|
||||
if (!line.ignore) {
|
||||
ArrayPrototypePush(lineReports, {
|
||||
__proto__: null,
|
||||
line: line.line,
|
||||
count: line.count,
|
||||
});
|
||||
}
|
||||
if (line.covered || line.ignore) {
|
||||
coveredCnt++;
|
||||
} else {
|
||||
ArrayPrototypePush(uncoveredLineNums, line.line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +233,9 @@ class TestCoverage {
|
||||
coveredLinePercent: toPercentage(coveredCnt, lines.length),
|
||||
coveredBranchPercent: toPercentage(branchesCovered, totalBranches),
|
||||
coveredFunctionPercent: toPercentage(functionsCovered, totalFunctions),
|
||||
uncoveredLineNumbers: uncoveredLineNums,
|
||||
functions: functionReports,
|
||||
branches: branchReports,
|
||||
lines: lineReports,
|
||||
});
|
||||
|
||||
coverageSummary.totals.totalLineCount += lines.length;
|
||||
@@ -320,6 +345,11 @@ function mapRangeToLines(range, lines) {
|
||||
if (count === 0 && startOffset <= line.startOffset &&
|
||||
endOffset >= line.endOffset) {
|
||||
line.covered = false;
|
||||
line.count = 0;
|
||||
}
|
||||
if (count > 0 && startOffset <= line.startOffset &&
|
||||
endOffset >= line.endOffset) {
|
||||
line.count = count;
|
||||
}
|
||||
|
||||
ArrayPrototypePush(mappedLines, line);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const {
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypeMap,
|
||||
ArrayPrototypeFlatMap,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeReduce,
|
||||
ObjectGetOwnPropertyDescriptor,
|
||||
@@ -297,6 +298,10 @@ function formatLinesToRanges(values) {
|
||||
}, []), (range) => ArrayPrototypeJoin(range, '-'));
|
||||
}
|
||||
|
||||
function getUncoveredLines(lines) {
|
||||
return ArrayPrototypeFlatMap(lines, (line) => (line.count === 0 ? line.line : []));
|
||||
}
|
||||
|
||||
function formatUncoveredLines(lines, table) {
|
||||
if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' ');
|
||||
return ArrayPrototypeJoin(lines, ', ');
|
||||
@@ -326,7 +331,7 @@ function getCoverageReport(pad, summary, symbol, color, table) {
|
||||
const columnsWidth = ArrayPrototypeReduce(columnPadLengths, (acc, columnPadLength) => acc + columnPadLength + 3, 0);
|
||||
|
||||
uncoveredLinesPadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) =>
|
||||
MathMax(acc, formatUncoveredLines(file.uncoveredLineNumbers, table).length), 0);
|
||||
MathMax(acc, formatUncoveredLines(getUncoveredLines(file.lines), table).length), 0);
|
||||
uncoveredLinesPadLength = MathMax(uncoveredLinesPadLength, 'uncovered lines'.length);
|
||||
const uncoveredLinesWidth = uncoveredLinesPadLength + 2;
|
||||
|
||||
@@ -388,7 +393,7 @@ function getCoverageReport(pad, summary, symbol, color, table) {
|
||||
|
||||
report += `${prefix}${getCell(relativePath, filePadLength, StringPrototypePadEnd, truncateStart, fileCoverage)}${kSeparator}` +
|
||||
`${ArrayPrototypeJoin(ArrayPrototypeMap(coverages, (coverage, j) => getCell(NumberPrototypeToFixed(coverage, 2), columnPadLengths[j], StringPrototypePadStart, false, coverage)), kSeparator)}${kSeparator}` +
|
||||
`${getCell(formatUncoveredLines(file.uncoveredLineNumbers, table), uncoveredLinesPadLength, false, truncateEnd)}\n`;
|
||||
`${getCell(formatUncoveredLines(getUncoveredLines(file.lines), table), uncoveredLinesPadLength, false, truncateEnd)}\n`;
|
||||
}
|
||||
|
||||
// Foot
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Transform } from 'node:stream';
|
||||
|
||||
export default class CoverageReporter extends Transform {
|
||||
constructor(options) {
|
||||
super({ ...options, writableObjectMode: true });
|
||||
}
|
||||
|
||||
_transform(event, _encoding, callback) {
|
||||
if (event.type === 'test:coverage') {
|
||||
callback(null, JSON.stringify(event.data, null, 2));
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,3 +182,59 @@ test('coverage is combined for multiple processes', skipIfNoInspector, () => {
|
||||
assert(result.stdout.toString().includes(report));
|
||||
assert.strictEqual(result.status, 0);
|
||||
});
|
||||
|
||||
test('coverage reports on lines, functions, and branches', skipIfNoInspector, async (t) => {
|
||||
const fixture = fixtures.path('test-runner', 'coverage.js');
|
||||
const child = spawnSync(process.execPath,
|
||||
['--test', '--experimental-test-coverage', '--test-reporter',
|
||||
fixtures.fileURL('test-runner/custom_reporters/coverage.mjs'),
|
||||
fixture]);
|
||||
assert.strictEqual(child.stderr.toString(), '');
|
||||
const stdout = child.stdout.toString();
|
||||
const coverage = JSON.parse(stdout);
|
||||
|
||||
await t.test('does not include node_modules', () => {
|
||||
assert.strictEqual(coverage.summary.files.length, 3);
|
||||
const files = ['coverage.js', 'invalid-tap.js', 'throw.js'];
|
||||
coverage.summary.files.forEach((file, index) => {
|
||||
assert.ok(file.path.endsWith(files[index]));
|
||||
});
|
||||
});
|
||||
|
||||
const file = coverage.summary.files[0];
|
||||
|
||||
await t.test('reports on function coverage', () => {
|
||||
const uncalledFunction = file.functions.find((f) => f.name === 'uncalledTopLevelFunction');
|
||||
assert.strictEqual(uncalledFunction.count, 0);
|
||||
assert.strictEqual(uncalledFunction.line, 16);
|
||||
|
||||
const calledTwice = file.functions.find((f) => f.name === 'fnWithControlFlow');
|
||||
assert.strictEqual(calledTwice.count, 2);
|
||||
assert.strictEqual(calledTwice.line, 35);
|
||||
});
|
||||
|
||||
await t.test('reports on branch coverage', () => {
|
||||
const uncalledBranch = file.branches.find((b) => b.line === 6);
|
||||
assert.strictEqual(uncalledBranch.count, 0);
|
||||
|
||||
const calledTwice = file.branches.find((b) => b.line === 35);
|
||||
assert.strictEqual(calledTwice.count, 2);
|
||||
});
|
||||
|
||||
await t.test('reports on line coverage', () => {
|
||||
[
|
||||
{ line: 36, count: 2 },
|
||||
{ line: 37, count: 1 },
|
||||
{ line: 38, count: 1 },
|
||||
{ line: 39, count: 0 },
|
||||
{ line: 40, count: 1 },
|
||||
{ line: 41, count: 1 },
|
||||
{ line: 42, count: 1 },
|
||||
{ line: 43, count: 0 },
|
||||
{ line: 44, count: 0 },
|
||||
].forEach((line) => {
|
||||
const testLine = file.lines.find((l) => l.line === line.line);
|
||||
assert.strictEqual(testLine.count, line.count);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user