Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add source map spec tests #505

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "test/source-map-tests"]
path = test/source-map-tests
url = https://github.com/takikawa/source-map-tests.git
4 changes: 3 additions & 1 deletion lib/source-map-consumer.js
Original file line number Diff line number Diff line change
Expand Up @@ -842,8 +842,10 @@ class IndexedSourceMapConsumer extends SourceMapConsumer {
return cmp;
}

// The needle column is 0-based, but the section offset column is
// stored 1-based.
return (
aNeedle.generatedColumn - section.generatedOffset.generatedColumn
aNeedle.generatedColumn + 1 - section.generatedOffset.generatedColumn
);
}
);
Expand Down
1 change: 1 addition & 0 deletions test/source-map-tests
Submodule source-map-tests added at ec5a47
9 changes: 9 additions & 0 deletions test/test-source-map-consumer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2183,3 +2183,12 @@ exports["test SourceMapConsumer.with and exceptions"] = async function (
assert.equal(error, 6);
assert.equal(consumer._mappingsPtr, 0);
};

exports["test a mapping at the boundary of indexed source map offset"] =
async function (assert) {
const map = await new SourceMapConsumer(
util.indexedTestMapAtOffsetBoundary
);
util.assertMapping(1, 0, "/the/root/one.js", 1, 0, null, null, map, assert);
map.destroy();
};
141 changes: 141 additions & 0 deletions test/test-spec-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2024 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/

const fs = require('node:fs/promises');
const SourceMapConsumer =
require("../lib/source-map-consumer").SourceMapConsumer;

const sourceMapSpecTests = require("./source-map-tests/source-map-spec-tests.json");

async function readJSON(path) {
const file = await fs.open(require.resolve(path));
const json = JSON.parse(await file.readFile());
file.close();
return json;
}

// Known failures due to intentional implementation choices or due to bugs.
const skippedTests = [
// Versions are explicitly checked a bit loosely.
"versionNumericString",
// Stricter sources array checking isn't implemented.
"sourcesNotStringOrNull",
"sourcesAndSourcesContentBothNull",
// Stricter names array checking isn't implemented.
"namesMissing",
"namesNotString",
// This check isn't as strict in this library.
"invalidMappingNotAString1",
// A mapping segment with no fields is technically invalid in the spec.
"invalidMappingSegmentWithZeroFields",
// These tests fail due to imprecision in the spec about the 32-bit limit.
"invalidMappingSegmentWithColumnExceeding32Bits",
"invalidMappingSegmentWithOriginalLineExceeding32Bits",
"invalidMappingSegmentWithOriginalColumnExceeding32Bits",
// A large VLQ that should parse, but currently does not.
"validMappingLargeVLQ",
// The library currently doesn't check the types of offset lines/columns.
"indexMapOffsetLineWrongType",
"indexMapOffsetColumnWrongType",
// The spec is not totally clear about this case.
"indexMapInvalidBaseMappings",
// The spec's definition of overlap can be refined
"indexMapInvalidOverlap",
]

// The source-map library converts null sources to the "null" URL in its
// sources list, so for equality checking we accept this as null.
function nullish(nullOrString) {
if (nullOrString === "null") {
return null;
}
return nullOrString;
}

async function testMappingAction(assert, rawSourceMap, action) {
return SourceMapConsumer.with(rawSourceMap, null, (consumer) => {
mappedPosition = consumer.originalPositionFor({
line: action.generatedLine + 1,
column: action.generatedColumn,
});

assert.equal(mappedPosition.line, action.originalLine + 1, `original line didn't match, expected ${action.originalLine + 1} got ${mappedPosition.line}`);
assert.equal(mappedPosition.column, action.originalColumn, `original column didn't match, expected ${action.originalColumn} got ${mappedPosition.column}`);
assert.equal(nullish(mappedPosition.source), action.originalSource, `original source didn't match, expected ${action.originalSource} got ${mappedPosition.source}`);
if (action.mappedName)
assert.equal(mappedPosition.name, action.mappedName, `mapped name didn't match, expected ${action.mappedName} got ${mappedPosition.name}`);

// When the source is null, a reverse lookup may not make sense
// because there isn't a unique way to look it up.
if (action.originalSource !== null) {
let mappedPosition = consumer.generatedPositionFor({
source: action.originalSource,
line: action.originalLine + 1,
column: action.originalColumn
});

assert.equal(mappedPosition.line, action.generatedLine + 1, `generated line didn't match, expected ${action.generatedLine + 1} got ${mappedPosition.line}`);
assert.equal(mappedPosition.column, action.generatedColumn, `generated column didn't match, expected ${action.generatedColumn} got ${mappedPosition.column}`);
}

});
}

