From 095e3e3107e7920380167d817e2c27d421eeef8a Mon Sep 17 00:00:00 2001
From: RedYetiDev <38299977+RedYetiDev@users.noreply.github.com>
Date: Sat, 21 Sep 2024 16:59:29 -0400
Subject: [PATCH] test_runner: throw on invalid source map
---
doc/api/errors.md | 6 +
lib/internal/errors.js | 1 +
lib/internal/test_runner/coverage.js | 115 ++++++++++--------
.../test-runner/source-map-invalid/index.js | 1 +
.../source-map-invalid/index.js.map | 1 +
test/parallel/test-runner-coverage.js | 17 +++
6 files changed, 89 insertions(+), 52 deletions(-)
create mode 100644 test/fixtures/test-runner/source-map-invalid/index.js
create mode 100644 test/fixtures/test-runner/source-map-invalid/index.js.map
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 = [