Skip to content

Commit 23216f1

Browse files
philnashUlisesGascon
authored andcommitted
test_runner: report covered lines, functions and branches to reporters
This is a breaking change for the format of test:coverage events. But the test coverage is still experimental, so I don't believe it requires a semver-major bump. Fixes#49303 PR-URL: #49320 Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
1 parent e845155 commit 23216f1

5 files changed

Lines changed: 125 additions & 9 deletions

File tree

‎doc/api/test.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,8 +2026,18 @@ object, streaming a series of events representing the execution of the tests.
20262026
*`coveredLinePercent` {number} The percentage of lines covered.
20272027
*`coveredBranchPercent` {number} The percentage of branches covered.
20282028
*`coveredFunctionPercent` {number} The percentage of functions covered.
2029-
*`uncoveredLineNumbers` {Array} An array of integers representing line
2030-
numbers that are uncovered.
2029+
*`functions` {Array} An array of functions representing function
2030+
coverage.
2031+
*`name` {string} The name of the function.
2032+
*`line` {number} The line number where the function is defined.
2033+
*`count` {number} The number of times the function was called.
2034+
*`branches` {Array} An array of branches representing branch coverage.
2035+
*`line` {number} The line number where the branch is defined.
2036+
*`count` {number} The number of times the branch was taken.
2037+
*`lines` {Array} An array of lines representing line
2038+
numbers and the number of times they were covered.
2039+
*`line` {number} The line number.
2040+
*`count` {number} The number of times the line was covered.
20312041
*`totals` {Object} An object containing a summary of coverage for all
20322042
files.
20332043
*`totalLineCount` {number} The total number of lines.

