diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 798e41f0ac4be6..cdbece1eeae7c3 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -33,6 +33,7 @@ const { fileURLToPath, URL } = require('internal/url'); const { kMappings, SourceMap } = require('internal/source_map/source_map'); const { codes: { + ERR_OPERATION_FAILED, ERR_SOURCE_MAP_CORRUPT, ERR_SOURCE_MAP_MISSING_SOURCE, }, @@ -402,7 +403,7 @@ class TestCoverage { } const coverageFile = join(this.coverageDirectory, entry.name); - const coverage = JSONParse(readFileSync(coverageFile, 'utf8')); + const coverage = readCoverageFile(coverageFile); this.mergeCoverage(result, this.mapCoverageWithSourceMap(coverage)); } @@ -587,6 +588,24 @@ class TestCoverage { } } +function readCoverageFile(coverageFile) { + const rawCoverage = readFileSync(coverageFile, 'utf8'); + + if (rawCoverage.length === 0) { + throw new ERR_OPERATION_FAILED(`coverage file is empty: ${coverageFile}`); + } + + try { + return JSONParse(rawCoverage); + } catch (err) { + const error = new ERR_OPERATION_FAILED( + `failed to parse coverage file ${coverageFile}: ${err.message}`, + ); + error.cause = err; + throw error; + } +} + function toPercentage(covered, total) { return total === 0 ? 100 : (covered / total) * 100; } diff --git a/test/parallel/test-runner-coverage.js b/test/parallel/test-runner-coverage.js index 5a8f3d743538cb..542078c93d5e45 100644 --- a/test/parallel/test-runner-coverage.js +++ b/test/parallel/test-runner-coverage.js @@ -77,6 +77,22 @@ function getSpecCoverageFixtureReport() { return report; } +function formatSpawnSyncResult(result) { + return [ + `status: ${result.status}`, + `signal: ${result.signal}`, + `stdout:\n${result.stdout}`, + `stderr:\n${result.stderr}`, + ].join('\n'); +} + +function assertIncludesReport(result, report) { + assert( + result.stdout.toString().includes(report), + formatSpawnSyncResult(result), + ); +} + test('test coverage report', async (t) => { await t.test('handles the inspector not being available', (t) => { if (process.features.inspector) { @@ -111,7 +127,7 @@ test('test tap coverage reporter', skipIfNoInspector, async (t) => { 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)); + assertIncludesReport(result, report); assert.strictEqual(result.stderr.toString(), ''); assert.strictEqual(result.status, 0); assert(findCoverageFileForPid(result.pid)); @@ -129,7 +145,7 @@ test('test tap coverage reporter', skipIfNoInspector, async (t) => { const result = spawnSync(process.execPath, args); const report = getTapCoverageFixtureReport(); - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.stderr.toString(), ''); assert.strictEqual(result.status, 0); assert(!findCoverageFileForPid(result.pid)); @@ -149,7 +165,7 @@ test('test spec coverage reporter', skipIfNoInspector, async (t) => { const result = spawnSync(process.execPath, args, options); const report = getSpecCoverageFixtureReport(); - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.stderr.toString(), ''); assert.strictEqual(result.status, 0); assert(findCoverageFileForPid(result.pid)); @@ -166,7 +182,7 @@ test('test spec coverage reporter', skipIfNoInspector, async (t) => { const result = spawnSync(process.execPath, args); const report = getSpecCoverageFixtureReport(); - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.stderr.toString(), ''); assert.strictEqual(result.status, 0); assert(!findCoverageFileForPid(result.pid)); @@ -187,7 +203,7 @@ test('single process coverage is the same with --test', skipIfNoInspector, () => const report = getTapCoverageFixtureReport(); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.status, 0); assert(!findCoverageFileForPid(result.pid)); }); @@ -226,7 +242,7 @@ test('coverage is combined for multiple processes', skipIfNoInspector, () => { }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.status, 0); }); @@ -268,7 +284,7 @@ test.skip('coverage works with isolation=none', skipIfNoInspector, common.mustCa }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.status, 0); }, 0)); @@ -285,6 +301,7 @@ test('coverage reports on lines, functions, and branches', skipIfNoInspector, as ]); assert.strictEqual(child.stderr.toString(), ''); const stdout = child.stdout.toString(); + assert.notStrictEqual(stdout, '', formatSpawnSyncResult(child)); const coverage = JSON.parse(stdout); await t.test('does not include node_modules', () => { @@ -366,7 +383,7 @@ test('coverage with ESM hook - source irrelevant', skipIfNoInspector, () => { const result = spawnSync(process.execPath, args, { cwd: fixture }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.status, 0); }); @@ -401,7 +418,7 @@ test('coverage with ESM hook - source transpiled', skipIfNoInspector, () => { const result = spawnSync(process.execPath, args, { cwd: fixture }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.status, 0); }); @@ -435,7 +452,7 @@ test('coverage with excluded files', skipIfNoInspector, () => { return report.replaceAll('/', '\\'); } - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.status, 0); assert(!findCoverageFileForPid(result.pid)); }); @@ -472,7 +489,7 @@ test('coverage with included files', skipIfNoInspector, () => { return report.replaceAll('/', '\\'); } - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.status, 0); assert(!findCoverageFileForPid(result.pid)); }); @@ -506,7 +523,7 @@ test('coverage with included and excluded files', skipIfNoInspector, () => { return report.replaceAll('/', '\\'); } - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.status, 0); assert(!findCoverageFileForPid(result.pid)); }); @@ -547,7 +564,7 @@ test('correctly prints the coverage report of files contained in parent director }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertIncludesReport(result, report); assert.strictEqual(result.status, 0); }); @@ -564,5 +581,5 @@ test('coverage with directory and file named "file"', skipIfNoInspector, () => { assert.strictEqual(result.stderr.toString(), ''); assert.strictEqual(result.status, 0); - assert(result.stdout.toString().includes('start of coverage report')); + assertIncludesReport(result, 'start of coverage report'); });