@@ -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
1422const { basename, relative } = require ( 'path' ) ;
1523const { createWriteStream } = require ( 'fs' ) ;
1624const { pathToFileURL } = require ( 'internal/url' ) ;
1725const { createDeferredPromise } = require ( 'internal/util' ) ;
1826const { 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
2129const {
2230 codes : {
@@ -27,6 +35,13 @@ const {
2735} = require ( 'internal/errors' ) ;
2836const { compose } = require ( 'stream' ) ;
2937
38+ const coverageColors = {
39+ __proto__ : null ,
40+ high : green ,
41+ medium : yellow ,
42+ low : red ,
43+ } ;
44+
3045const kMultipleCallbackInvocations = 'multipleCallbackInvocations' ;
3146const kRegExpPattern = / ^ \/ ( .* ) \/ ( [ a - z ] * ) $ / ;
3247const kSupportedFileExtensions = / \. [ c m ] ? j s $ / ;
@@ -256,45 +271,139 @@ function countCompletedTest(test, harness = test.root.harness) {
256271}
257272
258273
259- function coverageThreshold ( 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+ const memo = new SafeMap ( ) ;
275+ function addTableLine ( prefix , width ) {
276+ const key = `${ prefix } -${ width } ` ;
277+ let value = memo . get ( key ) ;
278+ if ( value === undefined ) {
279+ value = `${ prefix } ${ StringPrototypeRepeat ( '-' , width ) } \n` ;
280+ memo . set ( key , value ) ;
264281 }
265- return coverage ;
282+
283+ return value ;
284+ }
285+
286+ const kHorizontalEllipsis = '\u2026' ;
287+ function truncateStart ( string , width ) {
288+ return string . length > width ? `${ kHorizontalEllipsis } ${ StringPrototypeSlice ( string , string . length - width + 1 ) } ` : string ;
289+ }
290+
291+ function truncateEnd ( string , width ) {
292+ return string . length > width ? `${ StringPrototypeSlice ( string , 0 , width - 1 ) } ${ kHorizontalEllipsis } ` : string ;
293+ }
294+
295+ function formatLinesToRanges ( values ) {
296+ return ArrayPrototypeMap ( 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+ return prev ;
303+ } , [ ] ) , ( range ) => ArrayPrototypeJoin ( range , '-' ) ) ;
304+ }
305+
306+ function formatUncoveredLines ( lines , table ) {
307+ if ( table ) return ArrayPrototypeJoin ( formatLinesToRanges ( lines ) , ' ' ) ;
308+ return ArrayPrototypeJoin ( lines , ', ' ) ;
266309}
267310
268- function getCoverageReport ( pad , summary , symbol , color ) {
269- let report = `${ color } ${ pad } ${ symbol } start of coverage report\n` ;
311+ const kColumns = [ 'line %' , 'branch %' , 'funcs %' ] ;
312+ const kColumnsKeys = [ 'coveredLinePercent' , 'coveredBranchPercent' , 'coveredFunctionPercent' ] ;
313+ const kSeparator = ' | ' ;
314+
315+ function getCoverageReport ( pad , summary , symbol , color , table ) {
316+ const prefix = `${ pad } ${ symbol } ` ;
317+ let report = `${ color } ${ prefix } start of coverage report\n` ;
318+
319+ let filePadLength ;
320+ let columnPadLengths = [ ] ;
321+ let uncoveredLinesPadLength ;
322+ let tableWidth ;
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+ const fileWidth = filePadLength + 2 ;
330+
331+ columnPadLengths = ArrayPrototypeMap ( kColumns , ( column ) => ( table ? MathMax ( column . length , 6 ) : 0 ) ) ;
332+ const columnsWidth = 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+ const uncoveredLinesWidth = uncoveredLinesPadLength + 2 ;
338+
339+ tableWidth = fileWidth + columnsWidth + uncoveredLinesWidth ;
340+
341+ // Fit with sensible defaults
342+ const availableWidth = ( process . stdout . columns || Infinity ) - prefix . length ;
343+ const columnsExtras = tableWidth - availableWidth ;
344+ if ( table && columnsExtras > 0 ) {
345+ // Ensure file name is sufficiently visible
346+ const minFilePad = 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+ function getCell ( string , width , pad , truncate , coverage ) {
362+ if ( ! table ) return string ;
363+
364+ let result = 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+ return result ;
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 ( let i = 0 ; i < summary . files . length ; ++ i ) {
274- const {
275- path,
276- coveredLinePercent,
277- coveredBranchPercent,
278- coveredFunctionPercent,
279- uncoveredLineNumbers,
280- } = summary . files [ i ] ;
281- const relativePath = relative ( summary . workingDirectory , path ) ;
282- const lines = coverageThreshold ( coveredLinePercent , color ) ;
283- const branches = coverageThreshold ( coveredBranchPercent , color ) ;
284- const functions = coverageThreshold ( coveredFunctionPercent , color ) ;
285- const uncovered = ArrayPrototypeJoin ( uncoveredLineNumbers , ', ' ) ;
286-
287- report += `${ pad } ${ symbol } ${ relativePath } | ${ lines } | ${ branches } | ` +
288- `${ functions } | ${ uncovered } \n` ;
384+ const file = summary . files [ i ] ;
385+ const relativePath = relative ( summary . workingDirectory , file . path ) ;
386+
387+ let fileCoverage = 0 ;
388+ const coverages = ArrayPrototypeMap ( kColumnsKeys , ( columnKey ) => {
389+ const percent = file [ columnKey ] ;
390+ fileCoverage += percent ;
391+ return percent ;
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 }
0 commit comments