Skip to content

Commit b76f0c1

Browse files
committed
test_runner: refactor coverage report output for readability
Add a "table" parameter to getCoverageReport. Keep the tap coverage output intact. Change the output by adding padding and truncating the tables' cells. Add separation lines for table head/body/foot. Group uncovered lines as ranges. Add yellow color for coverage between 50 and 90. Refs: nodejs#46674
1 parent 6827dbb commit b76f0c1

File tree

3 files changed

+141
-38
lines changed

3 files changed

+141
-38
lines changed

lib/internal/test_runner/reporter/spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class SpecReporter extends Transform {
120120
case 'test:diagnostic':
121121
return `${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`;
122122
case 'test:coverage':
123-
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue);
123+
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue, true);
124124
}
125125
}
126126
_transform({ type, data }, encoding, callback) {

lib/internal/test_runner/utils.js

Lines changed: 129 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ const {
33
ArrayPrototypeJoin,
44
ArrayPrototypeMap,
55
ArrayPrototypePush,
6+
ArrayPrototypeReduce,
67
ObjectGetOwnPropertyDescriptor,
8+
MathFloor,
9+
MathMax,
10+
MathMin,
711
NumberPrototypeToFixed,
812
SafePromiseAllReturnArrayLike,
913
RegExp,
1014
RegExpPrototypeExec,
1115
SafeMap,
16+
StringPrototypePadStart,
17+
StringPrototypePadEnd,
1218
} = primordials;
1319

1420
const { basename, relative } = require('path');
@@ -27,6 +33,13 @@ const {
2733
} = require('internal/errors');
2834
const { compose } = require('stream');
2935

36+
const coverageColors = {
37+
'__proto__': null,
38+
'high': green,
39+
'medium': '\u001b[33m',
40+
'low': red,
41+
};
42+
3043
const kMultipleCallbackInvocations = 'multipleCallbackInvocations';
3144
const kRegExpPattern = /^\/(.*)\/([a-z]*)$/;
3245
const kSupportedFileExtensions = /\.[cm]?js$/;
@@ -250,45 +263,130 @@ function countCompletedTest(test, harness = test.root.harness) {
250263
}
251264

252265

253-
function coverageThreshold(coverage, color) {
254-
coverage = NumberPrototypeToFixed(coverage, 2);
255-
if (color) {
256-
if (coverage > 90) return `${green}${coverage}${color}`;
257-
if (coverage < 50) return `${red}${coverage}${color}`;
258-
}
259-
return coverage;
266+
function addTableLine(prefix, width) {
267+
return `${prefix}${'-'.repeat(width)}\n`;
268+
}
269+
270+
function truncateStart(string, width) {
271+
return string.length > width ? `\u2026${string.substring(string.length - width + 1, string.length)}` : string;
272+
}
273+
274+
function truncateEnd(string, width) {
275+
return string.length > width ? `${string.substring(0, width - 1)}\u2026` : string;
276+
}
277+
278+
function formatLinesToRanges(values) {
279+
return ArrayPrototypeMap(ArrayPrototypeReduce(values, (prev, current, index, array) => {
280+
if ((index > 0) && ((current - array[index - 1]) === 1)) {
281+
prev[prev.length - 1][1] = current;
282+
} else {
283+
prev.push([current]);
284+
}
285+
return prev;
286+
}, []), (range) => range.join('-'));
287+
}
288+
289+
function formatUncoveredLines(lines, table) {
290+
if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' ');
291+
return ArrayPrototypeJoin(lines, ', ');
260292
}
261293

262-
function getCoverageReport(pad, summary, symbol, color) {
263-
let report = `${color}${pad}${symbol}start of coverage report\n`;
294+
const kColumns = ['line %', 'branch %', 'funcs %'];
295+
const kColumnsKeys = ['coveredLinePercent', 'coveredBranchPercent', 'coveredFunctionPercent'];
296+
const kSeparator = ' | ';
297+
298+
function getCoverageReport(pad, summary, symbol, color, table) {
299+
const prefix = `${pad}${symbol}`;
300+
let report = `${color}${prefix}start of coverage report\n`;
301+
302+
let filePadLength;
303+
let columnPadLengths = [];
304+
let uncoveredLinesPadLength;
305+
let tableWidth;
306+
307+
if (table) {
308+
// Get expected column sizes
309+
filePadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) =>
310+
MathMax(acc, relative(summary.workingDirectory, file.path).length), 0);
311+
filePadLength = MathMax(filePadLength, 'file'.length);
312+
const fileWidth = filePadLength + 2;
313+
314+
columnPadLengths = ArrayPrototypeMap(kColumns, (column) => (table ? MathMax(column.length, 6) : 0));
315+
const columnsWidth = ArrayPrototypeReduce(columnPadLengths, (acc, columnPadLength) => acc + columnPadLength + 3, 0);
316+
317+
uncoveredLinesPadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) =>
318+
MathMax(acc, formatUncoveredLines(file.uncoveredLineNumbers, table).length), 0);
319+
uncoveredLinesPadLength = MathMax(uncoveredLinesPadLength, 'uncovered lines'.length);
320+
const uncoveredLinesWidth = uncoveredLinesPadLength + 2;
321+
322+
tableWidth = fileWidth + columnsWidth + uncoveredLinesWidth;
323+
324+
// Fit with sensible defaults
325+
const availableWidth = (process.stdout.columns || 9000) - prefix.length;
326+
const columnsExtras = tableWidth - availableWidth;
327+
if (table && columnsExtras > 0) {
328+
// Ensure file name is sufficiently visible
329+
const minFilePad = MathMin(8, filePadLength);
330+
filePadLength -= MathFloor(columnsExtras * 0.2);
331+
filePadLength = MathMax(filePadLength, minFilePad);
332+
333+
// Get rest of available space, subtracting margins
334+
uncoveredLinesPadLength = MathMax(availableWidth - columnsWidth - (filePadLength + 2) - 2, 1);
335+
336+
// Update table width
337+
tableWidth = availableWidth;
338+
} else {
339+
uncoveredLinesPadLength = Infinity;
340+
}
341+
}
342+
343+
344+
function getCell(string, width, { pad, truncate, coverage }) {
345+
if (!table) return string;
346+
347+
let result = string;
348+
if (pad) result = pad(result, width);
349+
if (truncate) result = truncate(result, width);
350+
if (color && coverage !== undefined) {
351+
if (coverage > 90) return `${coverageColors.high}${result}${color}`;
352+
if (coverage > 50) return `${coverageColors.medium}${result}${color}`;
353+
return `${coverageColors.low}${result}${color}`;
354+
}
355+
return result;
356+
}
264357

