Skip to content

Commit 17cf7f6

Browse files
committed
test_runner: add code coverage support to spec reporter
1 parent 7f2ab4e commit 17cf7f6

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

lib/internal/test_runner/reporter/spec.js

+38
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {
66
ArrayPrototypeShift,
77
ArrayPrototypeUnshift,
88
hardenRegExp,
9+
NumberPrototypeToFixed,
910
RegExpPrototypeSymbolSplit,
1011
SafeMap,
1112
StringPrototypeRepeat,
@@ -14,6 +15,7 @@ const assert = require('assert');
1415
const Transform = require('internal/streams/transform');
1516
const { inspectWithNoCustomRetry } = require('internal/errors');
1617
const { green, blue, red, white, gray } = require('internal/util/colors');
18+
const { relative } = require('path');
1719

1820

1921
const inspectOptions = { __proto__: null, colors: true, breakLength: Infinity };
@@ -29,6 +31,7 @@ const symbols = {
2931
'test:fail': '\u2716 ',
3032
'test:pass': '\u2714 ',
3133
'test:diagnostic': '\u2139 ',
34+
'test:coverage': '\u2139 ',
3235
'arrow:right': '\u25B6 ',
3336
'hyphen:minus': '\uFE63 ',
3437
};
@@ -60,6 +63,39 @@ class SpecReporter extends Transform {
6063
), `\n${indent} `);
6164
return `\n${indent} ${message}\n`;
6265
}
66+
#coverageThresholdColor(coverage, color = blue) {
67+
coverage = NumberPrototypeToFixed(coverage, 2);
68+
if (coverage > 90) return `${green}${coverage}${color}`;
69+
if (coverage < 50) return `${red}${coverage}${color}`;
70+
return coverage;
71+
}
72+
#reportCoverage(nesting, symbol, color, summary) {
73+
const indent = this.#indent(nesting);
74+
let report = `${color}${indent}${symbol}========= coverage report =========\n`;
75+
report += `${indent}${symbol}file | line % | branch % | funcs % | uncovered lines\n`;
76+
for (let i = 0; i < summary.files.length; ++i) {
77+
const {
78+
path,
79+
coveredLinePercent,
80+
coveredBranchPercent,
81+
coveredFunctionPercent,
82+
uncoveredLineNumbers,
83+
} = summary.files[i];
84+
const relativePath = relative(summary.workingDirectory, path);
85+
const uncovered = ArrayPrototypeJoin(uncoveredLineNumbers, ', ');
86+
report += `${indent}${symbol}${relativePath} | ${this.#coverageThresholdColor(coveredLinePercent)} | ${this.#coverageThresholdColor(coveredBranchPercent)} | ` +
87+
`${this.#coverageThresholdColor(coveredFunctionPercent)} | ${gray}${uncovered}${color}\n`;
88+
}
89+
const {
90+
coveredLinePercent,
91+
coveredBranchPercent,
92+
coveredFunctionPercent,
93+
} = summary.totals;
94+
report += `${indent}${symbol}all files | ${this.#coverageThresholdColor(coveredLinePercent)} | ${this.#coverageThresholdColor(coveredBranchPercent)} | ` +
95+
`${this.#coverageThresholdColor(coveredFunctionPercent)} |\n`;
96+
report += `${symbol}${indent}==================================== ${white}`
97+
return report;
98+
}
6399
#handleEvent({ type, data }) {
64100
let color = colors[type] ?? white;
65101
let symbol = symbols[type] ?? ' ';
@@ -103,6 +139,8 @@ class SpecReporter extends Transform {
103139
break;
104140
case 'test:diagnostic':
105141
return `${color}${this.#indent(data.nesting)}${symbol}${data.message}${white}\n`;
142+
case 'test:coverage':
143+
return this.#reportCoverage(data.nesting, symbols['test:coverage'], blue, data.summary);
106144
}
107145
}
108146
_transform({ type, data }, encoding, callback) {
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('node:assert');
4+
const { spawnSync } = require('node:child_process');
5+
const { readdirSync } = require('node:fs');
6+
const { test } = require('node:test');
7+
const fixtures = require('../common/fixtures');
8+
const tmpdir = require('../common/tmpdir');
9+
10+
tmpdir.refresh();
11+
12+
function findCoverageFileForPid(pid) {
13+
const pattern = `^coverage\\-${pid}\\-(\\d{13})\\-(\\d+)\\.json$`;
14+
const regex = new RegExp(pattern);
15+
16+
return readdirSync(tmpdir.path).find((file) => {
17+
return regex.test(file);
18+
});
19+
}
20+
21+
function getCoverageFixtureReport() {
22+
const report = [
23+
'\u2139 ========= coverage report =========',
24+
'\u2139 file | line % | branch % | funcs % | uncovered lines',
25+
'\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',
26+
'\u2139 test/fixtures/test-runner/invalid-tap.js | 100.00 | 100.00 | 100.00 | ',
27+
'\u2139 test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5, 6',
28+
'\u2139 all files | 78.35 | 43.75 | 60.00 |',
29+
'\u2139 ===================================='
30+
].join('\n');
31+
32+
if (common.isWindows) {
33+
return report.replaceAll('/', '\\');
34+
}
35+
36+
return report;
37+
}
38+
39+
test('coverage is reported and dumped to NODE_V8_COVERAGE if present', (t) => {
40+
if (!process.features.inspector) {
41+
return;
42+
}
43+
44+
const fixture = fixtures.path('test-runner', 'coverage.js');
45+
const args = ['--experimental-test-coverage', '--test-reporter', 'spec', fixture];
46+
const options = { env: { ...process.env, NODE_V8_COVERAGE: tmpdir.path } };
47+
const result = spawnSync(process.execPath, args, options);
48+
const report = getCoverageFixtureReport();
49+
50+
assert(result.stdout.toString().includes(report));
51+
assert.strictEqual(result.stderr.toString(), '');
52+
assert.strictEqual(result.status, 0);
53+
assert(findCoverageFileForPid(result.pid));
54+
});
55+
56+
test('coverage is reported without NODE_V8_COVERAGE present', (t) => {
57+
if (!process.features.inspector) {
58+
return;
59+
}
60+
61+
const fixture = fixtures.path('test-runner', 'coverage.js');
62+
const args = ['--experimental-test-coverage', '--test-reporter', 'spec', fixture];
63+
const result = spawnSync(process.execPath, args);
64+
const report = getCoverageFixtureReport();
65+
66+
assert(result.stdout.toString().includes(report));
67+
assert.strictEqual(result.stderr.toString(), '');
68+
assert.strictEqual(result.status, 0);
69+
assert(!findCoverageFileForPid(result.pid));
70+
});

0 commit comments

Comments
 (0)