From 7a1b7f41ca0475f231973056af999d77694ae12d Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Fri, 25 Aug 2023 15:46:03 +1000 Subject: [PATCH] test_runner: report covered lines, functions and branches to reporters This is a breaking change for the format of test:coverage events. But the test coverage is still experimental, so I don't believe it requires a semver-major bump. Fixes https://github.com/nodejs/node/issues/49303 --- doc/api/test.md | 14 +++++++++++-- lib/internal/test_runner/coverage.js | 30 +++++++++++++++++++++++++--- lib/internal/test_runner/utils.js | 16 ++++++++++++--- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 36f6851c6acf17..92cf0169d4fbd0 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -2011,8 +2011,18 @@ object, streaming a series of events representing the execution of the tests. * `coveredLinePercent` {number} The percentage of lines covered. * `coveredBranchPercent` {number} The percentage of branches covered. * `coveredFunctionPercent` {number} The percentage of functions covered. - * `uncoveredLineNumbers` {Array} An array of integers representing line - numbers that are uncovered. + * `functions` {Array} An array of functions representing function + coverage. + * `name` {string} The name of the function. + * `line` {number} The line number where the function is defined. + * `count` {number} The number of times the function was called. + * `branches` {Array} An array of branches representing branch coverage. + * `line` {number} The line number where the branch is defined. + * `count` {number} The number of times the branch was taken. + * `lines` {Array} An array of lines representing line + numbers and the number of times they were covered. + * `line` {number} The line number. + * `count` {number} The number of times the line was covered. * `totals` {Object} An object containing a summary of coverage for all files. * `totalLineCount` {number} The total number of lines. diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 70f88984c82150..978537ee8d85c0 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -13,6 +13,7 @@ const { StringPrototypeIncludes, StringPrototypeLocaleCompare, StringPrototypeStartsWith, + MathMax } = primordials; const { copyFileSync, @@ -43,6 +44,7 @@ class CoverageLine { this.startOffset = startOffset; this.endOffset = startOffset + src.length - newlineLength; this.ignore = false; + this.count = 0; this.#covered = true; } @@ -118,6 +120,8 @@ class TestCoverage { let totalFunctions = 0; let branchesCovered = 0; let functionsCovered = 0; + const functionReports = []; + const branchReports = []; const lines = ArrayPrototypeMap(linesWithBreaks, (line, i) => { const startOffset = offset; @@ -165,6 +169,11 @@ class TestCoverage { mapRangeToLines(range, lines); if (isBlockCoverage) { + ArrayPrototypePush(branchReports, { + line: range.lines[0].line, + count: range.count + }); + if (range.count !== 0 || range.ignoredLines === range.lines.length) { branchesCovered++; @@ -177,6 +186,12 @@ class TestCoverage { if (j > 0 && ranges.length > 0) { const range = ranges[0]; + ArrayPrototypePush(functionReports, { + name: functions[j].functionName, + count: MathMax(...ArrayPrototypeMap(ranges, r => r.count)), + line: range.lines[0].line + }); + if (range.count !== 0 || range.ignoredLines === range.lines.length) { functionsCovered++; } @@ -186,15 +201,18 @@ class TestCoverage { } let coveredCnt = 0; - const uncoveredLineNums = []; + const lineReports = []; for (let j = 0; j < lines.length; ++j) { const line = lines[j]; if (line.covered || line.ignore) { coveredCnt++; + if (!line.ignore) { + ArrayPrototypePush(lineReports, { line: line.line, count: line.count }) + } } else { - ArrayPrototypePush(uncoveredLineNums, line.line); + ArrayPrototypePush(lineReports, { line: line.line, count: 0 }); } } @@ -210,7 +228,9 @@ class TestCoverage { coveredLinePercent: toPercentage(coveredCnt, lines.length), coveredBranchPercent: toPercentage(branchesCovered, totalBranches), coveredFunctionPercent: toPercentage(functionsCovered, totalFunctions), - uncoveredLineNumbers: uncoveredLineNums, + functions: functionReports, + branches: branchReports, + lines: lineReports, }); coverageSummary.totals.totalLineCount += lines.length; @@ -321,6 +341,10 @@ function mapRangeToLines(range, lines) { endOffset >= line.endOffset) { line.covered = false; } + if (count > 0 && startOffset <= line.startOffset && + endOffset >= line.endOffset) { + line.count = count; + } ArrayPrototypePush(mappedLines, line); diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index fba2c3132339aa..7a68c8dba72fde 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -297,6 +297,15 @@ function formatLinesToRanges(values) { }, []), (range) => ArrayPrototypeJoin(range, '-')); } +function getUncoveredLines(lines) { + return ArrayPrototypeReduce(lines, (acc, line) => { + if (line.count === 0) { + ArrayPrototypePush(acc, line.line); + } + return acc; + }, []); +} + function formatUncoveredLines(lines, table) { if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' '); return ArrayPrototypeJoin(lines, ', '); @@ -325,8 +334,9 @@ function getCoverageReport(pad, summary, symbol, color, table) { columnPadLengths = ArrayPrototypeMap(kColumns, (column) => (table ? MathMax(column.length, 6) : 0)); const columnsWidth = ArrayPrototypeReduce(columnPadLengths, (acc, columnPadLength) => acc + columnPadLength + 3, 0); - uncoveredLinesPadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) => - MathMax(acc, formatUncoveredLines(file.uncoveredLineNumbers, table).length), 0); + uncoveredLinesPadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) => { + return MathMax(acc, formatUncoveredLines(getUncoveredLines(file.lines), table).length) + }, 0); uncoveredLinesPadLength = MathMax(uncoveredLinesPadLength, 'uncovered lines'.length); const uncoveredLinesWidth = uncoveredLinesPadLength + 2; @@ -388,7 +398,7 @@ function getCoverageReport(pad, summary, symbol, color, table) { report += `${prefix}${getCell(relativePath, filePadLength, StringPrototypePadEnd, truncateStart, fileCoverage)}${kSeparator}` + `${ArrayPrototypeJoin(ArrayPrototypeMap(coverages, (coverage, j) => getCell(NumberPrototypeToFixed(coverage, 2), columnPadLengths[j], StringPrototypePadStart, false, coverage)), kSeparator)}${kSeparator}` + - `${getCell(formatUncoveredLines(file.uncoveredLineNumbers, table), uncoveredLinesPadLength, false, truncateEnd)}\n`; + `${getCell(formatUncoveredLines(getUncoveredLines(file.lines), table), uncoveredLinesPadLength, false, truncateEnd)}\n`; } // Foot