Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test_runner: refactor coverage report output for readability #47791

Merged
merged 11 commits into from
Jun 7, 2023
2 changes: 1 addition & 1 deletion lib/internal/test_runner/reporter/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class SpecReporter extends Transform {
case 'test:diagnostic':
return `${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`;
case 'test:coverage':
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue);
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue, true);
}
}
_transform({ type, data }, encoding, callback) {
Expand Down
171 changes: 140 additions & 31 deletions lib/internal/test_runner/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@ const {
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeReduce,
ObjectGetOwnPropertyDescriptor,
MathFloor,
MathMax,
MathMin,
NumberPrototypeToFixed,
SafePromiseAllReturnArrayLike,
RegExp,
RegExpPrototypeExec,
SafeMap,
StringPrototypePadStart,
StringPrototypePadEnd,
StringPrototypeRepeat,
StringPrototypeSlice,
} = primordials;

const { basename, relative } = require('path');
const { createWriteStream } = require('fs');
const { pathToFileURL } = require('internal/url');
const { createDeferredPromise } = require('internal/util');
const { getOptionValue } = require('internal/options');
const { green, red, white, shouldColorize } = require('internal/util/colors');
const { green, yellow, red, white, shouldColorize } = require('internal/util/colors');

const {
codes: {
Expand All @@ -27,6 +35,13 @@ const {
} = require('internal/errors');
const { compose } = require('stream');

const coverageColors = {
__proto__: null,
high: green,
medium: yellow,
low: red,
};

const kMultipleCallbackInvocations = 'multipleCallbackInvocations';
const kRegExpPattern = /^\/(.*)\/([a-z]*)$/;
const kSupportedFileExtensions = /\.[cm]?js$/;
Expand Down Expand Up @@ -256,45 +271,139 @@ function countCompletedTest(test, harness = test.root.harness) {
}


function coverageThreshold(coverage, color) {
coverage = NumberPrototypeToFixed(coverage, 2);
if (color) {
if (coverage > 90) return `${green}${coverage}${color}`;
if (coverage < 50) return `${red}${coverage}${color}`;
const memo = new SafeMap();
function addTableLine(prefix, width) {
const key = `${prefix}-${width}`;
let value = memo.get(key);
if (value === undefined) {
value = `${prefix}${StringPrototypeRepeat('-', width)}\n`;
memo.set(key, value);
}
return coverage;

return value;
}

const kHorizontalEllipsis = '\u2026';
function truncateStart(string, width) {
return string.length > width ? `${kHorizontalEllipsis}${StringPrototypeSlice(string, string.length - width + 1)}` : string;
}

function truncateEnd(string, width) {
return string.length > width ? `${StringPrototypeSlice(string, 0, width - 1)}${kHorizontalEllipsis}` : string;
}

function formatLinesToRanges(values) {
return ArrayPrototypeMap(ArrayPrototypeReduce(values, (prev, current, index, array) => {
if ((index > 0) && ((current - array[index - 1]) === 1)) {
prev[prev.length - 1][1] = current;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use .at(-1)?

} else {
prev.push([current]);
}
return prev;
}, []), (range) => range.join('-'));
MoLow marked this conversation as resolved.
Show resolved Hide resolved
}

function formatUncoveredLines(lines, table) {
if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' ');
return ArrayPrototypeJoin(lines, ', ');
}

function getCoverageReport(pad, summary, symbol, color) {
let report = `${color}${pad}${symbol}start of coverage report\n`;
const kColumns = ['line %', 'branch %', 'funcs %'];
const kColumnsKeys = ['coveredLinePercent', 'coveredBranchPercent', 'coveredFunctionPercent'];
const kSeparator = ' | ';

function getCoverageReport(pad, summary, symbol, color, table) {
const prefix = `${pad}${symbol}`;
let report = `${color}${prefix}start of coverage report\n`;

let filePadLength;
let columnPadLengths = [];
let uncoveredLinesPadLength;
let tableWidth;

if (table) {
// Get expected column sizes
filePadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) =>
MathMax(acc, relative(summary.workingDirectory, file.path).length), 0);
filePadLength = MathMax(filePadLength, 'file'.length);
const fileWidth = filePadLength + 2;

columnPadLengths = ArrayPrototypeMap(kColumns, (column) => (table ? MathMax(column.length, 6) : 0));
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);
uncoveredLinesPadLength = MathMax(uncoveredLinesPadLength, 'uncovered lines'.length);
const uncoveredLinesWidth = uncoveredLinesPadLength + 2;

tableWidth = fileWidth + columnsWidth + uncoveredLinesWidth;

// Fit with sensible defaults
const availableWidth = (process.stdout.columns || Infinity) - prefix.length;
const columnsExtras = tableWidth - availableWidth;
if (table && columnsExtras > 0) {
// Ensure file name is sufficiently visible
const minFilePad = MathMin(8, filePadLength);
filePadLength -= MathFloor(columnsExtras * 0.2);
filePadLength = MathMax(filePadLength, minFilePad);

// Get rest of available space, subtracting margins
uncoveredLinesPadLength = MathMax(availableWidth - columnsWidth - (filePadLength + 2) - 2, 1);

// Update table width
tableWidth = availableWidth;
} else {
uncoveredLinesPadLength = Infinity;
}
}


function getCell(string, width, pad, truncate, coverage) {
if (!table) return string;

let result = string;
if (pad) result = pad(result, width);
if (truncate) result = truncate(result, width);
if (color && coverage !== undefined) {
if (coverage > 90) return `${coverageColors.high}${result}${color}`;
if (coverage > 50) return `${coverageColors.medium}${result}${color}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if these values should be configurable someway...

return `${coverageColors.low}${result}${color}`;
}
return result;
}

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

// Body
for (let i = 0; i < summary.files.length; ++i) {
const {
path,
coveredLinePercent,
coveredBranchPercent,
coveredFunctionPercent,
uncoveredLineNumbers,
} = summary.files[i];
const relativePath = relative(summary.workingDirectory, path);
const lines = coverageThreshold(coveredLinePercent, color);
const branches = coverageThreshold(coveredBranchPercent, color);
const functions = coverageThreshold(coveredFunctionPercent, color);
const uncovered = ArrayPrototypeJoin(uncoveredLineNumbers, ', ');

report += `${pad}${symbol}${relativePath} | ${lines} | ${branches} | ` +
`${functions} | ${uncovered}\n`;
const file = summary.files[i];
const relativePath = relative(summary.workingDirectory, file.path);

let fileCoverage = 0;
const coverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => {
const percent = file[columnKey];
fileCoverage += percent;
return percent;
});
fileCoverage /= kColumnsKeys.length;

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`;
}

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

report += `${pad}${symbol}end of coverage report\n`;
report += `${prefix}end of coverage report\n`;
if (color) {
report += white;
}
Expand Down
1 change: 1 addition & 0 deletions lib/internal/util/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module.exports = {
module.exports.blue = hasColors ? '\u001b[34m' : '';
module.exports.green = hasColors ? '\u001b[32m' : '';
module.exports.white = hasColors ? '\u001b[39m' : '';
module.exports.yellow = hasColors ? '\u001b[33m' : '';
module.exports.red = hasColors ? '\u001b[31m' : '';
module.exports.gray = hasColors ? '\u001b[90m' : '';
module.exports.clear = hasColors ? '\u001bc' : '';
Expand Down
17 changes: 11 additions & 6 deletions test/parallel/test-runner-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,21 @@ function getTapCoverageFixtureReport() {
}

function getSpecCoverageFixtureReport() {
/* eslint-disable max-len */
const report = [
'\u2139 start of coverage report',
'\u2139 file | line % | branch % | funcs % | uncovered lines',
'\u2139 test/fixtures/test-runner/coverage.js | 78.65 | 38.46 | 60.00 | 12, ' +
'13, 16, 17, 18, 19, 20, 21, 22, 27, 39, 43, 44, 61, 62, 66, 67, 71, 72',
'\u2139 test/fixtures/test-runner/invalid-tap.js | 100.00 | 100.00 | 100.00 | ',
'\u2139 test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5, 6',
'\u2139 all files | 78.35 | 43.75 | 60.00 |',
'\u2139 -------------------------------------------------------------------------------------------------------------------',
'\u2139 file | line % | branch % | funcs % | uncovered lines',
'\u2139 -------------------------------------------------------------------------------------------------------------------',
'\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',
'\u2139 test/fixtures/test-runner/invalid-tap.js | 100.00 | 100.00 | 100.00 | ',
'\u2139 test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5-6',
'\u2139 -------------------------------------------------------------------------------------------------------------------',
'\u2139 all files | 78.35 | 43.75 | 60.00 |',
'\u2139 -------------------------------------------------------------------------------------------------------------------',
'\u2139 end of coverage report',
].join('\n');
/* eslint-enable max-len */

if (common.isWindows) {
return report.replaceAll('/', '\\');
Expand Down