@@ -3,21 +3,29 @@ const {
33 ArrayPrototypeJoin,
44 ArrayPrototypeMap,
55 ArrayPrototypePush,
6+ ArrayPrototypeReduce,
67 ObjectCreate,
78 ObjectGetOwnPropertyDescriptor,
9+ MathFloor,
10+ MathMax,
11+ MathMin,
812 NumberPrototypeToFixed,
913 SafePromiseAllReturnArrayLike,
1014 RegExp,
1115 RegExpPrototypeExec,
1216 SafeMap,
17+ StringPrototypePadStart,
18+ StringPrototypePadEnd,
19+ StringPrototypeRepeat,
20+ StringPrototypeSlice,
1321} = primordials ;
1422
1523const { basename, relative } = require ( 'path' ) ;
1624const { createWriteStream } = require ( 'fs' ) ;
1725const { pathToFileURL } = require ( 'internal/url' ) ;
1826const { createDeferredPromise } = require ( 'internal/util' ) ;
1927const { getOptionValue } = require ( 'internal/options' ) ;
20- const { green, red, white, shouldColorize } = require ( 'internal/util/colors' ) ;
28+ const { green, yellow , red, white, shouldColorize } = require ( 'internal/util/colors' ) ;
2129
2230const {
2331 codes : {
@@ -28,6 +36,13 @@ const {
2836} = require ( 'internal/errors' ) ;
2937const { compose } = require ( 'stream' ) ;
3038
39+ const coverageColors = {
40+ __proto__ : null ,
41+ high : green ,
42+ medium : yellow ,
43+ low : red ,
44+ } ;
45+
3146const kMultipleCallbackInvocations = 'multipleCallbackInvocations' ;
3247const kRegExpPattern = / ^ \/ ( .* ) \/ ( [ a - z ] * ) $ / ;
3348const kSupportedFileExtensions = / \. [ c m ] ? j s $ / ;
@@ -257,45 +272,139 @@ function countCompletedTest(test, harness = test.root.harness) {
257272}
258273
259274
260- function coverageThreshold ( coverage , color ) {
261- coverage = NumberPrototypeToFixed ( coverage , 2 ) ;
262- if ( color ) {
263- if ( coverage > 90 ) return `${ green } ${ coverage } ${ color } ` ;
264- if ( coverage < 50 ) return `${ red } ${ coverage } ${ color } ` ;
275+ const memo = new SafeMap ( ) ;
276+ function addTableLine ( prefix , width ) {
277+ const key = `${ prefix } -${ width } ` ;
278+ let value = memo . get ( key ) ;
279+ if ( value === undefined ) {
280+ value = `${ prefix } ${ StringPrototypeRepeat ( '-' , width ) } \n` ;
281+ memo . set ( key , value ) ;
265282 }
266- return coverage ;
283+
284+ return value ;
285+ }
286+
287+ const kHorizontalEllipsis = '\u2026' ;
288+ function truncateStart ( string , width ) {
289+ return string . length > width ? `${ kHorizontalEllipsis } ${ StringPrototypeSlice ( string , string . length - width + 1 ) } ` : string ;
290+ }
291+
292+ function truncateEnd ( string , width ) {
293+ return string . length > width ? `${ StringPrototypeSlice ( string , 0 , width - 1 ) } ${ kHorizontalEllipsis } ` : string ;
294+ }
295+
296+ function formatLinesToRanges ( values ) {
297+ return ArrayPrototypeMap ( ArrayPrototypeReduce ( values , ( prev , current , index , array ) => {
298+ if ( ( index > 0 ) && ( ( current - array [ index - 1 ] ) === 1 ) ) {
299+ prev [ prev . length - 1 ] [ 1 ] = current ;
300+ } else {
301+ prev . push ( [ current ] ) ;
302+ }
303+ return prev ;
304+ } , [ ] ) , ( range ) => ArrayPrototypeJoin ( range , '-' ) ) ;
305+ }
306+
307+ function formatUncoveredLines ( lines , table ) {
308+ if ( table ) return ArrayPrototypeJoin ( formatLinesToRanges ( lines ) , ' ' ) ;
309+ return ArrayPrototypeJoin ( lines , ', ' ) ;
267310}
268311
269- function getCoverageReport ( pad , summary , symbol , color ) {
270- let report = `${ color } ${ pad } ${ symbol } start of coverage report\n` ;
312+ const kColumns = [ 'line %' , 'branch %' , 'funcs %' ] ;
313+ const kColumnsKeys = [ 'coveredLinePercent' , 'coveredBranchPercent' , 'coveredFunctionPercent' ] ;
314+ const kSeparator = ' | ' ;
315+
316+ function getCoverageReport ( pad , summary , symbol , color , table ) {
317+ const prefix = `${ pad } ${ symbol } ` ;
318+ let report = `${ color } ${ prefix } start of coverage report\n` ;
319+
320+ let filePadLength ;
321+ let columnPadLengths = [ ] ;
322+ let uncoveredLinesPadLength ;
323+ let tableWidth ;
324+
325+ if ( table ) {
326+ // Get expected column sizes
327+ filePadLength = table && ArrayPrototypeReduce ( summary . files , ( acc , file ) =>
328+ MathMax ( acc , relative ( summary . workingDirectory , file . path ) . length ) , 0 ) ;
329+ filePadLength = MathMax ( filePadLength , 'file' . length ) ;
330+ const fileWidth = filePadLength + 2 ;
331+
332+ columnPadLengths = ArrayPrototypeMap ( kColumns , ( column ) => ( table ? MathMax ( column . length , 6 ) : 0 ) ) ;
333+ const columnsWidth = ArrayPrototypeReduce ( columnPadLengths , ( acc , columnPadLength ) => acc + columnPadLength + 3 , 0 ) ;
334+
335+ uncoveredLinesPadLength = table && ArrayPrototypeReduce ( summary . files , ( acc , file ) =>
336+ MathMax ( acc , formatUncoveredLines ( file . uncoveredLineNumbers , table ) . length ) , 0 ) ;
337+ uncoveredLinesPadLength = MathMax ( uncoveredLinesPadLength , 'uncovered lines' . length ) ;
338+ const uncoveredLinesWidth = uncoveredLinesPadLength + 2 ;
339+
340+ tableWidth = fileWidth + columnsWidth + uncoveredLinesWidth ;
341+
342+ // Fit with sensible defaults
343+ const availableWidth = ( process . stdout . columns || Infinity ) - prefix . length ;
344+ const columnsExtras = tableWidth - availableWidth ;
345+ if ( table && columnsExtras > 0 ) {
346+ // Ensure file name is sufficiently visible
347+ const minFilePad = MathMin ( 8 , filePadLength ) ;
348+ filePadLength -= MathFloor ( columnsExtras * 0.2 ) ;
349+ filePadLength = MathMax ( filePadLength , minFilePad ) ;
350+
351+ // Get rest of available space, subtracting margins
352+ uncoveredLinesPadLength = MathMax ( availableWidth - columnsWidth - ( filePadLength + 2 ) - 2 , 1 ) ;
353+
354+ // Update table width
355+ tableWidth = availableWidth ;
356+ } else {
357+ uncoveredLinesPadLength = Infinity ;
358+ }
359+ }
360+
361+
362+ function getCell ( string , width , pad , truncate , coverage ) {
363+ if ( ! table ) return string ;
364+
365+ let result = string ;
366+ if ( pad ) result = pad ( result , width ) ;
367+ if ( truncate ) result = truncate ( result , width ) ;
368+ if ( color && coverage !== undefined ) {
369+ if ( coverage > 90 ) return `${ coverageColors . high } ${ result } ${ color } ` ;
370+ if ( coverage > 50 ) return `${ coverageColors . medium } ${ result } ${ color } ` ;
371+ return `${ coverageColors . low } ${ result } ${ color } ` ;
372+ }
373+ return result ;
374+ }
271375
272- report += `${ pad } ${ symbol } file | line % | branch % | funcs % | uncovered lines\n` ;
376+ // Head
377+ if ( table ) report += addTableLine ( prefix , tableWidth ) ;
378+ report += `${ prefix } ${ getCell ( 'file' , filePadLength , StringPrototypePadEnd , truncateEnd ) } ${ kSeparator } ` +
379+ `${ ArrayPrototypeJoin ( ArrayPrototypeMap ( kColumns , ( column , i ) => getCell ( column , columnPadLengths [ i ] , StringPrototypePadStart ) ) , kSeparator ) } ${ kSeparator } ` +
380+ `${ getCell ( 'uncovered lines' , uncoveredLinesPadLength , false , truncateEnd ) } \n` ;
381+ if ( table ) report += addTableLine ( prefix , tableWidth ) ;
273382
383+ // Body
274384 for ( let i = 0 ; i < summary . files . length ; ++ i ) {
275- const {
276- path,
277- coveredLinePercent,
278- coveredBranchPercent,
279- coveredFunctionPercent,
280- uncoveredLineNumbers,
281- } = summary . files [ i ] ;
282- const relativePath = relative ( summary . workingDirectory , path ) ;
283- const lines = coverageThreshold ( coveredLinePercent , color ) ;
284- const branches = coverageThreshold ( coveredBranchPercent , color ) ;
285- const functions = coverageThreshold ( coveredFunctionPercent , color ) ;
286- const uncovered = ArrayPrototypeJoin ( uncoveredLineNumbers , ', ' ) ;
287-
288- report += `${ pad } ${ symbol } ${ relativePath } | ${ lines } | ${ branches } | ` +
289- `${ functions } | ${ uncovered } \n` ;
385+ const file = summary . files [ i ] ;
386+ const relativePath = relative ( summary . workingDirectory , file . path ) ;
387+
388+ let fileCoverage = 0 ;
389+ const coverages = ArrayPrototypeMap ( kColumnsKeys , ( columnKey ) => {
390+ const percent = file [ columnKey ] ;
391+ fileCoverage += percent ;
392+ return percent ;
393+ } ) ;
394+ fileCoverage /= kColumnsKeys . length ;
395+
396+ report += `${ prefix } ${ getCell ( relativePath , filePadLength , StringPrototypePadEnd , truncateStart , fileCoverage ) } ${ kSeparator } ` +
397+ `${ ArrayPrototypeJoin ( ArrayPrototypeMap ( coverages , ( coverage , j ) => getCell ( NumberPrototypeToFixed ( coverage , 2 ) , columnPadLengths [ j ] , StringPrototypePadStart , false , coverage ) ) , kSeparator ) } ${ kSeparator } ` +
398+ `${ getCell ( formatUncoveredLines ( file . uncoveredLineNumbers , table ) , uncoveredLinesPadLength , false , truncateEnd ) } \n` ;
290399 }
291400
292- const { totals } = summary ;
293- report += ` ${ pad } ${ symbol } all files | ` +
294- `${ coverageThreshold ( totals . coveredLinePercent , color ) } | ` +
295- `${ coverageThreshold ( totals . coveredBranchPercent , color ) } | ` +
296- ` ${ coverageThreshold ( totals . coveredFunctionPercent , color ) } |\n` ;
401+ // Foot
402+ if ( table ) report += addTableLine ( prefix , tableWidth ) ;
403+ report += `${ prefix } ${ getCell ( 'all files' , filePadLength , StringPrototypePadEnd , truncateEnd ) } ${ kSeparator } ` +
404+ `${ ArrayPrototypeJoin ( ArrayPrototypeMap ( kColumnsKeys , ( columnKey , j ) => getCell ( NumberPrototypeToFixed ( summary . totals [ columnKey ] , 2 ) , columnPadLengths [ j ] , StringPrototypePadStart , false , summary . totals [ columnKey ] ) ) , kSeparator ) } |\n` ;
405+ if ( table ) report += addTableLine ( prefix , tableWidth ) ;
297406
298- report += `${ pad } ${ symbol } end of coverage report\n` ;
407+ report += `${ prefix } end of coverage report\n` ;
299408 if ( color ) {
300409 report += white ;
301410 }
0 commit comments