Skip to content

Commit d177485

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 1b4ce69 commit d177485

3 files changed

Lines changed: 141 additions & 38 deletions

File tree

lib/internal/test_runner/reporter/spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class SpecReporter extends Transform {
123123
case 'test:diagnostic':
124124
return `${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`;
125125
case 'test:coverage':
126-
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue);
126+
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue, true);
127127
}
128128
}
129129
_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$/;
@@ -256,45 +269,130 @@ function countCompletedTest(test, harness = test.root.harness) {
256269
}
257270

258271

259-
function coverageThreshold(coverage, color) {
260-
coverage = NumberPrototypeToFixed(coverage, 2);
261-
if (color) {
262-
if (coverage > 90) return `${green}${coverage}${color}`;
263-
if (coverage < 50) return `${red}${coverage}${color}`;
264-
}
265-
return coverage;
272+
function addTableLine(prefix, width) {
273+
return `${prefix}${'-'.repeat(width)}\n`;
274+
}
275+
276+
function truncateStart(string, width) {
277+
return string.length > width ? `\u2026${string.substring(string.length - width + 1, string.length)}` : string;
278+
}
279+
280+
function truncateEnd(string, width) {
281+
return string.length > width ? `${string.substring(0, width - 1)}\u2026` : string;
282+
}
283+
284+
function formatLinesToRanges(values) {
285+
return ArrayPrototypeMap(ArrayPrototypeReduce(values, (prev, current, index, array) => {
286+
if ((index > 0) && ((current - array[index - 1]) === 1)) {
287+
prev[prev.length - 1][1] = current;
288+
} else {
289+
prev.push([current]);
290+
}
291+
return prev;
292+
}, []), (range) => range.join('-'));
293+
}
294+
295+
function formatUncoveredLines(lines, table) {
296+
if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' ');
297+
return ArrayPrototypeJoin(lines, ', ');
266298
}
267299

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

271-
report += `${pad}${symbol}file | line % | branch % | funcs % | uncovered lines\n`;
364+
// Head
365+
if (table) report += addTableLine(prefix, tableWidth);
366+
report += `${prefix}${getCell('file', filePadLength, { pad: StringPrototypePadEnd, truncate: truncateEnd })}${kSeparator}` +
367+
`${ArrayPrototypeJoin(ArrayPrototypeMap(kColumns, (column, i) => getCell(column, columnPadLengths[i], { pad: StringPrototypePadStart })), kSeparator)}${kSeparator}` +
368+
`${getCell('uncovered lines', uncoveredLinesPadLength, { truncate: truncateEnd })}\n`;
369+
if (table) report += addTableLine(prefix, tableWidth);
272370

371+
// Body
273372
for (let i = 0; i < summary.files.length; ++i) {
274-
const {
275-
path,
276-
coveredLinePercent,
277-
coveredBranchPercent,
278-
coveredFunctionPercent,
279-
uncoveredLineNumbers,
280-
} = summary.files[i];
281-
const relativePath = relative(summary.workingDirectory, path);
282-
const lines = coverageThreshold(coveredLinePercent, color);
283-
const branches = coverageThreshold(coveredBranchPercent, color);
284-
const functions = coverageThreshold(coveredFunctionPercent, color);
285-
const uncovered = ArrayPrototypeJoin(uncoveredLineNumbers, ', ');
286-
287-
report += `${pad}${symbol}${relativePath} | ${lines} | ${branches} | ` +
288-
`${functions} | ${uncovered}\n`;
373+
const file = summary.files[i];
374+
const relativePath = relative(summary.workingDirectory, file.path);
375+
376+
let fileCoverage = 0;
377+
const coverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => {
378+
const percent = file[columnKey];
379+
fileCoverage += percent;
380+
return percent;
381+
});
382+
fileCoverage /= kColumnsKeys.length;
383+
384+
report += `${prefix}${getCell(relativePath, filePadLength, { pad: StringPrototypePadEnd, truncate: truncateStart, coverage: fileCoverage })}${kSeparator}` +
385+
`${ArrayPrototypeJoin(ArrayPrototypeMap(coverages, (coverage, j) => getCell(NumberPrototypeToFixed(coverage, 2), columnPadLengths[j], { coverage, pad: StringPrototypePadStart })), kSeparator)}${kSeparator}` +
386+
`${getCell(formatUncoveredLines(file.uncoveredLineNumbers, table), uncoveredLinesPadLength, { truncate: truncateEnd })}\n`;
289387
}
290388

291-
const { totals } = summary;
292-
report += `${pad}${symbol}all files | ` +
293-
`${coverageThreshold(totals.coveredLinePercent, color)} | ` +
294-
`${coverageThreshold(totals.coveredBranchPercent, color)} | ` +
295-
`${coverageThreshold(totals.coveredFunctionPercent, color)} |\n`;
389+
// Foot
390+
if (table) report += addTableLine(prefix, tableWidth);
391+
report += `${prefix}${getCell('all files', filePadLength, { pad: StringPrototypePadEnd, truncate: truncateEnd })}${kSeparator}` +
392+
`${ArrayPrototypeJoin(ArrayPrototypeMap(kColumnsKeys, (columnKey, j) => getCell(NumberPrototypeToFixed(summary.totals[columnKey], 2), columnPadLengths[j], { coverage: summary.totals[columnKey], pad: StringPrototypePadStart })), kSeparator)} |\n`;
393+
if (table) report += addTableLine(prefix, tableWidth);
296394

297-
report += `${pad}${symbol}end of coverage report\n`;
395+
report += `${prefix}end of coverage report\n`;
298396
if (color) {
299397
report += white;
300398
}

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)