265-
report += `${pad}${symbol}file | line % | branch % | funcs % | uncovered lines\n`;
358+
// Head
359+
if (table) report += addTableLine(prefix, tableWidth);
360+
report += `${prefix}${getCell('file', filePadLength, { pad: StringPrototypePadEnd, truncate: truncateEnd })}${kSeparator}` +
361+
`${ArrayPrototypeJoin(ArrayPrototypeMap(kColumns, (column, i) => getCell(column, columnPadLengths[i], { pad: StringPrototypePadStart })), kSeparator)}${kSeparator}` +
362+
`${getCell('uncovered lines', uncoveredLinesPadLength, { truncate: truncateEnd })}\n`;
363+
if (table) report += addTableLine(prefix, tableWidth);
266364

365+
// Body
267366
for (let i = 0; i < summary.files.length; ++i) {
268-
const {
269-
path,
270-
coveredLinePercent,
271-
coveredBranchPercent,
272-
coveredFunctionPercent,
273-
uncoveredLineNumbers,
274-
} = summary.files[i];
275-
const relativePath = relative(summary.workingDirectory, path);
276-
const lines = coverageThreshold(coveredLinePercent, color);
277-
const branches = coverageThreshold(coveredBranchPercent, color);
278-
const functions = coverageThreshold(coveredFunctionPercent, color);
279-
const uncovered = ArrayPrototypeJoin(uncoveredLineNumbers, ', ');
280-
281-
report += `${pad}${symbol}${relativePath} | ${lines} | ${branches} | ` +
282-
`${functions} | ${uncovered}\n`;
367+
const file = summary.files[i];
368+
const relativePath = relative(summary.workingDirectory, file.path);
369+
370+
let fileCoverage = 0;
371+
const coverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => {
372+
const percent = file[columnKey];
373+
fileCoverage += percent;
374+
return percent;
375+
});
376+
fileCoverage /= kColumnsKeys.length;
377+
378+
report += `${prefix}${getCell(relativePath, filePadLength, { pad: StringPrototypePadEnd, truncate: truncateStart, coverage: fileCoverage })}${kSeparator}` +
379+
`${ArrayPrototypeJoin(ArrayPrototypeMap(coverages, (coverage, j) => getCell(NumberPrototypeToFixed(coverage, 2), columnPadLengths[j], { coverage, pad: StringPrototypePadStart })), kSeparator)}${kSeparator}` +
380+
`${getCell(formatUncoveredLines(file.uncoveredLineNumbers, table), uncoveredLinesPadLength, { truncate: truncateEnd })}\n`;
283381
}
284382

