Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 84 additions & 37 deletions napi/parser/test/parse-raw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { parseSync } from '../index.js';
const TARGET_DIR_PATH = pathJoin(import.meta.dirname, '../../../target');
const TEST262_DIR_PATH = pathJoin(import.meta.dirname, '../../../tasks/coverage/test262/test');
const ACORN_TEST262_DIR_PATH = pathJoin(import.meta.dirname, '../../../tasks/coverage/acorn-test262/test');
const ESTREE_SNAPSHOT_PATH = pathJoin(import.meta.dirname, '../../../tasks/coverage/snapshots/estree_test262.snap');

const INFINITY_PLACEHOLDER = '__INFINITY__INFINITY__INFINITY__';
const INFINITY_REGEXP = new RegExp(`"${INFINITY_PLACEHOLDER}"`, 'g');

// Load/download fixtures.
// Save in `target` directory, same as where benchmarks store them.
Expand Down Expand Up @@ -38,64 +42,107 @@ const benchFixtures = await Promise.all(benchFixtureUrls.map(async (url) => {
return [filename, sourceText];
}));

// Only test Test262 fixtures which Acorn is able to parse
const test262FixturePaths = (await readdir(ACORN_TEST262_DIR_PATH, { recursive: true }))
.filter(path => path.endsWith('.json'))
.map(path => path.slice(0, -2));
// Only test Test262 fixtures which Acorn is able to parse.
// Skip tests which we know we can't pass (listed as failing in `ESTree` snapshot file),
// and skip tests related to hashbangs (where output is correct, but Acorn doesn't parse hashbangs).
const SNAPSHOT_FAIL_PREFIX = 'Mismatch: tasks/coverage/test262/test/';
const snapshotFailPaths = new Set(
(await readFile(ESTREE_SNAPSHOT_PATH, 'utf8'))
.split('\n')
.filter(line => line.startsWith(SNAPSHOT_FAIL_PREFIX))
.map(line => line.slice(SNAPSHOT_FAIL_PREFIX.length)),
);

const test262FixturePaths = [];
for (let path of await readdir(ACORN_TEST262_DIR_PATH, { recursive: true })) {
if (!path.endsWith('.json')) continue;
path = path.slice(0, -2);
if (snapshotFailPaths.has(path) || path.startsWith('language/comments/hashbang/')) continue;
test262FixturePaths.push(path);
}

// Test raw transfer output matches standard (via JSON) output for some large files
describe('fixtures', () => {
it.each(benchFixtures)('%s', testRaw, { timeout: 10000 });
it.each(benchFixtures)('%s', (filename, sourceText) => {
const retStandard = parseSync(filename, sourceText);
const { program: programStandard, comments: commentsStandard, module: moduleStandard, errors: errorsStandard } =
retStandard;

// @ts-ignore
const retRaw = parseSync(filename, sourceText, { experimentalRawTransfer: true });
const { program: programRaw, comments: commentsRaw } = retRaw;
// Remove `null` values, to match what NAPI-RS does
const moduleRaw = clean(retRaw.module);
const errorsRaw = clean(retRaw.errors);

// Compare as objects
expect(programRaw).toEqual(programStandard);
expect(commentsRaw).toEqual(commentsStandard);
expect(moduleRaw).toEqual(moduleStandard);
expect(errorsRaw).toEqual(errorsStandard);

// Compare as JSON (to ensure same field order)
const jsonStandard = stringify({
program: programStandard,
comments: commentsStandard,
module: moduleStandard,
errors: errorsStandard,
});
const jsonRaw = stringify({ program: programRaw, comments: commentsRaw, module: moduleRaw, errors: errorsRaw });
expect(jsonRaw).toEqual(jsonStandard);
});
});

// Test raw transfer output matches standard (via JSON) output for Test262 test cases
describe('test262', () => {
it.each(test262FixturePaths)('%s', async (path) => {
const filename = basename(path);
const sourceText = await readFile(pathJoin(TEST262_DIR_PATH, path), 'utf8');
testRaw(filename, sourceText);
});
});
const [sourceText, acornJson] = await Promise.all([
readFile(pathJoin(TEST262_DIR_PATH, path), 'utf8'),
readFile(pathJoin(ACORN_TEST262_DIR_PATH, `${path}on`), 'utf8'),
]);

function testRaw(filename, sourceText) {
const retStandard = parseSync(filename, sourceText);
const { program: programStandard, comments: commentsStandard, module: moduleStandard, errors: errorsStandard } =
retStandard;

// @ts-ignore
const retRaw = parseSync(filename, sourceText, { experimentalRawTransfer: true });
const { program: programRaw, comments: commentsRaw } = retRaw;
// Remove `null` values, to match what NAPI-RS does
const moduleRaw = clean(retRaw.module);
const errorsRaw = clean(retRaw.errors);

// Compare as objects
expect(programRaw).toEqual(programStandard);
expect(commentsRaw).toEqual(commentsStandard);
expect(moduleRaw).toEqual(moduleStandard);
expect(errorsRaw).toEqual(errorsStandard);

// Compare as JSON (to ensure same field order)
const jsonStandard = stringify({
program: programStandard,
comments: commentsStandard,
module: moduleStandard,
errors: errorsStandard,
// Acorn JSON files always end with:
// ```
// "sourceType": "script",
// "hashbang": null
// }
// ```
// For speed, extract `sourceType` with a slice, rather than parsing the JSON.
const sourceType = acornJson.slice(-29, -23);

// @ts-ignore
const { program } = parseSync(filename, sourceText, { sourceType, experimentalRawTransfer: true });
const json = stringifyAcornTest262Style(program);
expect(json).toEqual(acornJson);
});
const jsonRaw = stringify({ program: programRaw, comments: commentsRaw, module: moduleRaw, errors: errorsRaw });
expect(jsonRaw).toEqual(jsonStandard);
}
});

// Stringify to JSON, removing values which are invalid in JSON
function stringify(obj) {
return JSON.stringify(obj, (_key, value) => {
if (typeof value === 'bigint') return `__BIGINT__: ${value}`;
if (typeof value === 'object' && value instanceof RegExp) return `__REGEXP__: ${value}`;
if (value === Infinity) return `__INFINITY__`;
return value;
});
}

// Stringify to JSON, removing values which are invalid in JSON,
// matching `acorn-test262` fixtures.
function stringifyAcornTest262Style(obj) {
let containsInfinity = false;
const json = JSON.stringify(obj, (_key, value) => {
if (typeof value === 'bigint' || (typeof value === 'object' && value instanceof RegExp)) return null;
if (value === Infinity) {
containsInfinity = true;
return INFINITY_PLACEHOLDER;
}
return value;
}, 2);

return containsInfinity ? json.replace(INFINITY_REGEXP, '1e+400') : json;
}

// Remove `null` values, to match what NAPI-RS does
function clean(obj) {
return JSON.parse(JSON.stringify(obj, (_key, value) => value === null ? undefined : value));
Expand Down
Loading