| 'use strict'; |
| const common = require('../common'); |
| const assert = require('node:assert'); |
| const { spawnSync } = require('node:child_process'); |
| const { readdirSync } = require('node:fs'); |
| const { test } = require('node:test'); |
| const fixtures = require('../common/fixtures'); |
| const tmpdir = require('../common/tmpdir'); |
| const skipIfNoInspector = { |
| skip: !process.features.inspector ? 'inspector disabled' : false |
| }; |
| |
| tmpdir.refresh(); |
| |
| function findCoverageFileForPid(pid) { |
| const pattern = `^coverage\\-${pid}\\-(\\d{13})\\-(\\d+)\\.json$`; |
| const regex = new RegExp(pattern); |
| |
| return readdirSync(tmpdir.path).find((file) => { |
| return regex.test(file); |
| }); |
| } |
| |
| function getTapCoverageFixtureReport() { |
| |
| const report = [ |
| '# start of coverage report', |
| '# --------------------------------------------------------------------------------------------', |
| '# file | line % | branch % | funcs % | uncovered lines', |
| '# --------------------------------------------------------------------------------------------', |
| '# test | | | | ', |
| '# fixtures | | | | ', |
| '# test-runner | | | | ', |
| '# coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72', |
| '# invalid-tap.js | 100.00 | 100.00 | 100.00 | ', |
| '# v8-coverage | | | | ', |
| '# throw.js | 71.43 | 50.00 | 100.00 | 5-6', |
| '# --------------------------------------------------------------------------------------------', |
| '# all files | 78.35 | 43.75 | 60.00 | ', |
| '# --------------------------------------------------------------------------------------------', |
| '# end of coverage report', |
| ].join('\n'); |
| |
| |
| if (common.isWindows) { |
| return report.replaceAll('/', '\\'); |
| } |
| |
| return report; |
| } |
| |
| function getSpecCoverageFixtureReport() { |
| |
| const report = [ |
| '\u2139 start of coverage report', |
| '\u2139 --------------------------------------------------------------------------------------------', |
| '\u2139 file | line % | branch % | funcs % | uncovered lines', |
| '\u2139 --------------------------------------------------------------------------------------------', |
| '\u2139 test | | | | ', |
| '\u2139 fixtures | | | | ', |
| '\u2139 test-runner | | | | ', |
| '\u2139 coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72', |
| '\u2139 invalid-tap.js | 100.00 | 100.00 | 100.00 | ', |
| '\u2139 v8-coverage | | | | ', |
| '\u2139 throw.js | 71.43 | 50.00 | 100.00 | 5-6', |
| '\u2139 --------------------------------------------------------------------------------------------', |
| '\u2139 all files | 78.35 | 43.75 | 60.00 | ', |
| '\u2139 --------------------------------------------------------------------------------------------', |
| '\u2139 end of coverage report', |
| ].join('\n'); |
| |
| |
| if (common.isWindows) { |
| return report.replaceAll('/', '\\'); |
| } |
| |
| return report; |
| } |
| |
| test('test coverage report', async (t) => { |
| await t.test('handles the inspector not being available', (t) => { |
| if (process.features.inspector) { |
| return; |
| } |
| |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = ['--experimental-test-coverage', fixture]; |
| const result = spawnSync(process.execPath, args); |
| |
| assert(!result.stdout.toString().includes('# start of coverage report')); |
| assert(result.stderr.toString().includes('coverage could not be collected')); |
| assert.strictEqual(result.status, 0); |
| assert(!findCoverageFileForPid(result.pid)); |
| }); |
| }); |
| |
| test('test tap coverage reporter', skipIfNoInspector, async (t) => { |
| await t.test('coverage is reported and dumped to NODE_V8_COVERAGE if present', (t) => { |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = ['--experimental-test-coverage', '--test-reporter', 'tap', fixture]; |
| const options = { env: { ...process.env, NODE_V8_COVERAGE: tmpdir.path } }; |
| const result = spawnSync(process.execPath, args, options); |
| const report = getTapCoverageFixtureReport(); |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert.strictEqual(result.status, 0); |
| assert(findCoverageFileForPid(result.pid)); |
| }); |
| |
| await t.test('coverage is reported without NODE_V8_COVERAGE present', (t) => { |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = ['--experimental-test-coverage', '--test-reporter', 'tap', fixture]; |
| const result = spawnSync(process.execPath, args); |
| const report = getTapCoverageFixtureReport(); |
| |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert.strictEqual(result.status, 0); |
| assert(!findCoverageFileForPid(result.pid)); |
| }); |
| }); |
| |
| test('test spec coverage reporter', skipIfNoInspector, async (t) => { |
| await t.test('coverage is reported and dumped to NODE_V8_COVERAGE if present', (t) => { |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = ['--experimental-test-coverage', '--test-reporter', 'spec', fixture]; |
| const options = { env: { ...process.env, NODE_V8_COVERAGE: tmpdir.path } }; |
| const result = spawnSync(process.execPath, args, options); |
| const report = getSpecCoverageFixtureReport(); |
| |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert.strictEqual(result.status, 0); |
| assert(findCoverageFileForPid(result.pid)); |
| }); |
| |
| await t.test('coverage is reported without NODE_V8_COVERAGE present', (t) => { |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = ['--experimental-test-coverage', '--test-reporter', 'spec', fixture]; |
| const result = spawnSync(process.execPath, args); |
| const report = getSpecCoverageFixtureReport(); |
| |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert.strictEqual(result.status, 0); |
| assert(!findCoverageFileForPid(result.pid)); |
| }); |
| }); |
| |
| test('single process coverage is the same with --test', skipIfNoInspector, () => { |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = [ |
| '--test', '--experimental-test-coverage', '--test-reporter', 'tap', fixture, |
| ]; |
| const result = spawnSync(process.execPath, args); |
| const report = getTapCoverageFixtureReport(); |
| |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.status, 0); |
| assert(!findCoverageFileForPid(result.pid)); |
| }); |
| |
| test('coverage is combined for multiple processes', skipIfNoInspector, () => { |
| let report = [ |
| '# start of coverage report', |
| '# -------------------------------------------------------------------', |
| '# file | line % | branch % | funcs % | uncovered lines', |
| '# -------------------------------------------------------------------', |
| '# common.js | 89.86 | 62.50 | 100.00 | 8 13-14 18 34-35 53', |
| '# first.test.js | 83.33 | 100.00 | 50.00 | 5-6', |
| '# second.test.js | 100.00 | 100.00 | 100.00 | ', |
| '# third.test.js | 100.00 | 100.00 | 100.00 | ', |
| '# -------------------------------------------------------------------', |
| '# all files | 92.11 | 72.73 | 88.89 | ', |
| '# -------------------------------------------------------------------', |
| '# end of coverage report', |
| ].join('\n'); |
| |
| if (common.isWindows) { |
| report = report.replaceAll('/', '\\'); |
| } |
| |
| const fixture = fixtures.path('v8-coverage', 'combined_coverage'); |
| const args = [ |
| '--test', '--experimental-test-coverage', '--test-reporter', 'tap', |
| ]; |
| const result = spawnSync(process.execPath, args, { |
| env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path }, |
| cwd: fixture, |
| }); |
| |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.status, 0); |
| }); |
| |
| test.skip('coverage works with isolation=none', skipIfNoInspector, () => { |
| // There is a bug in coverage calculation. The branch % in the common.js |
| // fixture is different depending on the test isolation mode. The 'none' mode |
| // is closer to what c8 reports here, so the bug is likely in the code that |
| // merges reports from different processes. |
| let report = [ |
| '# start of coverage report', |
| '# -------------------------------------------------------------------', |
| '# file | line % | branch % | funcs % | uncovered lines', |
| '# -------------------------------------------------------------------', |
| '# common.js | 89.86 | 68.42 | 100.00 | 8 13-14 18 34-35 53', |
| '# first.test.js | 83.33 | 100.00 | 50.00 | 5-6', |
| '# second.test.js | 100.00 | 100.00 | 100.00 | ', |
| '# third.test.js | 100.00 | 100.00 | 100.00 | ', |
| '# -------------------------------------------------------------------', |
| '# all files | 92.11 | 76.00 | 88.89 | ', |
| '# -------------------------------------------------------------------', |
| '# end of coverage report', |
| ].join('\n'); |
| |
| if (common.isWindows) { |
| report = report.replaceAll('/', '\\'); |
| } |
| |
| const fixture = fixtures.path('v8-coverage', 'combined_coverage'); |
| const args = [ |
| '--test', '--experimental-test-coverage', '--test-reporter', 'tap', '--experimental-test-isolation=none', |
| ]; |
| const result = spawnSync(process.execPath, args, { |
| env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path }, |
| cwd: fixture, |
| }); |
| |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.status, 0); |
| }); |
| |
| test('coverage reports on lines, functions, and branches', skipIfNoInspector, async (t) => { |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const child = spawnSync(process.execPath, |
| ['--test', '--experimental-test-coverage', '--test-reporter', |
| fixtures.fileURL('test-runner/custom_reporters/coverage.mjs'), |
| fixture]); |
| assert.strictEqual(child.stderr.toString(), ''); |
| const stdout = child.stdout.toString(); |
| const coverage = JSON.parse(stdout); |
| |
| await t.test('does not include node_modules', () => { |
| assert.strictEqual(coverage.summary.files.length, 3); |
| const files = ['coverage.js', 'invalid-tap.js', 'throw.js']; |
| coverage.summary.files.forEach((file, index) => { |
| assert.ok(file.path.endsWith(files[index])); |
| }); |
| }); |
| |
| const file = coverage.summary.files[0]; |
| |
| await t.test('reports on function coverage', () => { |
| const uncalledFunction = file.functions.find((f) => f.name === 'uncalledTopLevelFunction'); |
| assert.strictEqual(uncalledFunction.count, 0); |
| assert.strictEqual(uncalledFunction.line, 16); |
| |
| const calledTwice = file.functions.find((f) => f.name === 'fnWithControlFlow'); |
| assert.strictEqual(calledTwice.count, 2); |
| assert.strictEqual(calledTwice.line, 35); |
| }); |
| |
| await t.test('reports on branch coverage', () => { |
| const uncalledBranch = file.branches.find((b) => b.line === 6); |
| assert.strictEqual(uncalledBranch.count, 0); |
| |
| const calledTwice = file.branches.find((b) => b.line === 35); |
| assert.strictEqual(calledTwice.count, 2); |
| }); |
| |
| await t.test('reports on line coverage', () => { |
| [ |
| { line: 36, count: 2 }, |
| { line: 37, count: 1 }, |
| { line: 38, count: 1 }, |
| { line: 39, count: 0 }, |
| { line: 40, count: 1 }, |
| { line: 41, count: 1 }, |
| { line: 42, count: 1 }, |
| { line: 43, count: 0 }, |
| { line: 44, count: 0 }, |
| ].forEach((line) => { |
| const testLine = file.lines.find((l) => l.line === line.line); |
| assert.strictEqual(testLine.count, line.count); |
| }); |
| }); |
| }); |
| |
| test('coverage with ESM hook - source irrelevant', skipIfNoInspector, () => { |
| let report = [ |
| '# start of coverage report', |
| '# ------------------------------------------------------------------', |
| '# file | line % | branch % | funcs % | uncovered lines', |
| '# ------------------------------------------------------------------', |
| '# hooks.mjs | 100.00 | 100.00 | 100.00 | ', |
| '# register-hooks.js | 100.00 | 100.00 | 100.00 | ', |
| '# virtual.js | 100.00 | 100.00 | 100.00 | ', |
| '# ------------------------------------------------------------------', |
| '# all files | 100.00 | 100.00 | 100.00 | ', |
| '# ------------------------------------------------------------------', |
| '# end of coverage report', |
| ].join('\n'); |
| |
| if (common.isWindows) { |
| report = report.replaceAll('/', '\\'); |
| } |
| |
| const fixture = fixtures.path('test-runner', 'coverage-loader'); |
| const args = [ |
| '--import', './register-hooks.js', '--test', '--experimental-test-coverage', '--test-reporter', 'tap', 'virtual.js', |
| ]; |
| const result = spawnSync(process.execPath, args, { cwd: fixture }); |
| |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.status, 0); |
| }); |
| |
| test('coverage with ESM hook - source transpiled', skipIfNoInspector, () => { |
| let report = [ |
| '# start of coverage report', |
| '# ------------------------------------------------------------------', |
| '# file | line % | branch % | funcs % | uncovered lines', |
| '# ------------------------------------------------------------------', |
| '# hooks.mjs | 100.00 | 100.00 | 100.00 | ', |
| '# register-hooks.js | 100.00 | 100.00 | 100.00 | ', |
| '# sum.test.ts | 100.00 | 100.00 | 100.00 | ', |
| '# sum.ts | 100.00 | 100.00 | 100.00 | ', |
| '# ------------------------------------------------------------------', |
| '# all files | 100.00 | 100.00 | 100.00 | ', |
| '# ------------------------------------------------------------------', |
| '# end of coverage report', |
| ].join('\n'); |
| |
| if (common.isWindows) { |
| report = report.replaceAll('/', '\\'); |
| } |
| |
| const fixture = fixtures.path('test-runner', 'coverage-loader'); |
| const args = [ |
| '--import', './register-hooks.js', '--test', '--experimental-test-coverage', |
| '--test-reporter', 'tap', 'sum.test.ts', |
| ]; |
| const result = spawnSync(process.execPath, args, { cwd: fixture }); |
| |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.status, 0); |
| }); |
| |
| test('coverage with excluded files', skipIfNoInspector, () => { |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = [ |
| '--experimental-test-coverage', '--test-reporter', 'tap', |
| '--test-coverage-exclude=test/*/test-runner/invalid-tap.js', |
| fixture]; |
| const result = spawnSync(process.execPath, args); |
| const report = [ |
| '# start of coverage report', |
| '# -----------------------------------------------------------------------------------------', |
| '# file | line % | branch % | funcs % | uncovered lines', |
| '# -----------------------------------------------------------------------------------------', |
| '# test | | | | ', |
| '# fixtures | | | | ', |
| '# test-runner | | | | ', |
| '# coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72', |
| '# v8-coverage | | | | ', |
| '# throw.js | 71.43 | 50.00 | 100.00 | 5-6', |
| '# -----------------------------------------------------------------------------------------', |
| '# all files | 78.13 | 40.00 | 60.00 | ', |
| '# -----------------------------------------------------------------------------------------', |
| '# end of coverage report', |
| ].join('\n'); |
| |
| |
| if (common.isWindows) { |
| return report.replaceAll('/', '\\'); |
| } |
| |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.status, 0); |
| assert(!findCoverageFileForPid(result.pid)); |
| }); |
| |
| test('coverage with included files', skipIfNoInspector, () => { |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = [ |
| '--experimental-test-coverage', '--test-reporter', 'tap', |
| '--test-coverage-include=test/fixtures/test-runner/coverage.js', |
| '--test-coverage-include=test/fixtures/v8-coverage/throw.js', |
| fixture, |
| ]; |
| const result = spawnSync(process.execPath, args); |
| const report = [ |
| '# start of coverage report', |
| '# -----------------------------------------------------------------------------------------', |
| '# file | line % | branch % | funcs % | uncovered lines', |
| '# -----------------------------------------------------------------------------------------', |
| '# test | | | | ', |
| '# fixtures | | | | ', |
| '# test-runner | | | | ', |
| '# coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72', |
| '# v8-coverage | | | | ', |
| '# throw.js | 71.43 | 50.00 | 100.00 | 5-6', |
| '# -----------------------------------------------------------------------------------------', |
| '# all files | 78.13 | 40.00 | 60.00 | ', |
| '# -----------------------------------------------------------------------------------------', |
| '# end of coverage report', |
| ].join('\n'); |
| |
| |
| if (common.isWindows) { |
| return report.replaceAll('/', '\\'); |
| } |
| |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.status, 0); |
| assert(!findCoverageFileForPid(result.pid)); |
| }); |
| |
| test('coverage with included and excluded files', skipIfNoInspector, () => { |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = [ |
| '--experimental-test-coverage', '--test-reporter', 'tap', |
| '--test-coverage-include=test/fixtures/test-runner/*.js', |
| '--test-coverage-exclude=test/fixtures/test-runner/*-tap.js', |
| fixture, |
| ]; |
| const result = spawnSync(process.execPath, args); |
| const report = [ |
| '# start of coverage report', |
| '# -----------------------------------------------------------------------------------------', |
| '# file | line % | branch % | funcs % | uncovered lines', |
| '# -----------------------------------------------------------------------------------------', |
| '# test | | | | ', |
| '# fixtures | | | | ', |
| '# test-runner | | | | ', |
| '# coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72', |
| '# -----------------------------------------------------------------------------------------', |
| '# all files | 78.65 | 38.46 | 60.00 | ', |
| '# -----------------------------------------------------------------------------------------', |
| '# end of coverage report', |
| ].join('\n'); |
| |
| |
| if (common.isWindows) { |
| return report.replaceAll('/', '\\'); |
| } |
| |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.status, 0); |
| assert(!findCoverageFileForPid(result.pid)); |
| }); |
| |
| test('correctly prints the coverage report of files contained in parent directories', skipIfNoInspector, () => { |
| let report = [ |
| '# start of coverage report', |
| '# --------------------------------------------------------------------------------------------', |
| '# file | line % | branch % | funcs % | uncovered lines', |
| '# --------------------------------------------------------------------------------------------', |
| '# .. | | | | ', |
| '# coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72', |
| '# invalid-tap.js | 100.00 | 100.00 | 100.00 | ', |
| '# .. | | | | ', |
| '# v8-coverage | | | | ', |
| '# throw.js | 71.43 | 50.00 | 100.00 | 5-6', |
| '# --------------------------------------------------------------------------------------------', |
| '# all files | 78.35 | 43.75 | 60.00 | ', |
| '# --------------------------------------------------------------------------------------------', |
| '# end of coverage report', |
| ].join('\n'); |
| |
| if (common.isWindows) { |
| report = report.replaceAll('/', '\\'); |
| } |
| const fixture = fixtures.path('test-runner', 'coverage.js'); |
| const args = [ |
| '--test', '--experimental-test-coverage', '--test-reporter', 'tap', fixture, |
| ]; |
| const result = spawnSync(process.execPath, args, { |
| env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path }, |
| cwd: fixtures.path('test-runner', 'coverage'), |
| }); |
| |
| assert.strictEqual(result.stderr.toString(), ''); |
| assert(result.stdout.toString().includes(report)); |
| assert.strictEqual(result.status, 0); |
| }); |