Skip to content

Commit 095e3e3

Browse files
committed
test_runner: throw on invalid source map
1 parent 059e08b commit 095e3e3

File tree

6 files changed

+89
-52
lines changed

6 files changed

+89
-52
lines changed

doc/api/errors.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,12 @@ type for one of its returned object properties on execution.
20492049
Thrown in case a function option does not return an expected value
20502050
type on execution, such as when a function is expected to return a promise.
20512051

2052+
<a id="ERR_INVALID_SOURCE_MAP"></a>
2053+
2054+
### `ERR_INVALID_SOURCE_MAP`
2055+
2056+
The source map cannot be parsed because it is invalid.
2057+
20522058
<a id="ERR_INVALID_STATE"></a>
20532059

20542060
### `ERR_INVALID_STATE`

lib/internal/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,7 @@ E('ERR_INVALID_RETURN_VALUE', (input, name, value) => {
15171517
return `Expected ${input} to be returned from the "${name}"` +
15181518
` function but got ${type}.`;
15191519
}, TypeError, RangeError);
1520+
E('ERR_INVALID_SOURCE_MAP', `Invalid source map for '%s'`, Error);
15201521
E('ERR_INVALID_STATE', 'Invalid state: %s', Error, TypeError, RangeError);
15211522
E('ERR_INVALID_SYNC_FORK_INPUT',
15221523
'Asynchronous forks do not support ' +

lib/internal/test_runner/coverage.js

Lines changed: 63 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ const { setupCoverageHooks } = require('internal/util');
2929
const { tmpdir } = require('os');
3030
const { join, resolve, relative, matchesGlob } = require('path');
3131
const { fileURLToPath } = require('internal/url');
32+
const {
33+
codes: {
34+
ERR_INVALID_SOURCE_MAP,
35+
},
36+
} = require('internal/errors');
3237
const { kMappings, SourceMap } = require('internal/source_map/source_map');
3338
const kCoverageFileRegex = /^coverage-(\d+)-(\d{13})-(\d+)\.json$/;
3439
const kIgnoreRegex = /\/\* node:coverage ignore next (?<count>\d+ )?\*\//;
@@ -347,67 +352,73 @@ class TestCoverage {
347352
newResult.set(url, script);
348353
continue;
349354
}
350-
const { data, lineLengths } = sourceMapCache[url];
351-
let offset = 0;
352-
const executedLines = ArrayPrototypeMap(lineLengths, (length, i) => {
353-
const coverageLine = new CoverageLine(i + 1, offset, null, length + 1);
354-
offset += length + 1;
355-
return coverageLine;
356-
});
357-
if (data.sourcesContent != null) {
358-
for (let j = 0; j < data.sources.length; ++j) {
359-
this.getLines(data.sources[j], data.sourcesContent[j]);
360-
}
361-
}
362-
const sourceMap = new SourceMap(data, { __proto__: null, lineLengths });
363355

364-
for (let j = 0; j < functions.length; ++j) {
365-
const { ranges, functionName, isBlockCoverage } = functions[j];
366-
if (ranges == null) {
367-
continue;
368-
}
369-
let newUrl;
370-
const newRanges = [];
371-
for (let k = 0; k < ranges.length; ++k) {
372-
const { startOffset, endOffset, count } = ranges[k];
373-
const { lines } = mapRangeToLines(ranges[k], executedLines);
374-
375-
let startEntry = sourceMap
376-
.findEntry(lines[0].line - 1, MathMax(0, startOffset - lines[0].startOffset));
377-
const endEntry = sourceMap
378-
.findEntry(lines[lines.length - 1].line - 1, (endOffset - lines[lines.length - 1].startOffset) - 1);
379-
if (!startEntry.originalSource && endEntry.originalSource &&
380-
lines[0].line === 1 && startOffset === 0 && lines[0].startOffset === 0) {
381-
// Edge case when the first line is not mappable
382-
const { 2: originalSource, 3: originalLine, 4: originalColumn } = sourceMap[kMappings][0];
383-
startEntry = { __proto__: null, originalSource, originalLine, originalColumn };
356+
try {
357+
const { data, lineLengths } = sourceMapCache[url];
358+
let offset = 0;
359+
const executedLines = ArrayPrototypeMap(lineLengths, (length, i) => {
360+
const coverageLine = new CoverageLine(i + 1, offset, null, length + 1);
361+
offset += length + 1;
362+
return coverageLine;
363+
});
364+
if (data.sourcesContent != null) {
365+
for (let j = 0; j < data.sources.length; ++j) {
366+
this.getLines(data.sources[j], data.sourcesContent[j]);
384367
}
368+
}
369+
const sourceMap = new SourceMap(data, { __proto__: null, lineLengths });
385370

386-
if (!startEntry.originalSource || startEntry.originalSource !== endEntry.originalSource) {
387-
// The range is not mappable. Skip it.
371+
for (let j = 0; j < functions.length; ++j) {
372+
const { ranges, functionName, isBlockCoverage } = functions[j];
373+
if (ranges == null) {
388374
continue;
389375
}
376+
let newUrl;
377+
const newRanges = [];
378+
for (let k = 0; k < ranges.length; ++k) {
379+
const { startOffset, endOffset, count } = ranges[k];
380+
const { lines } = mapRangeToLines(ranges[k], executedLines);
381+
382+
let startEntry = sourceMap
383+
.findEntry(lines[0].line - 1, MathMax(0, startOffset - lines[0].startOffset));
384+
const endEntry = sourceMap
385+
.findEntry(lines[lines.length - 1].line - 1, (endOffset - lines[lines.length - 1].startOffset) - 1);
386+
if (!startEntry.originalSource && endEntry.originalSource &&
387+
lines[0].line === 1 && startOffset === 0 && lines[0].startOffset === 0) {
388+
// Edge case when the first line is not mappable
389+
const { 2: originalSource, 3: originalLine, 4: originalColumn } = sourceMap[kMappings][0];
390+
startEntry = { __proto__: null, originalSource, originalLine, originalColumn };
391+
}
390392

391-
newUrl ??= startEntry?.originalSource;
392-
const mappedLines = this.getLines(newUrl);
393-
const mappedStartOffset = this.entryToOffset(startEntry, mappedLines);
394-
const mappedEndOffset = this.entryToOffset(endEntry, mappedLines) + 1;
395-
for (let l = startEntry.originalLine; l <= endEntry.originalLine; l++) {
396-
mappedLines[l].count = count;
397-
}
393+
if (!startEntry.originalSource || startEntry.originalSource !== endEntry.originalSource) {
394+
// The range is not mappable. Skip it.
395+
continue;
396+
}
398397

399-
ArrayPrototypePush(newRanges, {
400-
__proto__: null, startOffset: mappedStartOffset, endOffset: mappedEndOffset, count,
401-
});
402-
}
398+
newUrl ??= startEntry?.originalSource;
399+
const mappedLines = this.getLines(newUrl);
400+
const mappedStartOffset = this.entryToOffset(startEntry, mappedLines);
401+
const mappedEndOffset = this.entryToOffset(endEntry, mappedLines) + 1;
402+
for (let l = startEntry.originalLine; l <= endEntry.originalLine; l++) {
403+
mappedLines[l].count = count;
404+
}
403405

404-
if (!newUrl) {
405-
// No mappable ranges. Skip the function.
406-
continue;
406+
ArrayPrototypePush(newRanges, {
407+
__proto__: null, startOffset: mappedStartOffset, endOffset: mappedEndOffset, count,
408+
});
409+
}
410+
411+
if (!newUrl) {
412+
// No mappable ranges. Skip the function.
413+
continue;
414+
}
415+
const newScript = newResult.get(newUrl) ?? { __proto__: null, url: newUrl, functions: [] };
416+
ArrayPrototypePush(newScript.functions,
417+
{ __proto__: null, functionName, ranges: newRanges, isBlockCoverage });
418+
newResult.set(newUrl, newScript);
407419
}
408-
const newScript = newResult.get(newUrl) ?? { __proto__: null, url: newUrl, functions: [] };
409-
ArrayPrototypePush(newScript.functions, { __proto__: null, functionName, ranges: newRanges, isBlockCoverage });
410-
newResult.set(newUrl, newScript);
420+
} catch {
421+
throw new ERR_INVALID_SOURCE_MAP(url);
411422
}
412423
}
413424

test/fixtures/test-runner/source-map-invalid/index.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/test-runner/source-map-invalid/index.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/parallel/test-runner-coverage.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { readdirSync } = require('node:fs');
66
const { test } = require('node:test');
77
const fixtures = require('../common/fixtures');
88
const tmpdir = require('../common/tmpdir');
9+
const { pathToFileURL } = require('node:url');
910
const skipIfNoInspector = {
1011
skip: !process.features.inspector ? 'inspector disabled' : false
1112
};
@@ -486,6 +487,22 @@ test('coverage with included and excluded files', skipIfNoInspector, () => {
486487
assert(!findCoverageFileForPid(result.pid));
487488
});
488489

490+
test('throws when an invalid source map is used', skipIfNoInspector, () => {
491+
const fixture = fixtures.path('test-runner', 'source-map-invalid', 'index.js');
492+
const args = [
493+
'--test',
494+
'--enable-source-maps',
495+
'--experimental-test-coverage',
496+
'--test-reporter', 'tap',
497+
fixture,
498+
];
499+
500+
const result = spawnSync(process.execPath, args);
501+
assert.strictEqual(result.stderr.toString(), '');
502+
assert(result.stdout.toString().includes(`Invalid source map for '${pathToFileURL(fixture)}'`));
503+
assert.strictEqual(result.status, 1);
504+
});
505+
489506
test('properly accounts for line endings in source maps', skipIfNoInspector, () => {
490507
const fixture = fixtures.path('test-runner', 'source-map-line-lengths', 'index.js');
491508
const args = [

0 commit comments

Comments
 (0)