Skip to content

Commit

Permalink
test_runner: throw on invalid source map
Browse files Browse the repository at this point in the history
  • Loading branch information
RedYetiDev committed Sep 21, 2024
1 parent 059e08b commit 095e3e3
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 52 deletions.
6 changes: 6 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="ERR_INVALID_SOURCE_MAP"></a>

### `ERR_INVALID_SOURCE_MAP`

The source map cannot be parsed because it is invalid.

<a id="ERR_INVALID_STATE"></a>

### `ERR_INVALID_STATE`
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' +
Expand Down
115 changes: 63 additions & 52 deletions lib/internal/test_runner/coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (?<count>\d+ )?\*\//;
Expand Down Expand Up @@ -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);
}
}

Expand Down
1 change: 1 addition & 0 deletions test/fixtures/test-runner/source-map-invalid/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/test-runner/source-map-invalid/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions test/parallel/test-runner-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down Expand Up @@ -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 = [
Expand Down

0 comments on commit 095e3e3

Please sign in to comment.