@@ -3,20 +3,28 @@ const {
3
3
ArrayPrototypeJoin,
4
4
ArrayPrototypeMap,
5
5
ArrayPrototypePush,
6
+ ArrayPrototypeReduce,
6
7
ObjectGetOwnPropertyDescriptor,
8
+ MathFloor,
9
+ MathMax,
10
+ MathMin,
7
11
NumberPrototypeToFixed,
8
12
SafePromiseAllReturnArrayLike,
9
13
RegExp,
10
14
RegExpPrototypeExec,
11
15
SafeMap,
16
+ StringPrototypePadStart,
17
+ StringPrototypePadEnd,
18
+ StringPrototypeRepeat,
19
+ StringPrototypeSlice,
12
20
} = primordials ;
13
21
14
22
const { basename, relative } = require ( 'path' ) ;
15
23
const { createWriteStream } = require ( 'fs' ) ;
16
24
const { pathToFileURL } = require ( 'internal/url' ) ;
17
25
const { createDeferredPromise } = require ( 'internal/util' ) ;
18
26
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' ) ;
20
28
21
29
const {
22
30
codes : {
@@ -27,6 +35,13 @@ const {
27
35
} = require ( 'internal/errors' ) ;
28
36
const { compose } = require ( 'stream' ) ;
29
37
38
+ const coverageColors = {
39
+ __proto__ : null ,
40
+ high : green ,
41
+ medium : yellow ,
42
+ low : red ,
43
+ } ;
44
+
30
45
const kMultipleCallbackInvocations = 'multipleCallbackInvocations' ;
31
46
const kRegExpPattern = / ^ \/ ( .* ) \/ ( [ a - z ] * ) $ / ;
32
47
const kSupportedFileExtensions = / \. [ c m ] ? j s $ / ;
@@ -256,45 +271,139 @@ function countCompletedTest(test, harness = test.root.harness) {
256
271
}
257
272
258
273
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 ) ;
264
281
}
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 , ', ' ) ;
266
309
}
267
310
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
+ }
270
374
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 ) ;
272
381
382
+ // Body
273
383
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` ;
289
398
}
290
399
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 ) ;
296
405
297
- report += `${ pad } ${ symbol } end of coverage report\n` ;
406
+ report += `${ prefix } end of coverage report\n` ;
298
407
if ( color ) {
299
408
report += white ;
300
409
}
0 commit comments