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