‎lib/internal/test_runner/coverage.js‎

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
StringPrototypeIncludes,
1414
StringPrototypeLocaleCompare,
1515
StringPrototypeStartsWith,
16+
MathMax,
1617
}=primordials;
1718
const{
1819
copyFileSync,
@@ -43,6 +44,7 @@ class CoverageLine {
4344
this.startOffset=startOffset;
4445
this.endOffset=startOffset+src.length-newlineLength;
4546
this.ignore=false;
47+
this.count=0;
4648
this.#covered =true;
4749
}
4850

@@ -118,6 +120,8 @@ class TestCoverage {
118120
lettotalFunctions=0;
119121
letbranchesCovered=0;
120122
letfunctionsCovered=0;
123+
constfunctionReports=[];
124+
constbranchReports=[];
121125

122126
constlines=ArrayPrototypeMap(linesWithBreaks,(line,i)=>{
123127
conststartOffset=offset;
@@ -159,12 +163,20 @@ class TestCoverage {
159163
for(letj=0;j<functions.length;++j){
160164
const{ isBlockCoverage, ranges }=functions[j];
161165

166+
letmaxCountPerFunction=0;
162167
for(letk=0;k<ranges.length;++k){
163168
constrange=ranges[k];
169+
maxCountPerFunction=MathMax(maxCountPerFunction,range.count);
164170

165171
mapRangeToLines(range,lines);
166172

167173
if(isBlockCoverage){
174+
ArrayPrototypePush(branchReports,{
175+
__proto__: null,
176+
line: range.lines[0].line,
177+
count: range.count,
178+
});
179+
168180
if(range.count!==0||
169181
range.ignoredLines===range.lines.length){
170182
branchesCovered++;
@@ -177,6 +189,13 @@ class TestCoverage {
177189
if(j>0&&ranges.length>0){
178190
constrange=ranges[0];
179191

192+
ArrayPrototypePush(functionReports,{
193+
__proto__: null,
194+
name: functions[j].functionName,
195+
count: maxCountPerFunction,
196+
line: range.lines[0].line,
197+
});
198+
180199
if(range.count!==0||range.ignoredLines===range.lines.length){
181200
functionsCovered++;
182201
}
@@ -186,15 +205,19 @@ class TestCoverage {
186205
}
187206

188207
letcoveredCnt=0;
189-
constuncoveredLineNums=[];
208+
constlineReports=[];
190209

191210
for(letj=0;j<lines.length;++j){
192211
constline=lines[j];
193-
212+
if(!line.ignore){
213+
ArrayPrototypePush(lineReports,{
214+
__proto__: null,
215+
line: line.line,
216+
count: line.count,
217+
});
218+
}
194219
if(line.covered||line.ignore){
195220
coveredCnt++;
196-
}else{
197-
ArrayPrototypePush(uncoveredLineNums,line.line);
198221
}
199222
}
200223

@@ -210,7 +233,9 @@ class TestCoverage {
210233
coveredLinePercent: toPercentage(coveredCnt,lines.length),
211234
coveredBranchPercent: toPercentage(branchesCovered,totalBranches),
212235
coveredFunctionPercent: toPercentage(functionsCovered,totalFunctions),
213-
uncoveredLineNumbers: uncoveredLineNums,
236+
functions: functionReports,
237+
branches: branchReports,
238+
lines: lineReports,
214239
});
215240

216241
coverageSummary.totals.totalLineCount+=lines.length;
@@ -320,6 +345,11 @@ function mapRangeToLines(range, lines) {
320345
if(count===0&&startOffset<=line.startOffset&&
321346
endOffset>=line.endOffset){
322347
line.covered=false;
348+
line.count=0;
349+
}
350+
if(count>0&&startOffset<=line.startOffset&&
351+
endOffset>=line.endOffset){
352+
line.count=count;
323353
}
324354

325355
ArrayPrototypePush(mappedLines,line);

‎lib/internal/test_runner/utils.js‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const{
33
ArrayPrototypeJoin,
44
ArrayPrototypeMap,
5+
ArrayPrototypeFlatMap,
56
ArrayPrototypePush,
67
ArrayPrototypeReduce,
78
ObjectGetOwnPropertyDescriptor,
@@ -303,6 +304,10 @@ function formatLinesToRanges(values) {
303304
},[]),(range)=>ArrayPrototypeJoin(range,'-'));
304305
}
305306

307+
functiongetUncoveredLines(lines){
308+
returnArrayPrototypeFlatMap(lines,(line)=>(line.count===0 ? line.line : []));
309+
}
310+
306311
functionformatUncoveredLines(lines,table){
307312
if(table)returnArrayPrototypeJoin(formatLinesToRanges(lines),' ');
308313
returnArrayPrototypeJoin(lines,', ');
@@ -332,7 +337,7 @@ function getCoverageReport(pad, summary, symbol, color, table) {
332337
constcolumnsWidth=ArrayPrototypeReduce(columnPadLengths,(acc,columnPadLength)=>acc+columnPadLength+3,0);
333338

334339
uncoveredLinesPadLength=table&&ArrayPrototypeReduce(summary.files,(acc,file)=>
335-
MathMax(acc,formatUncoveredLines(file.uncoveredLineNumbers,table).length),0);
340+
MathMax(acc,formatUncoveredLines(getUncoveredLines(file.lines),table).length),0);
336341
uncoveredLinesPadLength=MathMax(uncoveredLinesPadLength,'uncovered lines'.length);
337342
constuncoveredLinesWidth=uncoveredLinesPadLength+2;
338343

@@ -394,7 +399,7 @@ function getCoverageReport(pad, summary, symbol, color, table) {
394399

395400
report+=`${prefix}${getCell(relativePath,filePadLength,StringPrototypePadEnd,truncateStart,fileCoverage)}${kSeparator}`+
396401
`${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`;
402+
`${getCell(formatUncoveredLines(getUncoveredLines(file.lines),table),uncoveredLinesPadLength,false,truncateEnd)}\n`;
398403
}
399404

400405
// Foot
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import{Transform}from'node:stream';
2+
3+
exportdefaultclassCoverageReporterextendsTransform{
4+
constructor(options){
5+
super({ ...options,writableObjectMode: true});
6+
}
7+
8+
_transform(event,_encoding,callback){
9+
if(event.type==='test:coverage'){
10+
callback(null,JSON.stringify(event.data,null,2));
11+
}else{
12+
callback(null);
13+
}
14+
}
15+
}

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,59 @@ test('coverage is combined for multiple processes', skipIfNoInspector, () => {
181181
assert(result.stdout.toString().includes(report));
182182
assert.strictEqual(result.status,0);
183183
});
184+
185+
test('coverage reports on lines, functions, and branches',skipIfNoInspector,async(t)=>{
186+
constfixture=fixtures.path('test-runner','coverage.js');
187+
constchild=spawnSync(process.execPath,
188+
['--test','--experimental-test-coverage','--test-reporter',
189+
fixtures.fileURL('test-runner/custom_reporters/coverage.mjs'),
190+
fixture]);
191+
assert.strictEqual(child.stderr.toString(),'');
192+
conststdout=child.stdout.toString();
193+
constcoverage=JSON.parse(stdout);
194+
195+
awaitt.test('does not include node_modules',()=>{
196+
assert.strictEqual(coverage.summary.files.length,3);
197+
constfiles=['coverage.js','invalid-tap.js','throw.js'];
198+
coverage.summary.files.forEach((file,index)=>{
199+
assert.ok(file.path.endsWith(files[index]));
200+
});
201+
});
202+
203+
constfile=coverage.summary.files[0];
204+
205+
awaitt.test('reports on function coverage',()=>{
206+
constuncalledFunction=file.functions.find((f)=>f.name==='uncalledTopLevelFunction');
207+
assert.strictEqual(uncalledFunction.count,0);
208+
assert.strictEqual(uncalledFunction.line,16);
209+
210+
constcalledTwice=file.functions.find((f)=>f.name==='fnWithControlFlow');
211+
assert.strictEqual(calledTwice.count,2);
212+
assert.strictEqual(calledTwice.line,35);
213+
});
214+
215+
awaitt.test('reports on branch coverage',()=>{
216+
constuncalledBranch=file.branches.find((b)=>b.line===6);
217+
assert.strictEqual(uncalledBranch.count,0);
218+
219+
constcalledTwice=file.branches.find((b)=>b.line===35);
220+
assert.strictEqual(calledTwice.count,2);
221+
});
222+
223+
awaitt.test('reports on line coverage',()=>{
224+
[
225+
{line: 36,count: 2},
226+
{line: 37,count: 1},
227+
{line: 38,count: 1},
228+
{line: 39,count: 0},
229+
{line: 40,count: 1},
230+
{line: 41,count: 1},
231+
{line: 42,count: 1},
232+
{line: 43,count: 0},
233+
{line: 44,count: 0},
234+
].forEach((line)=>{
235+
consttestLine=file.lines.find((l)=>l.line===line.line);
236+
assert.strictEqual(testLine.count,line.count);
237+
});
238+
});
239+
});

0 commit comments

Comments
 (0)