Skip to content

Commit f302286

Browse files
dmnsgnruyadorno
authored andcommittedAug 29, 2023
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: #46674 PR-URL: #47791 Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
1 parent bf1525c commit f302286

File tree

4 files changed

+153
-38
lines changed

4 files changed

+153
-38
lines changed
 

‎lib/internal/test_runner/reporter/spec.js

+1-1
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

+140-31
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,29 @@ const {
33
ArrayPrototypeJoin,
44
ArrayPrototypeMap,
55
ArrayPrototypePush,
6+
ArrayPrototypeReduce,
67
ObjectCreate,
78
ObjectGetOwnPropertyDescriptor,
9+
MathFloor,
10+
MathMax,
11+
MathMin,
812
NumberPrototypeToFixed,
913
SafePromiseAllReturnArrayLike,
1014
RegExp,
1115
RegExpPrototypeExec,
1216
SafeMap,
17+
StringPrototypePadStart,
18+
StringPrototypePadEnd,
19+
StringPrototypeRepeat,
20+
StringPrototypeSlice,
1321
} = primordials;
1422

1523
const { basename, relative } = require('path');
1624
const { createWriteStream } = require('fs');
1725
const { pathToFileURL } = require('internal/url');
1826
const { createDeferredPromise } = require('internal/util');
1927
const { getOptionValue } = require('internal/options');
20-
const { green, red, white, shouldColorize } = require('internal/util/colors');
28+
const { green, yellow, red, white, shouldColorize } = require('internal/util/colors');
2129

