Skip to content

Commit 6d06586

Browse files
authored
test_runner: add code coverage support to spec reporter
PR-URL: #46674 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Moshe Atlow <[email protected]>
1 parent 85705a4 commit 6d06586

File tree

4 files changed

+163
-89
lines changed

4 files changed

+163
-89
lines changed

lib/internal/test_runner/reporter/spec.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const assert = require('assert');
1515
const Transform = require('internal/streams/transform');
1616
const { inspectWithNoCustomRetry } = require('internal/errors');
1717
const { green, blue, red, white, gray } = require('internal/util/colors');
18-
18+
const { getCoverageReport } = require('internal/test_runner/utils');
1919

2020
const inspectOptions = { __proto__: null, colors: true, breakLength: Infinity };
2121

@@ -30,6 +30,7 @@ const symbols = {
3030
'test:fail': '\u2716 ',
3131
'test:pass': '\u2714 ',
3232
'test:diagnostic': '\u2139 ',
33+
'test:coverage': '\u2139 ',
3334
'arrow:right': '\u25B6 ',
3435
'hyphen:minus': '\uFE63 ',
3536
};
@@ -115,6 +116,8 @@ class SpecReporter extends Transform {
115116
break;
116117
case 'test:diagnostic':
117118
return `${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`;
119+
case 'test:coverage':
120+
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue);
118121
}
119122
}
120123
_transform({ type, data }, encoding, callback) {

lib/internal/test_runner/reporter/tap.js

+2-37
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ const {
33
ArrayPrototypeForEach,
44
ArrayPrototypeJoin,
55
ArrayPrototypePush,
6-
NumberPrototypeToFixed,
76
ObjectEntries,
87
RegExpPrototypeSymbolReplace,
98
SafeMap,
@@ -13,7 +12,7 @@ const {
1312
} = primordials;
1413
const { inspectWithNoCustomRetry } = require('internal/errors');
1514
const { isError, kEmptyObject } = require('internal/util');
16-
const { relative } = require('path');
15+
const { getCoverageReport } = require('internal/test_runner/utils');
1716
const kDefaultIndent = ' '; // 4 spaces
1817
const kFrameStartRegExp = /^ {4}at /;
1918
const kLineBreakRegExp = /\n|\r\n/;
@@ -49,7 +48,7 @@ async function * tapReporter(source) {
4948
yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`;
5049
break;
5150
case 'test:coverage':
52-
yield reportCoverage(data.nesting, data.summary);
51+
yield getCoverageReport(indent(data.nesting), data.summary, '# ', '');
5352
break;
5453
}
5554
}
@@ -73,40 +72,6 @@ function reportTest(nesting, testNumber, status, name, skip, todo) {
7372
return line;
7473
}
7574

76-
function reportCoverage(nesting, summary) {
77-
const pad = indent(nesting);
78-
let report = `${pad}# start of coverage report\n`;
79-
80-
report += `${pad}# file | line % | branch % | funcs % | uncovered lines\n`;
81-
82-
for (let i = 0; i < summary.files.length; ++i) {
83-
const {
84-
path,
85-
coveredLinePercent,
86-
coveredBranchPercent,
87-
coveredFunctionPercent,
88-
uncoveredLineNumbers,
89-
} = summary.files[i];
90-
const relativePath = relative(summary.workingDirectory, path);
91-
const lines = NumberPrototypeToFixed(coveredLinePercent, 2);
92-
const branches = NumberPrototypeToFixed(coveredBranchPercent, 2);
93-
const functions = NumberPrototypeToFixed(coveredFunctionPercent, 2);
94-
const uncovered = ArrayPrototypeJoin(uncoveredLineNumbers, ', ');
95-
96-
report += `${pad}# ${relativePath} | ${lines} | ${branches} | ` +
97-
`${functions} | ${uncovered}\n`;
98-
}
99-
100-
const { totals } = summary;
101-
report += `${pad}# all files | ` +
102-
`${NumberPrototypeToFixed(totals.coveredLinePercent, 2)} | ` +
103-
`${NumberPrototypeToFixed(totals.coveredBranchPercent, 2)} | ` +
104-
`${NumberPrototypeToFixed(totals.coveredFunctionPercent, 2)} |\n`;
105-
106-
report += `${pad}# end of coverage report\n`;
107-
return report;
108-
}
109-
11075
function reportDetails(nesting, data = kEmptyObject) {
11176
const { error, duration_ms } = data;
11277
const _indent = indent(nesting);

lib/internal/test_runner/utils.js

+51-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
'use strict';
22
const {
3+
ArrayPrototypeJoin,
34
ArrayPrototypeMap,
45
ArrayPrototypePush,
56
ObjectGetOwnPropertyDescriptor,
7+
NumberPrototypeToFixed,
68
SafePromiseAllReturnArrayLike,
79
RegExp,
810
RegExpPrototypeExec,
911
SafeMap,
1012
} = primordials;
1113

12-
const { basename } = require('path');
14+
const { basename, relative } = require('path');
1315
const { createWriteStream } = require('fs');
1416
const { pathToFileURL } = require('internal/url');
1517
const { createDeferredPromise } = require('internal/util');
1618
const { getOptionValue } = require('internal/options');
19+
const { green, red, white } = require('internal/util/colors');
1720

1821
const {
1922
codes: {
@@ -246,6 +249,52 @@ function countCompletedTest(test, harness = test.root.harness) {
246249
harness.counters.all++;
247250
}
248251

252+
253+
function coverageThreshold(coverage, color) {
254+
coverage = NumberPrototypeToFixed(coverage, 2);
255+
if (color) {
256+
if (coverage > 90) return `${green}${coverage}${color}`;
257+
if (coverage < 50) return `${red}${coverage}${color}`;
258+
}
259+
return coverage;
260+
}
261+
262+
function getCoverageReport(pad, summary, symbol, color) {
263+
let report = `${color}${pad}${symbol}start of coverage report\n`;
264+
265+
report += `${pad}${symbol}file | line % | branch % | funcs % | uncovered lines\n`;
266+
267+
for (let i = 0; i < summary.files.length; ++i) {
268+
const {
269+
path,
270+
coveredLinePercent,
271+
coveredBranchPercent,
272+
coveredFunctionPercent,
273+
uncoveredLineNumbers,
274+
} = summary.files[i];
275+
const relativePath = relative(summary.workingDirectory, path);
276+
const lines = coverageThreshold(coveredLinePercent, color);
277+
const branches = coverageThreshold(coveredBranchPercent, color);
278+
const functions = coverageThreshold(coveredFunctionPercent, color);
279+
const uncovered = ArrayPrototypeJoin(uncoveredLineNumbers, ', ');
280+
281+
report += `${pad}${symbol}${relativePath} | ${lines} | ${branches} | ` +
282+
`${functions} | ${uncovered}\n`;
283+
}
284+
285+
const { totals } = summary;
286+
report += `${pad}${symbol}all files | ` +
287+
`${coverageThreshold(totals.coveredLinePercent, color)} | ` +
288+
`${coverageThreshold(totals.coveredBranchPercent, color)} | ` +
289+
`${coverageThreshold(totals.coveredFunctionPercent, color)} |\n`;
290+
291+
report += `${pad}${symbol}end of coverage report\n`;
292+
if (color) {
293+
report += white;
294+
}
295+
return report;
296+
}
297+
249298
module.exports = {
250299
convertStringToRegExp,
251300
countCompletedTest,
@@ -255,4 +304,5 @@ module.exports = {
255304
isTestFailureError,
256305
parseCommandLine,
257306
setupTestReporters,
307+
getCoverageReport,
258308
};

test/parallel/test-runner-coverage.js

+106-50
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function findCoverageFileForPid(pid) {
1818
});
1919
}
2020

21-
function getCoverageFixtureReport() {
21+
function getTapCoverageFixtureReport() {
2222
const report = [
2323
'# start of coverage report',
2424
'# file | line % | branch % | funcs % | uncovered lines',
@@ -37,64 +37,120 @@ function getCoverageFixtureReport() {
3737
return report;
3838
}
3939

40-
test('--experimental-test-coverage and --test cannot be combined', () => {
41-
// TODO(cjihrig): This test can be removed once multi-process code coverage
42-
// is supported.
43-
const args = ['--test', '--experimental-test-coverage'];
44-
const result = spawnSync(process.execPath, args);
45-
46-
// 9 is the documented exit code for an invalid CLI argument.
47-
assert.strictEqual(result.status, 9);
48-
assert.match(
49-
result.stderr.toString(),
50-
/--experimental-test-coverage cannot be used with --test/
51-
);
52-
});
40+
function getSpecCoverageFixtureReport() {
41+
const report = [
42+
'\u2139 start of coverage report',
43+
'\u2139 file | line % | branch % | funcs % | uncovered lines',
44+
'\u2139 test/fixtures/test-runner/coverage.js | 78.65 | 38.46 | 60.00 | 12, ' +
45+
'13, 16, 17, 18, 19, 20, 21, 22, 27, 39, 43, 44, 61, 62, 66, 67, 71, 72',
46+
'\u2139 test/fixtures/test-runner/invalid-tap.js | 100.00 | 100.00 | 100.00 | ',
47+
'\u2139 test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5, 6',
48+
'\u2139 all files | 78.35 | 43.75 | 60.00 |',
49+
'\u2139 end of coverage report',
50+
].join('\n');
5351

54-
test('handles the inspector not being available', (t) => {
55-
if (process.features.inspector) {
56-
return;
52+
if (common.isWindows) {
53+
return report.replaceAll('/', '\\');
5754
}
5855

59-
const fixture = fixtures.path('test-runner', 'coverage.js');
60-
const args = ['--experimental-test-coverage', fixture];
61-
const result = spawnSync(process.execPath, args);
56+
return report;
57+
}
6258

63-
assert(!result.stdout.toString().includes('# start of coverage report'));
64-
assert(result.stderr.toString().includes('coverage could not be collected'));
65-
assert.strictEqual(result.status, 0);
66-
assert(!findCoverageFileForPid(result.pid));
67-
});
59+
test('test coverage report', async (t) => {
60+
await t.test('--experimental-test-coverage and --test cannot be combined', () => {
61+
// TODO(cjihrig): This test can be removed once multi-process code coverage
62+
// is supported.
63+
const args = ['--test', '--experimental-test-coverage'];
64+
const result = spawnSync(process.execPath, args);
65+
66+
// 9 is the documented exit code for an invalid CLI argument.
67+
assert.strictEqual(result.status, 9);
68+
assert.match(
69+
result.stderr.toString(),
70+
/--experimental-test-coverage cannot be used with --test/
71+
);
72+
});
6873

69-
test('coverage is reported and dumped to NODE_V8_COVERAGE if present', (t) => {
70-
if (!process.features.inspector) {
71-
return;
72-
}
74+
await t.test('handles the inspector not being available', (t) => {
75+
if (process.features.inspector) {
76+
return;
77+
}
7378

74-
const fixture = fixtures.path('test-runner', 'coverage.js');
75-
const args = ['--experimental-test-coverage', fixture];
76-
const options = { env: { ...process.env, NODE_V8_COVERAGE: tmpdir.path } };
77-
const result = spawnSync(process.execPath, args, options);
78-
const report = getCoverageFixtureReport();
79+
const fixture = fixtures.path('test-runner', 'coverage.js');
80+
const args = ['--experimental-test-coverage', fixture];
81+
const result = spawnSync(process.execPath, args);
7982

80-
assert(result.stdout.toString().includes(report));
81-
assert.strictEqual(result.stderr.toString(), '');
82-
assert.strictEqual(result.status, 0);
83-
assert(findCoverageFileForPid(result.pid));
83+
assert(!result.stdout.toString().includes('# start of coverage report'));
84+
assert(result.stderr.toString().includes('coverage could not be collected'));
85+
assert.strictEqual(result.status, 0);
86+
assert(!findCoverageFileForPid(result.pid));
87+
});
8488
});
8589

86-
test('coverage is reported without NODE_V8_COVERAGE present', (t) => {
87-
if (!process.features.inspector) {
88-
return;
89-
}
90+
test('test tap coverage reporter', async (t) => {
91+
await t.test('coverage is reported and dumped to NODE_V8_COVERAGE if present', (t) => {
92+
if (!process.features.inspector) {
93+
return;
94+
}
95+
96+
const fixture = fixtures.path('test-runner', 'coverage.js');
97+
const args = ['--experimental-test-coverage', '--test-reporter', 'tap', fixture];
98+
const options = { env: { ...process.env, NODE_V8_COVERAGE: tmpdir.path } };
99+
const result = spawnSync(process.execPath, args, options);
100+
const report = getTapCoverageFixtureReport();
101+
102+
assert(result.stdout.toString().includes(report));
103+
assert.strictEqual(result.stderr.toString(), '');
104+
assert.strictEqual(result.status, 0);
105+
assert(findCoverageFileForPid(result.pid));
106+
});
107+
108+
await t.test('coverage is reported without NODE_V8_COVERAGE present', (t) => {
109+
if (!process.features.inspector) {
110+
return;
111+
}
112+
113+
const fixture = fixtures.path('test-runner', 'coverage.js');
114+
const args = ['--experimental-test-coverage', '--test-reporter', 'tap', fixture];
115+
const result = spawnSync(process.execPath, args);
116+
const report = getTapCoverageFixtureReport();
90117

91-
const fixture = fixtures.path('test-runner', 'coverage.js');
92-
const args = ['--experimental-test-coverage', fixture];
93-
const result = spawnSync(process.execPath, args);
94-
const report = getCoverageFixtureReport();
118+
assert(result.stdout.toString().includes(report));
119+
assert.strictEqual(result.stderr.toString(), '');
120+
assert.strictEqual(result.status, 0);
121+
assert(!findCoverageFileForPid(result.pid));
122+
});
123+
});
95124

96-
assert(result.stdout.toString().includes(report));
97-
assert.strictEqual(result.stderr.toString(), '');
98-
assert.strictEqual(result.status, 0);
99-
assert(!findCoverageFileForPid(result.pid));
125+
test('test spec coverage reporter', async (t) => {
126+
await t.test('coverage is reported and dumped to NODE_V8_COVERAGE if present', (t) => {
127+
if (!process.features.inspector) {
128+
return;
129+
}
130+
const fixture = fixtures.path('test-runner', 'coverage.js');
131+
const args = ['--experimental-test-coverage', '--test-reporter', 'spec', fixture];
132+
const options = { env: { ...process.env, NODE_V8_COVERAGE: tmpdir.path } };
133+
const result = spawnSync(process.execPath, args, options);
134+
const report = getSpecCoverageFixtureReport();
135+
136+
assert(result.stdout.toString().includes(report));
137+
assert.strictEqual(result.stderr.toString(), '');
138+
assert.strictEqual(result.status, 0);
139+
assert(findCoverageFileForPid(result.pid));
140+
});
141+
142+
await t.test('coverage is reported without NODE_V8_COVERAGE present', (t) => {
143+
if (!process.features.inspector) {
144+
return;
145+
}
146+
const fixture = fixtures.path('test-runner', 'coverage.js');
147+
const args = ['--experimental-test-coverage', '--test-reporter', 'spec', fixture];
148+
const result = spawnSync(process.execPath, args);
149+
const report = getSpecCoverageFixtureReport();
150+
151+
assert(result.stdout.toString().includes(report));
152+
assert.strictEqual(result.stderr.toString(), '');
153+
assert.strictEqual(result.status, 0);
154+
assert(!findCoverageFileForPid(result.pid));
155+
});
100156
});

0 commit comments

Comments
 (0)