async function testTransitiveMappingAction(assert, rawSourceMap, action) {
return SourceMapConsumer.with(rawSourceMap, null, async (consumer) => {
assert.ok(Array.isArray(action.intermediateMaps), "transitive mapping case requires intermediate maps");

let mappedPosition = consumer.originalPositionFor({
line: action.generatedLine + 1,
column: action.generatedColumn,
});

for (let intermediateMapPath of action.intermediateMaps) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (let intermediateMapPath of action.intermediateMaps) {
for (const intermediateMapPath of action.intermediateMaps) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since intermediateMapPath is not reassigned

const intermediateMap = await readJSON(`./source-map-tests/resources/${intermediateMapPath}`);
await SourceMapConsumer.with(intermediateMap, null, (consumer) => {
mappedPosition = consumer.originalPositionFor({
line: mappedPosition.line,
column: mappedPosition.column,
});
});
}

assert.equal(mappedPosition.line, action.originalLine + 1, `original line didn't match, expected ${action.originalLine + 1} got ${mappedPosition.line}`);
assert.equal(mappedPosition.column, action.originalColumn, `original column didn't match, expected ${action.originalColumn} got ${mappedPosition.column}`);
assert.equal(mappedPosition.source, action.originalSource, `original source didn't match, expected ${action.originalSource} got ${mappedPosition.source}`);
});
}

for (let testCase of sourceMapSpecTests.tests) {
if (skippedTests.includes(testCase.name))
continue;
exports[`test from source map spec tests, name: ${testCase.name}`] =
async function (assert) {
const json = await readJSON(`./source-map-tests/resources/${testCase.sourceMapFile}`);
let sourceMapFailed = false;
Copy link
Collaborator

@bomsy bomsy May 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seem to be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching that, I think it was a vestige of an earlier attempt. I'll remove it next time I update the patch.

try {
const map = await new SourceMapConsumer(json);
map.eachMapping(() => {});
map.destroy();
} catch (exn) {
if (testCase.sourceMapIsValid)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (testCase.sourceMapIsValid)
if (testCase.sourceMapShouldBeValid)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related directly to this patch but it just a tiny suggestion while trying the understanding the code.
sourceMapShouldBeValid might make it clearer what the expectation is.

assert.fail("Expected valid source map but failed to load successfully: " + exn.message);
return;
}
if (!testCase.sourceMapIsValid)
assert.fail("Expected invalid source map but loaded successfully");
if (testCase.testActions) {
for (let testAction of testCase.testActions) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (let testAction of testCase.testActions) {
for (const testAction of testCase.testActions) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could use const since testAction is not reassigned.

if (testAction.actionType == "checkMapping") {
await testMappingAction(assert, json, testAction);
} else if (testAction.actionType == "checkMappingTransitive") {
await testTransitiveMappingAction(assert, json, testAction);
}
}
}
};
};
25 changes: 25 additions & 0 deletions test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,31 @@ exports.indexedTestMapColumnOffset = {
},
],
};
// This mapping is for testing a case where the mapped position is at the
// section offset.
exports.indexedTestMapAtOffsetBoundary = {
version: 3,
file: "min.js",
sections: [
{
offset: {
line: 0,
column: 0,
},
map: {
version: 3,
sources: ["one.js"],
sourcesContent: [
"ONE.foo = function (bar) {\n return baz(bar);\n };",
],
names: ["bar", "baz"],
mappings: "AAAA",
file: "min.js",
sourceRoot: "/the/root",
},
},
],
};
exports.testMapWithSourcesContent = {
version: 3,
file: "min.js",
Expand Down