Skip to content

Commit 23c7f65

Browse files
authoredJun 7, 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 <[email protected]>
1 parent 72ba099 commit 23c7f65

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,20 +3,28 @@ 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,
18+
StringPrototypeRepeat,
19+
StringPrototypeSlice,
1220
} = primordials;
1321

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

2129
const {
2230
codes: {
@@ -27,6 +35,13 @@ const {
2735
} = require('internal/errors');
2836
const { compose } = require('stream');
2937

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

258273

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}`;
274+
const memo = new SafeMap();
275+
function addTableLine(prefix, width) {
276+
const key = `${prefix}-${width}`;
277+
let value = memo.get(key);
278+
if (value === undefined) {
279+
value = `${prefix}${StringPrototypeRepeat('-', width)}\n`;
280+
memo.set(key, value);
264281
}
265-
return coverage;
282+
283+
return value;
284+
}
285+
286+
const kHorizontalEllipsis = '\u2026';
287+
function truncateStart(string, width) {
288+
return string.length > width ? `${kHorizontalEllipsis}${StringPrototypeSlice(string, string.length - width + 1)}` : string;
289+
}
290+
291+
function truncateEnd(string, width) {
292+
return string.length > width ? `${StringPrototypeSlice(string, 0, width - 1)}${kHorizontalEllipsis}` : string;
293+
}
294+
295+
function formatLinesToRanges(values) {
296+
return ArrayPrototypeMap(ArrayPrototypeReduce(values, (prev, current, index, array) => {
297+
if ((index > 0) && ((current - array[index - 1]) === 1)) {
298+
prev[prev.length - 1][1] = current;
299+
} else {
300+
prev.push([current]);
301+
}
302+
return prev;
303+
}, []), (range) => ArrayPrototypeJoin(range, '-'));
304+
}
305+
306+
function formatUncoveredLines(lines, table) {
307+
if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' ');
308+
return ArrayPrototypeJoin(lines, ', ');
266309
}
267310

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

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

382+
// Body
273383
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`;
384+
const file = summary.files[i];
385+
const relativePath = relative(summary.workingDirectory, file.path);
386+
387+
let fileCoverage = 0;
388+
const coverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => {
389+
const percent = file[columnKey];
390+
fileCoverage += percent;
391+
return percent;
392+
});
393+
fileCoverage /= kColumnsKeys.length;
394+
395+
report += `${prefix}${getCell(relativePath, filePadLength, StringPrototypePadEnd, truncateStart, fileCoverage)}${kSeparator}` +
396+
`${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`;
289398
}
290399

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`;
400+
// Foot
401+
if (table) report += addTableLine(prefix, tableWidth);
402+
report += `${prefix}${getCell('all files', filePadLength, StringPrototypePadEnd, truncateEnd)}${kSeparator}` +
403+
`${ArrayPrototypeJoin(ArrayPrototypeMap(kColumnsKeys, (columnKey, j) => getCell(NumberPrototypeToFixed(summary.totals[columnKey], 2), columnPadLengths[j], StringPrototypePadStart, false, summary.totals[columnKey])), kSeparator)} |\n`;
404+
if (table) report += addTableLine(prefix, tableWidth);
296405

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

‎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.