285-
const { totals } = summary;
286-
report += `${pad}${symbol}all files | ` +
287-
`${coverageThreshold(totals.coveredLinePercent, color)} | ` +
288-
`${coverageThreshold(totals.coveredBranchPercent, color)} | ` +
289-
`${coverageThreshold(totals.coveredFunctionPercent, color)} |\n`;
383+
// Foot
384+
if (table) report += addTableLine(prefix, tableWidth);
385+
report += `${prefix}${getCell('all files', filePadLength, { pad: StringPrototypePadEnd, truncate: truncateEnd })}${kSeparator}` +
386+
`${ArrayPrototypeJoin(ArrayPrototypeMap(kColumnsKeys, (columnKey, j) => getCell(NumberPrototypeToFixed(summary.totals[columnKey], 2), columnPadLengths[j], { coverage: summary.totals[columnKey], pad: StringPrototypePadStart })), kSeparator)} |\n`;
387+
if (table) report += addTableLine(prefix, tableWidth);
290388

291-
report += `${pad}${symbol}end of coverage report\n`;
389+
report += `${prefix}end of coverage report\n`;
292390
if (color) {
293391
report += white;
294392
}

test/parallel/test-runner-coverage.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,21 @@ function getTapCoverageFixtureReport() {
4141
}
4242

4343
function getSpecCoverageFixtureReport() {
44+
/* eslint-disable max-len */
4445
const report = [
4546
'\u2139 start of coverage report',
46-
'\u2139 file | line % | branch % | funcs % | uncovered lines',
47-
'\u2139 test/fixtures/test-runner/coverage.js | 78.65 | 38.46 | 60.00 | 12, ' +
48-
'13, 16, 17, 18, 19, 20, 21, 22, 27, 39, 43, 44, 61, 62, 66, 67, 71, 72',
49-
'\u2139 test/fixtures/test-runner/invalid-tap.js | 100.00 | 100.00 | 100.00 | ',
50-
'\u2139 test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5, 6',
51-
'\u2139 all files | 78.35 | 43.75 | 60.00 |',
47+
'\u2139 -------------------------------------------------------------------------------------------------------------------',
48+
'\u2139 file | line % | branch % | funcs % | uncovered lines',
49+
'\u2139 -------------------------------------------------------------------------------------------------------------------',
50+
'\u2139 test/fixtures/test-runner/coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72',
51+
'\u2139 test/fixtures/test-runner/invalid-tap.js | 100.00 | 100.00 | 100.00 | ',
52+
'\u2139 test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5-6',
53+
'\u2139 -------------------------------------------------------------------------------------------------------------------',
54+
'\u2139 all files | 78.35 | 43.75 | 60.00 |',
55+
'\u2139 -------------------------------------------------------------------------------------------------------------------',
5256
'\u2139 end of coverage report',
5357
].join('\n');
58+
/* eslint-enable max-len */
5459

5560
if (common.isWindows) {
5661
return report.replaceAll('/', '\\');

0 commit comments

Comments
 (0)