Skip to content

Commit 93bf447

Browse files
dmnsgnRafaelGSS
authored andcommitted
test_runner: refactor coverage report output for readability
Add a "table" parameter to getCoverageReport. Keep the tap coverage output intact. Change the output by adding padding and truncating the tables' cells. Add separation lines for table head/body/foot. Group uncovered lines as ranges. Add yellow color for coverage between 50 and 90. Refs: #46674 PR-URL: #47791 Reviewed-By: Moshe Atlow <[email protected]>
1 parent 159ab66 commit 93bf447

File tree

4 files changed

+153
-38
lines changed

4 files changed

+153
-38
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class SpecReporter extends Transform{
123123
case'test:diagnostic':
124124
return`${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`;
125125
case'test:coverage':
126-
returngetCoverageReport(this.#indent(data.nesting),data.summary,symbols['test:coverage'],blue);
126+
returngetCoverageReport(this.#indent(data.nesting),data.summary,symbols['test:coverage'],blue,true);
127127
}
128128
}
129129
_transform({ type, data },encoding,callback){

‎lib/internal/test_runner/utils.js‎

Lines changed: 140 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,28 @@ const{
33
ArrayPrototypeJoin,
44
ArrayPrototypeMap,
55
ArrayPrototypePush,
6+
ArrayPrototypeReduce,
67
ObjectGetOwnPropertyDescriptor,
8+
MathFloor,
9+
MathMax,
10+
MathMin,
711
NumberPrototypeToFixed,
812
SafePromiseAllReturnArrayLike,
913
RegExp,
1014
RegExpPrototypeExec,
1115
SafeMap,
16+
StringPrototypePadStart,
17+
StringPrototypePadEnd,
18+
StringPrototypeRepeat,
19+
StringPrototypeSlice,
1220
}=primordials;
1321

1422
const{ basename, relative }=require('path');
1523
const{ createWriteStream }=require('fs');
1624
const{ pathToFileURL }=require('internal/url');
1725
const{ createDeferredPromise }=require('internal/util');
1826
const{ getOptionValue }=require('internal/options');
19-
const{ green, red, white, shouldColorize }=require('internal/util/colors');
27+
const{ green,yellow,red, white, shouldColorize }=require('internal/util/colors');
2028

2129
const{
2230
codes: {
@@ -27,6 +35,13 @@ const{
2735
}=require('internal/errors');
2836
const{ compose }=require('stream');
2937

38+
constcoverageColors={
39+
__proto__: null,
40+
high: green,
41+
medium: yellow,
42+
low: red,
43+
};
44+
3045
constkMultipleCallbackInvocations='multipleCallbackInvocations';
3146
constkRegExpPattern=/^\/(.*)\/([a-z]*)$/;
3247
constkSupportedFileExtensions=/\.[cm]?js$/;
@@ -256,45 +271,139 @@ function countCompletedTest(test, harness = test.root.harness){
256271
}
257272

258273

259-
functioncoverageThreshold(coverage,color){
260-
coverage=NumberPrototypeToFixed(coverage,2);
261-
if(color){
262-
if(coverage>90)return`${green}${coverage}${color}`;
263-
if(coverage<50)return`${red}${coverage}${color}`;
274+
constmemo=newSafeMap();
275+
functionaddTableLine(prefix,width){
276+
constkey=`${prefix}-${width}`;
277+
letvalue=memo.get(key);
278+
if(value===undefined){
279+
value=`${prefix}${StringPrototypeRepeat('-',width)}\n`;
280+
memo.set(key,value);
264281
}
265-
returncoverage;
282+
283+
returnvalue;
284+
}
285+
286+
constkHorizontalEllipsis='\u2026';
287+
functiontruncateStart(string,width){
288+
returnstring.length>width ? `${kHorizontalEllipsis}${StringPrototypeSlice(string,string.length-width+1)}` : string;
289+
}
290+
291+
functiontruncateEnd(string,width){
292+
returnstring.length>width ? `${StringPrototypeSlice(string,0,width-1)}${kHorizontalEllipsis}` : string;
293+
}
294+
295+
functionformatLinesToRanges(values){
296+
returnArrayPrototypeMap(ArrayPrototypeReduce(values,(prev,current,index,array)=>{
297+
if((index>0)&&((current-array[index-1])===1)){
298+
prev[prev.length-1][1]=current;
299+
}else{
300+
prev.push([current]);
301+
}
302+
returnprev;
303+
},[]),(range)=>ArrayPrototypeJoin(range,'-'));
304+
}
305+
306+
functionformatUncoveredLines(lines,table){
307+
if(table)returnArrayPrototypeJoin(formatLinesToRanges(lines),' ');
308+
returnArrayPrototypeJoin(lines,', ');
266309
}
267310

268-
functiongetCoverageReport(pad,summary,symbol,color){
269-
letreport=`${color}${pad}${symbol}start of coverage report\n`;
311+
constkColumns=['line %','branch %','funcs %'];
312+
constkColumnsKeys=['coveredLinePercent','coveredBranchPercent','coveredFunctionPercent'];
313+
constkSeparator=' | ';
314+
315+
functiongetCoverageReport(pad,summary,symbol,color,table){
316+
constprefix=`${pad}${symbol}`;
317+
letreport=`${color}${prefix}start of coverage report\n`;
318+
319+
letfilePadLength;
320+
letcolumnPadLengths=[];
321+
letuncoveredLinesPadLength;
322+
lettableWidth;
323+
324+
if(table){
325+
// Get expected column sizes
326+
filePadLength=table&&ArrayPrototypeReduce(summary.files,(acc,file)=>
327+
MathMax(acc,relative(summary.workingDirectory,file.path).length),0);
328+
filePadLength=MathMax(filePadLength,'file'.length);
329+
constfileWidth=filePadLength+2;
330+
331+
columnPadLengths=ArrayPrototypeMap(kColumns,(column)=>(table ? MathMax(column.length,6) : 0));
332+
constcolumnsWidth=ArrayPrototypeReduce(columnPadLengths,(acc,columnPadLength)=>acc+columnPadLength+3,0);
333+
334+
uncoveredLinesPadLength=table&&ArrayPrototypeReduce(summary.files,(acc,file)=>
335+
MathMax(acc,formatUncoveredLines(file.uncoveredLineNumbers,table).length),0);
336+
uncoveredLinesPadLength=MathMax(uncoveredLinesPadLength,'uncovered lines'.length);
337+
constuncoveredLinesWidth=uncoveredLinesPadLength+2;
338+
339+
tableWidth=fileWidth+columnsWidth+uncoveredLinesWidth;
340+
341+
// Fit with sensible defaults
342+
constavailableWidth=(process.stdout.columns||Infinity)-prefix.length;
343+
constcolumnsExtras=tableWidth-availableWidth;
344+
if(table&&columnsExtras>0){
345+
// Ensure file name is sufficiently visible
346+
constminFilePad=MathMin(8,filePadLength);
347+
filePadLength-=MathFloor(columnsExtras*0.2);
348+
filePadLength=MathMax(filePadLength,minFilePad);
349+
350+
// Get rest of available space, subtracting margins
351+
uncoveredLinesPadLength=MathMax(availableWidth-columnsWidth-(filePadLength+2)-2,1);
352+
353+
// Update table width
354+
tableWidth=availableWidth;
355+
}else{
356+
uncoveredLinesPadLength=Infinity;
357+
}
358+
}
359+
360+
361+
functiongetCell(string,width,pad,truncate,coverage){
362+
if(!table)returnstring;
363+
364+
letresult=string;
365+
if(pad)result=pad(result,width);
366+
if(truncate)result=truncate(result,width);
367+
if(color&&coverage!==undefined){
368+
if(coverage>90)return`${coverageColors.high}${result}${color}`;
369+
if(coverage>50)return`${coverageColors.medium}${result}${color}`;
370+
return`${coverageColors.low}${result}${color}`;
371+
}
372+
returnresult;
373+
}
270374

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

382+
// Body
273383
for(leti=0;i<summary.files.length;++i){
274-
const{
275-
path,
276-
coveredLinePercent,
277-
coveredBranchPercent,
278-
coveredFunctionPercent,
279-
uncoveredLineNumbers,
280-
}=summary.files[i];
281-
constrelativePath=relative(summary.workingDirectory,path);
282-
constlines=coverageThreshold(coveredLinePercent,color);
283-
constbranches=coverageThreshold(coveredBranchPercent,color);
284-
constfunctions=coverageThreshold(coveredFunctionPercent,color);
285-
constuncovered=ArrayPrototypeJoin(uncoveredLineNumbers,', ');
286-
287-
report+=`${pad}${symbol}${relativePath} | ${lines} | ${branches} | `+
288-
`${functions} | ${uncovered}\n`;
384+
constfile=summary.files[i];
385+
constrelativePath=relative(summary.workingDirectory,file.path);
386+
387+
letfileCoverage=0;
388+
constcoverages=ArrayPrototypeMap(kColumnsKeys,(columnKey)=>{
389+
constpercent=file[columnKey];
390+
fileCoverage+=percent;
391+
returnpercent;
392+
});
393+
fileCoverage/=kColumnsKeys.length;
394+
395+
report+=`${prefix}${getCell(relativePath,filePadLength,StringPrototypePadEnd,truncateStart,fileCoverage)}${kSeparator}`+
396+
`${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`;
289398
}
290399

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

297-
report+=`${pad}${symbol}end of coverage report\n`;
406+
report+=`${prefix}end of coverage report\n`;
298407
if(color){
299408
report+=white;
300409
}

‎lib/internal/util/colors.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module.exports ={
2828
module.exports.blue=hasColors ? '\u001b[34m' : '';
2929
module.exports.green=hasColors ? '\u001b[32m' : '';
3030
module.exports.white=hasColors ? '\u001b[39m' : '';
31+
module.exports.yellow=hasColors ? '\u001b[33m' : '';
3132
module.exports.red=hasColors ? '\u001b[31m' : '';
3233
module.exports.gray=hasColors ? '\u001b[90m' : '';
3334
module.exports.clear=hasColors ? '\u001bc' : '';

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,21 @@ function getTapCoverageFixtureReport(){
4141
}
4242

4343
functiongetSpecCoverageFixtureReport(){
44+
/* eslint-disable max-len */
4445
constreport=[
4546
'\u2139 start of coverage report',
46-
'\u2139 file | line % | branch % | funcs % | uncovered lines',
47-
'\u2139 test/fixtures/test-runner/coverage.js | 78.65 | 38.46 | 60.00 | 12, '+
48-
'13, 16, 17, 18, 19, 20, 21, 22, 27, 39, 43, 44, 61, 62, 66, 67, 71, 72',
49-
'\u2139 test/fixtures/test-runner/invalid-tap.js | 100.00 | 100.00 | 100.00 | ',
50-
'\u2139 test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5, 6',
51-
'\u2139 all files | 78.35 | 43.75 | 60.00 |',
47+
'\u2139 -------------------------------------------------------------------------------------------------------------------',
48+
'\u2139 file | line % | branch % | funcs % | uncovered lines',
49+
'\u2139 -------------------------------------------------------------------------------------------------------------------',
50+
'\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',
51+
'\u2139 test/fixtures/test-runner/invalid-tap.js | 100.00 | 100.00 | 100.00 | ',
52+
'\u2139 test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5-6',
53+
'\u2139 -------------------------------------------------------------------------------------------------------------------',
54+
'\u2139 all files | 78.35 | 43.75 | 60.00 |',
55+
'\u2139 -------------------------------------------------------------------------------------------------------------------',
5256
'\u2139 end of coverage report',
5357
].join('\n');
58+
/* eslint-enable max-len */
5459

5560
if(common.isWindows){
5661
returnreport.replaceAll('/','\\');

0 commit comments

Comments
(0)