diff --git a/doc/api/errors.md b/doc/api/errors.md index 78456d0d28e0f0..b696a838598573 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -2049,6 +2049,12 @@ type for one of its returned object properties on execution. Thrown in case a function option does not return an expected value type on execution, such as when a function is expected to return a promise. + + +### `ERR_INVALID_SOURCE_MAP` + +The source map cannot be parsed because it is invalid. + ### `ERR_INVALID_STATE` diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 87f7b1da094d93..79e4010e7f184c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1517,6 +1517,7 @@ E('ERR_INVALID_RETURN_VALUE', (input, name, value) => { return `Expected ${input} to be returned from the "${name}"` + ` function but got ${type}.`; }, TypeError, RangeError); +E('ERR_INVALID_SOURCE_MAP', `Invalid source map for '%s'`, Error); E('ERR_INVALID_STATE', 'Invalid state: %s', Error, TypeError, RangeError); E('ERR_INVALID_SYNC_FORK_INPUT', 'Asynchronous forks do not support ' + diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 7ef57020728302..f6567054075f63 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -29,6 +29,11 @@ const { setupCoverageHooks } = require('internal/util'); const { tmpdir } = require('os'); const { join, resolve, relative, matchesGlob } = require('path'); const { fileURLToPath } = require('internal/url'); +const { + codes: { + ERR_INVALID_SOURCE_MAP, + }, +} = require('internal/errors'); const { kMappings, SourceMap } = require('internal/source_map/source_map'); const kCoverageFileRegex = /^coverage-(\d+)-(\d{13})-(\d+)\.json$/; const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; @@ -347,67 +352,73 @@ class TestCoverage { newResult.set(url, script); continue; } - const { data, lineLengths } = sourceMapCache[url]; - let offset = 0; - const executedLines = ArrayPrototypeMap(lineLengths, (length, i) => { - const coverageLine = new CoverageLine(i + 1, offset, null, length + 1); - offset += length + 1; - return coverageLine; - }); - if (data.sourcesContent != null) { - for (let j = 0; j < data.sources.length; ++j) { - this.getLines(data.sources[j], data.sourcesContent[j]); - } - } - const sourceMap = new SourceMap(data, { __proto__: null, lineLengths }); - for (let j = 0; j < functions.length; ++j) { - const { ranges, functionName, isBlockCoverage } = functions[j]; - if (ranges == null) { - continue; - } - let newUrl; - const newRanges = []; - for (let k = 0; k < ranges.length; ++k) { - const { startOffset, endOffset, count } = ranges[k]; - const { lines } = mapRangeToLines(ranges[k], executedLines); - - let startEntry = sourceMap - .findEntry(lines[0].line - 1, MathMax(0, startOffset - lines[0].startOffset)); - const endEntry = sourceMap - .findEntry(lines[lines.length - 1].line - 1, (endOffset - lines[lines.length - 1].startOffset) - 1); - if (!startEntry.originalSource && endEntry.originalSource && - lines[0].line === 1 && startOffset === 0 && lines[0].startOffset === 0) { - // Edge case when the first line is not mappable - const { 2: originalSource, 3: originalLine, 4: originalColumn } = sourceMap[kMappings][0]; - startEntry = { __proto__: null, originalSource, originalLine, originalColumn }; + try { + const { data, lineLengths } = sourceMapCache[url]; + let offset = 0; + const executedLines = ArrayPrototypeMap(lineLengths, (length, i) => { + const coverageLine = new CoverageLine(i + 1, offset, null, length + 1); + offset += length + 1; + return coverageLine; + }); + if (data.sourcesContent != null) { + for (let j = 0; j < data.sources.length; ++j) { + this.getLines(data.sources[j], data.sourcesContent[j]); } + } + const sourceMap = new SourceMap(data, { __proto__: null, lineLengths }); - if (!startEntry.originalSource || startEntry.originalSource !== endEntry.originalSource) { - // The range is not mappable. Skip it. + for (let j = 0; j < functions.length; ++j) { + const { ranges, functionName, isBlockCoverage } = functions[j]; + if (ranges == null) { continue; } + let newUrl; + const newRanges = []; + for (let k = 0; k < ranges.length; ++k) { + const { startOffset, endOffset, count } = ranges[k]; + const { lines } = mapRangeToLines(ranges[k], executedLines); + + let startEntry = sourceMap + .findEntry(lines[0].line - 1, MathMax(0, startOffset - lines[0].startOffset)); + const endEntry = sourceMap + .findEntry(lines[lines.length - 1].line - 1, (endOffset - lines[lines.length - 1].startOffset) - 1); + if (!startEntry.originalSource && endEntry.originalSource && + lines[0].line === 1 && startOffset === 0 && lines[0].startOffset === 0) { + // Edge case when the first line is not mappable + const { 2: originalSource, 3: originalLine, 4: originalColumn } = sourceMap[kMappings][0]; + startEntry = { __proto__: null, originalSource, originalLine, originalColumn }; + } - newUrl ??= startEntry?.originalSource; - const mappedLines = this.getLines(newUrl); - const mappedStartOffset = this.entryToOffset(startEntry, mappedLines); - const mappedEndOffset = this.entryToOffset(endEntry, mappedLines) + 1; - for (let l = startEntry.originalLine; l <= endEntry.originalLine; l++) { - mappedLines[l].count = count; - } + if (!startEntry.originalSource || startEntry.originalSource !== endEntry.originalSource) { + // The range is not mappable. Skip it. + continue; + } - ArrayPrototypePush(newRanges, { - __proto__: null, startOffset: mappedStartOffset, endOffset: mappedEndOffset, count, - }); - } + newUrl ??= startEntry?.originalSource; + const mappedLines = this.getLines(newUrl); + const mappedStartOffset = this.entryToOffset(startEntry, mappedLines); + const mappedEndOffset = this.entryToOffset(endEntry, mappedLines) + 1; + for (let l = startEntry.originalLine; l <= endEntry.originalLine; l++) { + mappedLines[l].count = count; + } - if (!newUrl) { - // No mappable ranges. Skip the function. - continue; + ArrayPrototypePush(newRanges, { + __proto__: null, startOffset: mappedStartOffset, endOffset: mappedEndOffset, count, + }); + } + + if (!newUrl) { + // No mappable ranges. Skip the function. + continue; + } + const newScript = newResult.get(newUrl) ?? { __proto__: null, url: newUrl, functions: [] }; + ArrayPrototypePush(newScript.functions, + { __proto__: null, functionName, ranges: newRanges, isBlockCoverage }); + newResult.set(newUrl, newScript); } - const newScript = newResult.get(newUrl) ?? { __proto__: null, url: newUrl, functions: [] }; - ArrayPrototypePush(newScript.functions, { __proto__: null, functionName, ranges: newRanges, isBlockCoverage }); - newResult.set(newUrl, newScript); + } catch { + throw new ERR_INVALID_SOURCE_MAP(url); } } diff --git a/test/fixtures/test-runner/source-map-invalid/index.js b/test/fixtures/test-runner/source-map-invalid/index.js new file mode 100644 index 00000000000000..4bf9eabc05b947 --- /dev/null +++ b/test/fixtures/test-runner/source-map-invalid/index.js @@ -0,0 +1 @@ +//# sourceMappingURL=index.mjs.map \ No newline at end of file diff --git a/test/fixtures/test-runner/source-map-invalid/index.js.map b/test/fixtures/test-runner/source-map-invalid/index.js.map new file mode 100644 index 00000000000000..e466dcbd8e8f2b --- /dev/null +++ b/test/fixtures/test-runner/source-map-invalid/index.js.map @@ -0,0 +1 @@ +invalid \ No newline at end of file diff --git a/test/parallel/test-runner-coverage.js b/test/parallel/test-runner-coverage.js index ba767283e672c4..101fbe2db7aa80 100644 --- a/test/parallel/test-runner-coverage.js +++ b/test/parallel/test-runner-coverage.js @@ -6,6 +6,7 @@ const { readdirSync } = require('node:fs'); const { test } = require('node:test'); const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); +const { pathToFileURL } = require('node:url'); const skipIfNoInspector = { skip: !process.features.inspector ? 'inspector disabled' : false }; @@ -486,6 +487,22 @@ test('coverage with included and excluded files', skipIfNoInspector, () => { assert(!findCoverageFileForPid(result.pid)); }); +test('throws when an invalid source map is used', skipIfNoInspector, () => { + const fixture = fixtures.path('test-runner', 'source-map-invalid', 'index.js'); + const args = [ + '--test', + '--enable-source-maps', + '--experimental-test-coverage', + '--test-reporter', 'tap', + fixture, + ]; + + const result = spawnSync(process.execPath, args); + assert.strictEqual(result.stderr.toString(), ''); + assert(result.stdout.toString().includes(`Invalid source map for '${pathToFileURL(fixture)}'`)); + assert.strictEqual(result.status, 1); +}); + test('properly accounts for line endings in source maps', skipIfNoInspector, () => { const fixture = fixtures.path('test-runner', 'source-map-line-lengths', 'index.js'); const args = [