Skip to content

Commit 99312a5

Browse files
pulkit-30MoLow
authored andcommitted
test_runner: add code coverage support to spec reporter
PR-URL: #46674 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
1 parent dc22edb commit 99312a5

4 files changed

Lines changed: 163 additions & 89 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const assert = require('assert');
1515
constTransform=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
constinspectOptions={__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+
returngetCoverageReport(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‎

Lines changed: 2 additions & 37 deletions
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
constkDefaultIndent=' ';// 4 spaces
1817
constkFrameStartRegExp=/^{4}at/;
1918
constkLineBreakRegExp=/\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-
yieldreportCoverage(data.nesting,data.summary);
51+
yieldgetCoverageReport(indent(data.nesting),data.summary,'# ','');
5352
break;
5453
}
5554
}
@@ -73,40 +72,6 @@ function reportTest(nesting, testNumber, status, name, skip, todo) {
7372
returnline;
7473
}
7574

76-
functionreportCoverage(nesting,summary){
77-
constpad=indent(nesting);
78-
letreport=`${pad}# start of coverage report\n`;
79-
80-
report+=`${pad}# file | line % | branch % | funcs % | uncovered lines\n`;
81-
82-
for(leti=0;i<summary.files.length;++i){
83-
const{
84-
path,
85-
coveredLinePercent,
86-
coveredBranchPercent,
87-
coveredFunctionPercent,
88-
uncoveredLineNumbers,
89-
}=summary.files[i];
90-
constrelativePath=relative(summary.workingDirectory,path);
91-
constlines=NumberPrototypeToFixed(coveredLinePercent,2);
92-
constbranches=NumberPrototypeToFixed(coveredBranchPercent,2);
93-
constfunctions=NumberPrototypeToFixed(coveredFunctionPercent,2);
94-
constuncovered=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-
returnreport;
108-
}
109-
11075
functionreportDetails(nesting,data=kEmptyObject){
11176
const{ error, duration_ms }=data;
11277
const_indent=indent(nesting);

‎lib/internal/test_runner/utils.js‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
'use strict';
22
const{
3+
ArrayPrototypeJoin,
34
ArrayPrototypeMap,
45
ArrayPrototypePush,
56
ObjectCreate,
67
ObjectGetOwnPropertyDescriptor,
8+
NumberPrototypeToFixed,
79
SafePromiseAllReturnArrayLike,
810
RegExp,
911
RegExpPrototypeExec,
1012
SafeMap,
1113
}=primordials;
1214

13-
const{ basename }=require('path');
15+
const{ basename, relative}=require('path');
1416
const{ createWriteStream }=require('fs');
1517
const{ pathToFileURL }=require('internal/url');
1618
const{ createDeferredPromise }=require('internal/util');
1719
const{ getOptionValue }=require('internal/options');
20+
const{ green, red, white }=require('internal/util/colors');
1821

1922
const{
2023
codes: {
@@ -247,6 +250,52 @@ function countCompletedTest(test, harness = test.root.harness) {
247250
harness.counters.all++;
248251
}
249252

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

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

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

21-
functiongetCoverageFixtureReport(){
21+
functiongetTapCoverageFixtureReport(){
2222
constreport=[
2323
'# start of coverage report',
2424
'# file | line % | branch % | funcs % | uncovered lines',
@@ -37,64 +37,120 @@ function getCoverageFixtureReport() {
3737
returnreport;
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-
constargs=['--test','--experimental-test-coverage'];
44-
constresult=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-coveragecannotbeusedwith--test/
51-
);
52-
});
40+
functiongetSpecCoverageFixtureReport(){
41+
constreport=[
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+
returnreport.replaceAll('/','\\');
5754
}
5855

59-
constfixture=fixtures.path('test-runner','coverage.js');
60-
constargs=['--experimental-test-coverage',fixture];
61-
constresult=spawnSync(process.execPath,args);
56+
returnreport;
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+
awaitt.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+
constargs=['--test','--experimental-test-coverage'];
64+
constresult=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-coveragecannotbeusedwith--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+
awaitt.test('handles the inspector not being available',(t)=>{
75+
if(process.features.inspector){
76+
return;
77+
}
7378

74-
constfixture=fixtures.path('test-runner','coverage.js');
75-
constargs=['--experimental-test-coverage',fixture];
76-
constoptions={env: { ...process.env,NODE_V8_COVERAGE: tmpdir.path}};
77-
constresult=spawnSync(process.execPath,args,options);
78-
constreport=getCoverageFixtureReport();
79+
constfixture=fixtures.path('test-runner','coverage.js');
80+
constargs=['--experimental-test-coverage',fixture];
81+
constresult=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+
awaitt.test('coverage is reported and dumped to NODE_V8_COVERAGE if present',(t)=>{
92+
if(!process.features.inspector){
93+
return;
94+
}
95+
96+
constfixture=fixtures.path('test-runner','coverage.js');
97+
constargs=['--experimental-test-coverage','--test-reporter','tap',fixture];
98+
constoptions={env: { ...process.env,NODE_V8_COVERAGE: tmpdir.path}};
99+
constresult=spawnSync(process.execPath,args,options);
100+
constreport=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+
awaitt.test('coverage is reported without NODE_V8_COVERAGE present',(t)=>{
109+
if(!process.features.inspector){
110+
return;
111+
}
112+
113+
constfixture=fixtures.path('test-runner','coverage.js');
114+
constargs=['--experimental-test-coverage','--test-reporter','tap',fixture];
115+
constresult=spawnSync(process.execPath,args);
116+
constreport=getTapCoverageFixtureReport();
90117

91-
constfixture=fixtures.path('test-runner','coverage.js');
92-
constargs=['--experimental-test-coverage',fixture];
93-
constresult=spawnSync(process.execPath,args);
94-
constreport=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+
awaitt.test('coverage is reported and dumped to NODE_V8_COVERAGE if present',(t)=>{
127+
if(!process.features.inspector){
128+
return;
129+
}
130+
constfixture=fixtures.path('test-runner','coverage.js');
131+
constargs=['--experimental-test-coverage','--test-reporter','spec',fixture];
132+
constoptions={env: { ...process.env,NODE_V8_COVERAGE: tmpdir.path}};
133+
constresult=spawnSync(process.execPath,args,options);
134+
constreport=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+
awaitt.test('coverage is reported without NODE_V8_COVERAGE present',(t)=>{
143+
if(!process.features.inspector){
144+
return;
145+
}
146+
constfixture=fixtures.path('test-runner','coverage.js');
147+
constargs=['--experimental-test-coverage','--test-reporter','spec',fixture];
148+
constresult=spawnSync(process.execPath,args);
149+
constreport=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)