@@ -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 = /\.[cm]?js$/; |
@@ -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 | } |
|