2230
const {
2331
codes: {
@@ -28,6 +36,13 @@ const {
2836
} = require('internal/errors');
2937
const { compose } = require('stream');
3038

39+
const coverageColors = {
40+
__proto__: null,
41+
high: green,
42+
medium: yellow,
43+
low: red,
44+
};
45+
3146
const kMultipleCallbackInvocations = 'multipleCallbackInvocations';
3247
const kRegExpPattern = /^\/(.*)\/([a-z]*)$/;
3348
const kSupportedFileExtensions = /\.[cm]?js$/;
@@ -257,45 +272,139 @@ function countCompletedTest(test, harness = test.root.harness) {
257272
}
258273

259274

260-
function coverageThreshold(coverage, color) {
261-
coverage = NumberPrototypeToFixed(coverage, 2);
262-
if (color) {
263-
if (coverage > 90) return `${green}${coverage}${color}`;
264-
if (coverage < 50) return `${red}${coverage}${color}`;
275+
const memo = new SafeMap();
276+
function addTableLine(prefix, width) {
277+
const key = `${prefix}-${width}`;
278+
let value = memo.get(key);
279+
if (value === undefined) {
280+
value = `${prefix}${StringPrototypeRepeat('-', width)}\n`;
281+
memo.set(key, value);
265282
}
266-
return coverage;
283+
284+
return value;
285+
}
286+
287+
const kHorizontalEllipsis = '\u2026';
288+
function truncateStart(string, width) {
289+
return string.length > width ? `${kHorizontalEllipsis}${StringPrototypeSlice(string, string.length - width + 1)}` : string;
290+
}
291+
292+
function truncateEnd(string, width) {
293+
return string.length > width ? `${StringPrototypeSlice(string, 0, width - 1)}${kHorizontalEllipsis}` : string;
294+
}
295+
296+
function formatLinesToRanges(values) {
297+
return ArrayPrototypeMap(ArrayPrototypeReduce(values, (prev, current, index, array) => {
298+
if ((index > 0) && ((current - array[index - 1]) === 1)) {
299+
prev[prev.length - 1][1] = current;
300+
} else {
301+
prev.push([current]);
302+
}
303+
return prev;
304+
}, []), (range) => ArrayPrototypeJoin(range, '-'));
305+
}
306+
307+
function formatUncoveredLines(lines, table) {
308+
if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' ');
309+
return ArrayPrototypeJoin(lines, ', ');
267310
}
268311

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

272-
report += `${pad}${symbol}file | line % | branch % | funcs % | uncovered lines\n`;
376+
// Head
377+
if (table) report += addTableLine(prefix, tableWidth);
378+
report += `${prefix}${getCell('file', filePadLength, StringPrototypePadEnd, truncateEnd)}${kSeparator}` +
379+
`${ArrayPrototypeJoin(ArrayPrototypeMap(kColumns, (column, i) => getCell(column, columnPadLengths[i], StringPrototypePadStart)), kSeparator)}${kSeparator}` +
380+
`${getCell('uncovered lines', uncoveredLinesPadLength, false, truncateEnd)}\n`;
381+
if (table) report += addTableLine(prefix, tableWidth);
273382

383+
// Body
274384
for (let i = 0; i < summary.files.length; ++i) {
275-
const {
276-
path,
277-
coveredLinePercent,
278-
coveredBranchPercent,
279-
coveredFunctionPercent,
280-
uncoveredLineNumbers,
281-
} = summary.files[i];
282-
const relativePath = relative(summary.workingDirectory, path);
283-
const lines = coverageThreshold(coveredLinePercent, color);
284-
const branches = coverageThreshold(coveredBranchPercent, color);
285-
const functions = coverageThreshold(coveredFunctionPercent, color);
286-
const uncovered = ArrayPrototypeJoin(uncoveredLineNumbers, ', ');
287-
288-
report += `${pad}${symbol}${relativePath} | ${lines} | ${branches} | ` +
289-
`${functions} | ${uncovered}\n`;
385+
const file = summary.files[i];
386+
const relativePath = relative(summary.workingDirectory, file.path);
387+
388+
let fileCoverage = 0;
389+
const coverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => {
390+
const percent = file[columnKey];
391+
fileCoverage += percent;
392+
return percent;
393+
});
394+
fileCoverage /= kColumnsKeys.length;
395+
396+
report += `${prefix}${getCell(relativePath, filePadLength, StringPrototypePadEnd, truncateStart, fileCoverage)}${kSeparator}` +
397+
`${ArrayPrototypeJoin(ArrayPrototypeMap(coverages, (coverage, j) => getCell(NumberPrototypeToFixed(coverage, 2), columnPadLengths[j], StringPrototypePadStart, false, coverage)), kSeparator)}${kSeparator}` +
398+
`${getCell(formatUncoveredLines(file.uncoveredLineNumbers, table), uncoveredLinesPadLength, false, truncateEnd)}\n`;
290399
}
291400

292-
const { totals } = summary;
293-
report += `${pad}${symbol}all files | ` +
294-
`${coverageThreshold(totals.coveredLinePercent, color)} | ` +
295-
`${coverageThreshold(totals.coveredBranchPercent, color)} | ` +
296-
`${coverageThreshold(totals.coveredFunctionPercent, color)} |\n`;
401+
// Foot
402+
if (table) report += addTableLine(prefix, tableWidth);
403+
report += `${prefix}${getCell('all files', filePadLength, StringPrototypePadEnd, truncateEnd)}${kSeparator}` +
404+
`${ArrayPrototypeJoin(ArrayPrototypeMap(kColumnsKeys, (columnKey, j) => getCell(NumberPrototypeToFixed(summary.totals[columnKey], 2), columnPadLengths[j], StringPrototypePadStart, false, summary.totals[columnKey])), kSeparator)} |\n`;
405+
if (table) report += addTableLine(prefix, tableWidth);
297406

298-
report += `${pad}${symbol}end of coverage report\n`;
407+
report += `${prefix}end of coverage report\n`;
299408
if (color) {
300409
report += white;
301410
}

‎lib/internal/util/colors.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module.exports = {
2828
module.exports.blue = hasColors ? '\u001b[34m' : '';
2929
module.exports.green = hasColors ? '\u001b[32m' : '';
3030
module.exports.white = hasColors ? '\u001b[39m' : '';
31+
module.exports.yellow = hasColors ? '\u001b[33m' : '';
3132
module.exports.red = hasColors ? '\u001b[31m' : '';
3233
module.exports.gray = hasColors ? '\u001b[90m' : '';
3334
module.exports.clear = hasColors ? '\u001bc' : '';

‎test/parallel/test-runner-coverage.js

+11-6
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)
Please sign in to comment.