GeezerVison is showing github.com — test_runner: add code coverage support to spec reporter · nodejs/node@6b87f29
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/reporter/spec.js +4-1Lines changed: 4 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -15,7 +15,7 @@ const assert = require('assert'); | 15 | 15 | const Transform = require('internal/streams/transform'); | 16 | 16 | const { inspectWithNoCustomRetry } = require('internal/errors'); | 17 | 17 | const { green, blue, red, white, gray } = require('internal/util/colors'); | 18 | | - | | 18 | +const { getCoverageReport } = require('internal/test_runner/utils'); | 19 | 19 |
| 20 | 20 | const inspectOptions = { __proto__: null, colors: true, breakLength: Infinity }; | 21 | 21 |
| @@ -30,6 +30,7 @@ const symbols = { | 30 | 30 | 'test:fail': '\u2716 ', | 31 | 31 | 'test:pass': '\u2714 ', | 32 | 32 | 'test:diagnostic': '\u2139 ', | | 33 | + 'test:coverage': '\u2139 ', | 33 | 34 | 'arrow:right': '\u25B6 ', | 34 | 35 | 'hyphen:minus': '\uFE63 ', | 35 | 36 | }; | @@ -115,6 +116,8 @@ class SpecReporter extends Transform { | 115 | 116 | break; | 116 | 117 | case 'test:diagnostic': | 117 | 118 | return `${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`; | | 119 | + case 'test:coverage': | | 120 | + return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue); | 118 | 121 | } | 119 | 122 | } | 120 | 123 | _transform({ type, data }, encoding, callback) { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/reporter/tap.js +2-37Lines changed: 2 additions & 37 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 3 | 3 | ArrayPrototypeForEach, | 4 | 4 | ArrayPrototypeJoin, | 5 | 5 | ArrayPrototypePush, | 6 | | - NumberPrototypeToFixed, | 7 | 6 | ObjectEntries, | 8 | 7 | RegExpPrototypeSymbolReplace, | 9 | 8 | SafeMap, | @@ -13,7 +12,7 @@ const { | 13 | 12 | } = primordials; | 14 | 13 | const { inspectWithNoCustomRetry } = require('internal/errors'); | 15 | 14 | const { isError, kEmptyObject } = require('internal/util'); | 16 | | -const { relative } = require('path'); | | 15 | +const { getCoverageReport } = require('internal/test_runner/utils'); | 17 | 16 | const kDefaultIndent = ' '; // 4 spaces | 18 | 17 | const kFrameStartRegExp = /^ {4}at /; | 19 | 18 | const kLineBreakRegExp = /\n|\r\n/; | @@ -49,7 +48,7 @@ async function * tapReporter(source) { | 49 | 48 | yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`; | 50 | 49 | break; | 51 | 50 | case 'test:coverage': | 52 | | - yield reportCoverage(data.nesting, data.summary); | | 51 | + yield getCoverageReport(indent(data.nesting), data.summary, '# ', ''); | 53 | 52 | break; | 54 | 53 | } | 55 | 54 | } | @@ -73,40 +72,6 @@ function reportTest(nesting, testNumber, status, name, skip, todo) { | 73 | 72 | return line; | 74 | 73 | } | 75 | 74 |
| 76 | | -function reportCoverage(nesting, summary) { | 77 | | - const pad = indent(nesting); | 78 | | - let report = `${pad}# start of coverage report\n`; | 79 | | - | 80 | | - report += `${pad}# file | line % | branch % | funcs % | uncovered lines\n`; | 81 | | - | 82 | | - for (let i = 0; i < summary.files.length; ++i) { | 83 | | - const { | 84 | | - path, | 85 | | - coveredLinePercent, | 86 | | - coveredBranchPercent, | 87 | | - coveredFunctionPercent, | 88 | | - uncoveredLineNumbers, | 89 | | - } = summary.files[i]; | 90 | | - const relativePath = relative(summary.workingDirectory, path); | 91 | | - const lines = NumberPrototypeToFixed(coveredLinePercent, 2); | 92 | | - const branches = NumberPrototypeToFixed(coveredBranchPercent, 2); | 93 | | - const functions = NumberPrototypeToFixed(coveredFunctionPercent, 2); | 94 | | - const uncovered = ArrayPrototypeJoin(uncoveredLineNumbers, ', '); | 95 | | - | 96 | | - report += `${pad}# ${relativePath} | ${lines} | ${branches} | ` + | 97 | | - `${functions} | ${uncovered}\n`; | 98 | | - } | 99 | | - | 100 | | - const { totals } = summary; | 101 | | - report += `${pad}# all files | ` + | 102 | | - `${NumberPrototypeToFixed(totals.coveredLinePercent, 2)} | ` + | 103 | | - `${NumberPrototypeToFixed(totals.coveredBranchPercent, 2)} | ` + | 104 | | - `${NumberPrototypeToFixed(totals.coveredFunctionPercent, 2)} |\n`; | 105 | | - | 106 | | - report += `${pad}# end of coverage report\n`; | 107 | | - return report; | 108 | | -} | 109 | | - | 110 | 75 | function reportDetails(nesting, data = kEmptyObject) { | 111 | 76 | const { error, duration_ms } = data; | 112 | 77 | const _indent = indent(nesting); | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/utils.js +51-1Lines changed: 51 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 | const { | | 3 | + ArrayPrototypeJoin, | 3 | 4 | ArrayPrototypeMap, | 4 | 5 | ArrayPrototypePush, | 5 | 6 | ObjectGetOwnPropertyDescriptor, | | 7 | + NumberPrototypeToFixed, | 6 | 8 | SafePromiseAllReturnArrayLike, | 7 | 9 | RegExp, | 8 | 10 | RegExpPrototypeExec, | 9 | 11 | SafeMap, | 10 | 12 | } = primordials; | 11 | 13 |
| 12 | | -const { basename } = require('path'); | | 14 | +const { basename, relative } = require('path'); | 13 | 15 | const { createWriteStream } = require('fs'); | 14 | 16 | const { pathToFileURL } = require('internal/url'); | 15 | 17 | const { createDeferredPromise } = require('internal/util'); | 16 | 18 | const { getOptionValue } = require('internal/options'); | | 19 | +const { green, red, white } = require('internal/util/colors'); | 17 | 20 |
| 18 | 21 | const { | 19 | 22 | codes: { | @@ -246,6 +249,52 @@ function countCompletedTest(test, harness = test.root.harness) { | 246 | 249 | harness.counters.all++; | 247 | 250 | } | 248 | 251 |
| | 252 | + | | 253 | +function coverageThreshold(coverage, color) { | | 254 | + coverage = NumberPrototypeToFixed(coverage, 2); | | 255 | + if (color) { | | 256 | + if (coverage > 90) return `${green}${coverage}${color}`; | | 257 | + if (coverage < 50) return `${red}${coverage}${color}`; | | 258 | + } | | 259 | + return coverage; | | 260 | +} | | 261 | + | | 262 | +function getCoverageReport(pad, summary, symbol, color) { | | 263 | + let report = `${color}${pad}${symbol}start of coverage report\n`; | | 264 | + | | 265 | + report += `${pad}${symbol}file | line % | branch % | funcs % | uncovered lines\n`; | | 266 | + | | 267 | + for (let i = 0; i < summary.files.length; ++i) { | | 268 | + const { | | 269 | + path, | | 270 | + coveredLinePercent, | | 271 | + coveredBranchPercent, | | 272 | + coveredFunctionPercent, | | 273 | + uncoveredLineNumbers, | | 274 | + } = summary.files[i]; | | 275 | + const relativePath = relative(summary.workingDirectory, path); | | 276 | + const lines = coverageThreshold(coveredLinePercent, color); | | 277 | + const branches = coverageThreshold(coveredBranchPercent, color); | | 278 | + const functions = coverageThreshold(coveredFunctionPercent, color); | | 279 | + const uncovered = ArrayPrototypeJoin(uncoveredLineNumbers, ', '); | | 280 | + | | 281 | + report += `${pad}${symbol}${relativePath} | ${lines} | ${branches} | ` + | | 282 | + `${functions} | ${uncovered}\n`; | | 283 | + } | | 284 | + | | 285 | + const { totals } = summary; | | 286 | + report += `${pad}${symbol}all files | ` + | | 287 | + `${coverageThreshold(totals.coveredLinePercent, color)} | ` + | | 288 | + `${coverageThreshold(totals.coveredBranchPercent, color)} | ` + | | 289 | + `${coverageThreshold(totals.coveredFunctionPercent, color)} |\n`; | | 290 | + | | 291 | + report += `${pad}${symbol}end of coverage report\n`; | | 292 | + if (color) { | | 293 | + report += white; | | 294 | + } | | 295 | + return report; | | 296 | +} | | 297 | + | 249 | 298 | module.exports = { | 250 | 299 | convertStringToRegExp, | 251 | 300 | countCompletedTest, | @@ -255,4 +304,5 @@ module.exports = { | 255 | 304 | isTestFailureError, | 256 | 305 | parseCommandLine, | 257 | 306 | setupTestReporters, | | 307 | + getCoverageReport, | 258 | 308 | }; |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-runner-coverage.js +106-50Lines changed: 106 additions & 50 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -18,7 +18,7 @@ function findCoverageFileForPid(pid) { | 18 | 18 | }); | 19 | 19 | } | 20 | 20 |
| 21 | | -function getCoverageFixtureReport() { | | 21 | +function getTapCoverageFixtureReport() { | 22 | 22 | const report = [ | 23 | 23 | '# start of coverage report', | 24 | 24 | '# file | line % | branch % | funcs % | uncovered lines', | @@ -37,64 +37,120 @@ function getCoverageFixtureReport() { | 37 | 37 | return report; | 38 | 38 | } | 39 | 39 |
| 40 | | -test('--experimental-test-coverage and --test cannot be combined', () => { | 41 | | - // TODO(cjihrig): This test can be removed once multi-process code coverage | 42 | | - // is supported. | 43 | | - const args = ['--test', '--experimental-test-coverage']; | 44 | | - const result = spawnSync(process.execPath, args); | 45 | | - | 46 | | - // 9 is the documented exit code for an invalid CLI argument. | 47 | | - assert.strictEqual(result.status, 9); | 48 | | - assert.match( | 49 | | - result.stderr.toString(), | 50 | | - /--experimental-test-coverage cannot be used with --test/ | 51 | | - ); | 52 | | -}); | | 40 | +function getSpecCoverageFixtureReport() { | | 41 | + const report = [ | | 42 | + '\u2139 start of coverage report', | | 43 | + '\u2139 file | line % | branch % | funcs % | uncovered lines', | | 44 | + '\u2139 test/fixtures/test-runner/coverage.js | 78.65 | 38.46 | 60.00 | 12, ' + | | 45 | + '13, 16, 17, 18, 19, 20, 21, 22, 27, 39, 43, 44, 61, 62, 66, 67, 71, 72', | | 46 | + '\u2139 test/fixtures/test-runner/invalid-tap.js | 100.00 | 100.00 | 100.00 | ', | | 47 | + '\u2139 test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5, 6', | | 48 | + '\u2139 all files | 78.35 | 43.75 | 60.00 |', | | 49 | + '\u2139 end of coverage report', | | 50 | + ].join('\n'); | 53 | 51 |
| 54 | | -test('handles the inspector not being available', (t) => { | 55 | | - if (process.features.inspector) { | 56 | | - return; | | 52 | + if (common.isWindows) { | | 53 | + return report.replaceAll('/', '\\'); | 57 | 54 | } | 58 | 55 |
| 59 | | - const fixture = fixtures.path('test-runner', 'coverage.js'); | 60 | | - const args = ['--experimental-test-coverage', fixture]; | 61 | | - const result = spawnSync(process.execPath, args); | | 56 | + return report; | | 57 | +} | 62 | 58 |
| 63 | | - assert(!result.stdout.toString().includes('# start of coverage report')); | 64 | | - assert(result.stderr.toString().includes('coverage could not be collected')); | 65 | | - assert.strictEqual(result.status, 0); | 66 | | - assert(!findCoverageFileForPid(result.pid)); | 67 | | -}); | | 59 | +test('test coverage report', async (t) => { | | 60 | + await t.test('--experimental-test-coverage and --test cannot be combined', () => { | | 61 | + // TODO(cjihrig): This test can be removed once multi-process code coverage | | 62 | + // is supported. | | 63 | + const args = ['--test', '--experimental-test-coverage']; | | 64 | + const result = spawnSync(process.execPath, args); | | 65 | + | | 66 | + // 9 is the documented exit code for an invalid CLI argument. | | 67 | + assert.strictEqual(result.status, 9); | | 68 | + assert.match( | | 69 | + result.stderr.toString(), | | 70 | + /--experimental-test-coverage cannot be used with --test/ | | 71 | + ); | | 72 | + }); | 68 | 73 |
| 69 | | -test('coverage is reported and dumped to NODE_V8_COVERAGE if present', (t) => { | 70 | | - if (!process.features.inspector) { | 71 | | - return; | 72 | | - } | | 74 | + await t.test('handles the inspector not being available', (t) => { | | 75 | + if (process.features.inspector) { | | 76 | + return; | | 77 | + } | 73 | 78 |
| 74 | | - const fixture = fixtures.path('test-runner', 'coverage.js'); | 75 | | - const args = ['--experimental-test-coverage', fixture]; | 76 | | - const options = { env: { ...process.env, NODE_V8_COVERAGE: tmpdir.path } }; | 77 | | - const result = spawnSync(process.execPath, args, options); | 78 | | - const report = getCoverageFixtureReport(); | | 79 | + const fixture = fixtures.path('test-runner', 'coverage.js'); | | 80 | + const args = ['--experimental-test-coverage', fixture]; | | 81 | + const result = spawnSync(process.execPath, args); | 79 | 82 |
| 80 | | - assert(result.stdout.toString().includes(report)); | 81 | | - assert.strictEqual(result.stderr.toString(), ''); | 82 | | - assert.strictEqual(result.status, 0); | 83 | | - assert(findCoverageFileForPid(result.pid)); | | 83 | + assert(!result.stdout.toString().includes('# start of coverage report')); | | 84 | + assert(result.stderr.toString().includes('coverage could not be collected')); | | 85 | + assert.strictEqual(result.status, 0); | | 86 | + assert(!findCoverageFileForPid(result.pid)); | | 87 | + }); | 84 | 88 | }); | 85 | 89 |
| 86 | | -test('coverage is reported without NODE_V8_COVERAGE present', (t) => { | 87 | | - if (!process.features.inspector) { | 88 | | - return; | 89 | | - } | | 90 | +test('test tap coverage reporter', async (t) => { | | 91 | + await t.test('coverage is reported and dumped to NODE_V8_COVERAGE if present', (t) => { | | 92 | + if (!process.features.inspector) { | | 93 | + return; | | 94 | + } | | 95 | + | | 96 | + const fixture = fixtures.path('test-runner', 'coverage.js'); | | 97 | + const args = ['--experimental-test-coverage', '--test-reporter', 'tap', fixture]; | | 98 | + const options = { env: { ...process.env, NODE_V8_COVERAGE: tmpdir.path } }; | | 99 | + const result = spawnSync(process.execPath, args, options); | | 100 | + const report = getTapCoverageFixtureReport(); | | 101 | + | | 102 | + assert(result.stdout.toString().includes(report)); | | 103 | + assert.strictEqual(result.stderr.toString(), ''); | | 104 | + assert.strictEqual(result.status, 0); | | 105 | + assert(findCoverageFileForPid(result.pid)); | | 106 | + }); | | 107 | + | | 108 | + await t.test('coverage is reported without NODE_V8_COVERAGE present', (t) => { | | 109 | + if (!process.features.inspector) { | | 110 | + return; | | 111 | + } | | 112 | + | | 113 | + const fixture = fixtures.path('test-runner', 'coverage.js'); | | 114 | + const args = ['--experimental-test-coverage', '--test-reporter', 'tap', fixture]; | | 115 | + const result = spawnSync(process.execPath, args); | | 116 | + const report = getTapCoverageFixtureReport(); | 90 | 117 |
| 91 | | - const fixture = fixtures.path('test-runner', 'coverage.js'); | 92 | | - const args = ['--experimental-test-coverage', fixture]; | 93 | | - const result = spawnSync(process.execPath, args); | 94 | | - const report = getCoverageFixtureReport(); | | 118 | + assert(result.stdout.toString().includes(report)); | | 119 | + assert.strictEqual(result.stderr.toString(), ''); | | 120 | + assert.strictEqual(result.status, 0); | | 121 | + assert(!findCoverageFileForPid(result.pid)); | | 122 | + }); | | 123 | +}); | 95 | 124 |
| 96 | | - assert(result.stdout.toString().includes(report)); | 97 | | - assert.strictEqual(result.stderr.toString(), ''); | 98 | | - assert.strictEqual(result.status, 0); | 99 | | - assert(!findCoverageFileForPid(result.pid)); | | 125 | +test('test spec coverage reporter', async (t) => { | | 126 | + await t.test('coverage is reported and dumped to NODE_V8_COVERAGE if present', (t) => { | | 127 | + if (!process.features.inspector) { | | 128 | + return; | | 129 | + } | | 130 | + const fixture = fixtures.path('test-runner', 'coverage.js'); | | 131 | + const args = ['--experimental-test-coverage', '--test-reporter', 'spec', fixture]; | | 132 | + const options = { env: { ...process.env, NODE_V8_COVERAGE: tmpdir.path } }; | | 133 | + const result = spawnSync(process.execPath, args, options); | | 134 | + const report = getSpecCoverageFixtureReport(); | | 135 | + | | 136 | + assert(result.stdout.toString().includes(report)); | | 137 | + assert.strictEqual(result.stderr.toString(), ''); | | 138 | + assert.strictEqual(result.status, 0); | | 139 | + assert(findCoverageFileForPid(result.pid)); | | 140 | + }); | | 141 | + | | 142 | + await t.test('coverage is reported without NODE_V8_COVERAGE present', (t) => { | | 143 | + if (!process.features.inspector) { | | 144 | + return; | | 145 | + } | | 146 | + const fixture = fixtures.path('test-runner', 'coverage.js'); | | 147 | + const args = ['--experimental-test-coverage', '--test-reporter', 'spec', fixture]; | | 148 | + const result = spawnSync(process.execPath, args); | | 149 | + const report = getSpecCoverageFixtureReport(); | | 150 | + | | 151 | + assert(result.stdout.toString().includes(report)); | | 152 | + assert.strictEqual(result.stderr.toString(), ''); | | 153 | + assert.strictEqual(result.status, 0); | | 154 | + assert(!findCoverageFileForPid(result.pid)); | | 155 | + }); | 100 | 156 | }); |
|