tenko
Version:
A "pixel perfect" 100% spec compliant ES2021 JavaScript parser written in JS.
434 lines (376 loc) • 17.8 kB
JavaScript
// Run this suite by `./t t` in the Tenko project root
// Requires a clone of https://github.com/tc39/test262.git into tenko/ignore/test262
import fs from 'fs';
import path from 'path';
import {
isCommentToken,
} from '../src/tokentype.mjs';
import {
ASSERT,
astToString,
} from "./utils.mjs";
import {
compareAcorn,
ignoreTest262Acorn,
processAcornResult,
} from './parse_acorn.mjs';
import {
compareBabel,
ignoreTest262Babel,
processBabelResult,
} from './parse_babel.mjs';
import {testPrinter} from "./run_printer.mjs";
// Lazy loaded
let GOAL_MODULE;
let GOAL_SCRIPT;
let COLLECT_TOKENS_ALL;
let COLLECT_TOKENS_TYPES;
let COLLECT_TOKENS_SOLID;
let COLLECT_TOKENS_NONE;
// node does not expose __dirname under module mode, but we can use import.meta to get it
let filePath = import.meta.url.replace(/^file:\/\//, '');
let dirname = path.dirname(filePath);
const USE_BUILD = process.argv.includes('-b');
if (USE_BUILD) console.log('-- Using prod build of Tenko');
const ACORN_AST = process.argv.includes('--acorn'); // run Tenko with babelCompat=true?
const COMPARE_ACORN = process.argv.includes('--test-acorn'); // compare Tenko output for each test with Acorn output?
const BABEL_AST = process.argv.includes('--babel'); // run Tenko with babelCompat=true?
const COMPARE_BABEL = process.argv.includes('--test-babel'); // compare Tenko output for each test with Babel output?
const TARGET_FILE = (process.argv.includes('-f') && process.argv[process.argv.indexOf('-f') + 1]) || '';
if (ACORN_AST) console.log('Generating an Acorn compatible AST for all tests');
if (BABEL_AST) console.log('Generating a Babel compatible AST for all tests');
if (COMPARE_ACORN) console.log('Comparing to Acorn output');
if (COMPARE_BABEL) console.log('Comparing to Babel output');
const TENKO_DEV_FILE = '../src/index.mjs';
const TENKO_PROD_FILE = '../build/tenko.prod.mjs';
const BOLD = '\x1b[;1;1m';
const BLINK = '\x1b[;5;1m';
const RED = '\x1b[31m';
const GREEN = '\x1b[32m';
const RESET = '\x1b[0m';
if (TARGET_FILE) {
if (!TARGET_FILE.startsWith('test262/') && !TARGET_FILE.startsWith('/')) {
console.error('\nThe path should be an absolute path or relative to test262 folder, so for example: `test262/test/language/white-space/string-space.js`');
process.exit(1);
}
console.log('\nFiltering for ' + TARGET_FILE + '\n');
}
// Placeholder...
let Tenko = function(){ throw new Error('not yet loaded through import...'); };
// cd tenko/ignore
// git clone https://github.com/tc39/test262.git
const PATH262 = path.join(dirname, './../ignore/test262/test');
let stdout = [];
function parse(input, strict, module, annexB) {
stdout = [];
return Tenko(
input,
{
goalMode: module ? GOAL_MODULE : GOAL_SCRIPT,
collectTokens: COLLECT_TOKENS_ALL,
strictMode: strict,
webCompat: !!annexB,
acornCompat: ACORN_AST,
babelCompat: BABEL_AST,
errorCodeFrame: true,
truncCodeFrame: false,
// Collect output but don't print it in case the retry fails
$log: (...a) => stdout.push(a),
$warn: (...a) => stdout.push(a),
$error: (...a) => stdout.push(a),
}
);
}
function read(path, file, onContent) {
let combo = path + file;
if (fs.statSync(combo).isFile()) {
if (file.slice(-3) === '.js') {
onContent(combo, fs.readFileSync(combo, 'utf8'));
}
} else {
fs.readdirSync(combo).forEach(s => read(combo + '/', s, onContent));
}
}
let counter = 0;
let compareDrops = new Set;
let compareFails = new Set;
let compareSkips = new Set;
function onRead(file, content) {
++counter;
// if (counter < 22000) return;
let displayFile = file.slice(path.resolve(dirname, '../ignore').length + 1);
if (displayFile.includes('FIXTURE')) return;
if (TARGET_FILE) {
if (displayFile !== TARGET_FILE) return;
console.log(BOLD, counter, RESET, 'Testing', BOLD, displayFile, RESET);
console.log('-> ' + file);
} else {
console.log(BOLD, counter, RESET, 'Testing', BOLD, displayFile, RESET);
// Whitelist, currently unused
switch (displayFile) {
// case 'test262/test/annexB/language/expressions/object/__proto__-duplicate.js':
// console.log(BOLD, 'SKIP', RESET, '(see test runner code for reasoning)');
// return;
}
}
ASSERT(content.includes('/*---') && content.includes('---*/'), 'missing test262 header', file);
let header = content.slice(content.indexOf('/*---'), content.indexOf('---*/') + 5);
// The header is yaml... ... Whatever, split all the things :)
let flags = header.match(/\n\s*flags: \[(.*?)\]/);
let features = header.match(/\n\s*features: \[(.*?)\]/);
features = features ? features[1].split(', ') : [];
flags = flags ? flags[1].split(/\s*,\s*/g) : [];
let negative = /\n\s*negative:/.test(header) && !/\n\s*phase:\s*runtime/.test(header) && !header.includes('phase: resolution');
let webcompat = file.includes('annexB');
if (features.includes('class-fields-public')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage ?: class-fields-public)');
} else if (features.includes('class-fields-private')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage ?: class-fields-private)');
} else if (features.includes('class-static-fields-public')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage ?: class-static-fields-public)');
} else if (features.includes('class-static-fields-private')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage ?: class-static-fields-private)');
} else if (features.includes('class-methods-private')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage 3: class-methods-private)');
} else if (features.includes('class-static-methods-private')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage 3?: class-static-methods-private)');
} else if (features.includes('hashbang')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage 3: hashbang)');
} else if (features.includes('import.meta')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage 3: import.meta)');
} else if (features.includes('numeric-separator-literal')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage 3: numeric-separator-literal)');
} else if (features.includes('optional-chaining')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage 3: optional-chaining)');
} else if (features.includes('top-level-await')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage 3: top-level-await)');
} else if (features.includes('optional-chaining')) {
return console.log(BOLD, 'SKIP', RESET, '(Stage 4: `?.`)');
}
let printedOnce = false;
if (!flags.includes('onlyStrict') && !flags.includes('module')) {
// This is the sloppy or web run (governed by the test config!)
let failed = false;
let z;
try {
z = parse(content, false, false, webcompat);
console.log(GREEN, 'PASS', RESET, 'sloppy, webcompat=', webcompat);
} catch (e) {
console.log(GREEN, 'FAIL', RESET, 'sloppy, webcompat=', webcompat);
failed = e;
}
if (failed && failed.toString().toLowerCase().includes('assertion fail')) {
console.log('\nUnrolling output;\n');
stdout.forEach(a => console.log.apply(console, a));
console.log('e:', failed);
console.log('flags:', flags);
throw new Error('File ' + BOLD + file + RESET + BLINK + ' threw assertion error' + RESET + ' in ' + BOLD + 'sloppy' + RESET);
}
if (failed && !(failed.toString().toLowerCase().includes('parser error!') || failed.toString().toLowerCase().includes('lexer error!'))) {
console.log('\nUnrolling output;\n');
stdout.forEach(a => console.log.apply(console, a));
console.log('e:', failed);
console.log('flags:', flags);
throw new Error('File ' + BOLD + file + RESET + BLINK + ' threw an unexpected error' + RESET + ' in ' + BOLD + 'sloppy' + RESET);
}
if (!failed && !printedOnce) {
testPrinter(content, 'sloppy', true, z.ast, false, false, false, false);
printedOnce = true;
}
if (!!failed !== negative) {
console.log('\nUnrolling output;\n');
stdout.forEach(a => console.log.apply(console, a));
console.log('e:', failed, negative);
console.log('flags:', flags);
throw new Error('File ' + BOLD + file + RESET + ' did not meet expectations in '+BOLD+'sloppy'+RESET+', expecting ' + (negative?'fail':'pass') + ' but got ' + (failed?'fail':'pass'));
}
// #######################
// ### OTHER PARSERS ###
// #######################
if (COMPARE_ACORN) {
if (ignoreTest262Acorn(displayFile)) compareDrops.add(displayFile);
// Parse and hope for the best. AnnexB is applied by default, no opt-out
// Acorn doesn't spam comments so we don't have to scrub the input
let [acornOk, acornFail, zasa] = compareAcorn(content, !failed, 'sloppy', true, file);
let matchProblems = processAcornResult(acornOk, acornFail, failed, zasa, false);
if (matchProblems) {
if (TARGET_FILE) {
console.log('matchProblems for sloppy-annexb:', matchProblems);
console.log('##### non-strict annexb content that was tested in Acorn:');
console.log(content);
console.log('#####');
console.log(zasa ? astToString(zasa.ast) : '<no zasa>');
console.log(matchProblems);
console.log('File:', file);
// console.error('Exit now.');
// process.exit(1);
}
if (ignoreTest262Acorn(displayFile)) {
compareSkips.add(displayFile);
console.log(BOLD, 'SKIP', RESET, '(error whitelisted by ignoreTest262Acorn)');
} else {
compareFails.add(displayFile);
console.log(RED, 'BAD!', RESET, '(output not same and file not whitelisted by ignoreTest262Acorn)');
}
} else if (ignoreTest262Acorn(displayFile)) {
console.log(BOLD, 'DROP', RESET, '(error whitelisted by ignoreTest262Acorn but output is same)');
}
}
if (COMPARE_BABEL) {
if (ignoreTest262Babel(displayFile)) compareDrops.add(displayFile);
// Parse and hope for the best. AnnexB is applied by default, no opt-out.
// We just want to confirm Tenko does the same as Babel, not test the actual test
let noCommentContent = scrubCommentsForBabel(content, z);
let [babelOk, babelFail, zasb] = compareBabel(noCommentContent, !failed, 'sloppy', true, file);
let matchProblems = processBabelResult(babelOk, babelFail, failed, zasb, false);
if (matchProblems) {
if (TARGET_FILE) {
console.log('matchProblems for sloppy-annexb:', matchProblems);
console.log('##### non-strict annexb content without comments that was tested in Babel:');
console.log(noCommentContent);
console.log('#####');
console.log(zasb ? astToString(zasb.ast) : '<no zasb>');
console.log(matchProblems);
console.log('File:', file);
// console.error('Exit now.');
// process.exit(1);
}
if (ignoreTest262Babel(displayFile)) {
compareSkips.add(displayFile);
console.log(BOLD, 'SKIP', RESET, '(output not same but file whitelisted by ignoreTest262Babel)');
} else {
compareFails.add(displayFile);
console.log(RED, 'BAD!', RESET, '(output not same and file not whitelisted by ignoreTest262Babel)');
}
} else if (ignoreTest262Babel(displayFile)) {
console.log(BOLD, 'DROP', RESET, '(error whitelisted by ignoreTest262Babel but output is same)');
}
}
}
let modstr = flags.includes('module') ? 'module' : 'strict';
if (!webcompat && !flags.includes('noStrict')) {
// This is the module run
let failed = false;
let z;
try {
z = parse(content, true, flags.includes('module'), webcompat);
console.log(GREEN, 'PASS', RESET, modstr + ', webcompat=', webcompat);
} catch (e) {
console.log(GREEN, 'FAIL', RESET, modstr + ', webcompat=', webcompat);
failed = e;
}
if (failed && failed.toString().toLowerCase().includes('assertion fail')) {
stdout.forEach(a => console.log.apply(console, a));
console.log('e:', failed);
console.log('flags:', flags);
throw new Error('File ' + BOLD + file + RESET + BLINK + ' threw assertion error' + RESET + ' in ' + BOLD + modstr + RESET);
}
if (failed && !(failed.toString().toLowerCase().includes('parser error!') || failed.toString().toLowerCase().includes('lexer error!'))) {
stdout.forEach(a => console.log.apply(console, a));
console.log('e:', failed);
console.log('flags:', flags);
throw new Error('File ' + BOLD + file + RESET + BLINK + ' threw an unexpected error' + RESET + ' in ' + BOLD + modstr + RESET);
}
if (!failed && !printedOnce) {
testPrinter(content, 'module', false, z.ast, false, false, false, false);
printedOnce = true;
}
if (!!failed !== negative) {
stdout.forEach(a => console.log.apply(console, a));
console.log('e:', failed);
console.log('flags:', flags);
throw new Error('File ' + BOLD + file + RESET + ' did not meet expectations in '+BOLD+modstr+RESET+', expecting ' + (failed?'fail':'pass') + ' but got ' + (!failed?'fail':'pass'));
}
// #######################
// ### OTHER PARSERS ###
// #######################
if (COMPARE_ACORN && flags.includes('module')) {
if (ignoreTest262Acorn(displayFile)) compareDrops.add(displayFile);
// Parse and hope for the best. AnnexB is applied by default, no opt-out
// Acorn doesn't spam comments so we don't have to scrub the input
let [acornOk, acornFail, zasa] = compareAcorn(content, !failed, 'module', true, file);
let matchProblems = processAcornResult(acornOk, acornFail, failed, zasa, false);
if (matchProblems) {
if (TARGET_FILE) {
console.log('matchProblems for module-annexb:', matchProblems);
console.log('##### module annexb content that was tested in Acorn:');
console.log(content);
console.log('#####');
console.log(zasa ? astToString(zasa.ast) : '<no zasa>');
console.log(matchProblems);
console.log('File:', file);
// console.error('Exit now.');
// process.exit(1);
}
if (ignoreTest262Acorn(displayFile)) {
compareSkips.add(displayFile);
console.log(BOLD, 'SKIP', RESET, '(output not same but file whitelisted by ignoreTest262Acorn)');
} else {
compareFails.add(displayFile);
console.log(RED, 'BAD!', RESET, '(output not same and file not whitelisted by ignoreTest262Acorn)');
}
} else if (ignoreTest262Acorn(displayFile)) {
console.log(BOLD, 'DROP', RESET, '(error whitelisted by ignoreTest262Acorn but output is same)');
}
}
if (COMPARE_BABEL && flags.includes('module')) {
if (ignoreTest262Babel(displayFile)) compareDrops.add(displayFile);
// Parse and hope for the best. AnnexB is applied by default, no opt-out.
// We just want to confirm Tenko does the same as Babel, not test the actual test
let noCommentContent = scrubCommentsForBabel(content, z);
let [babelOk, babelFail, zasb] = compareBabel(noCommentContent, !failed, 'module', true, file);
let matchProblems = processBabelResult(babelOk, babelFail, failed, zasb, false);
if (matchProblems) {
if (TARGET_FILE) {
console.log('matchProblems for module-annexb:', matchProblems);
console.log('##### module annexb content without comments that was tested in Babel:');
console.log(noCommentContent);
console.log('#####');
console.log(zasb ? astToString(zasb.ast) : '<no zasb>');
console.log(matchProblems);
console.log('File:', file);
// console.error('Exit now.');
// process.exit(1);
}
if (ignoreTest262Babel(displayFile)) {
compareSkips.add(displayFile);
console.log(BOLD, 'SKIP', RESET, '(output not same but file whitelisted by ignoreTest262Babel)');
} else {
compareFails.add(displayFile);
console.log(RED, 'BAD!', RESET, '(output not same and file not whitelisted by ignoreTest262Babel)');
}
} else if (ignoreTest262Babel(displayFile)) {
console.log(BOLD, 'DROP', RESET, '(error whitelisted by ignoreTest262Babel but output is same)');
}
}
}
}
function scrubCommentsForBabel(content, z) {
if (!z) return content;
// Strip comment nodes because that's the only expected difference between the two ASTs
return z.tokens.map(token => (token.nl ? '\n' : '') + (!isCommentToken(token.type) ? content.slice(token.start, token.stop) : '')).join('') || ';';
}
(async function(){
({
Tenko,
GOAL_MODULE,
GOAL_SCRIPT,
COLLECT_TOKENS_ALL,
COLLECT_TOKENS_TYPES,
COLLECT_TOKENS_SOLID,
COLLECT_TOKENS_NONE,
} = (await import(USE_BUILD ? TENKO_PROD_FILE : TENKO_DEV_FILE)));
read(PATH262, '', onRead);
// Any file that's skipped (failed+whitelist) or failed is not droppable. Any file that's white listed is not failed.
compareSkips.forEach(s => (compareFails.delete(s), compareDrops.delete(s)));
compareFails.forEach(s => compareDrops.delete(s));
if (compareDrops.size) {
console.log(BOLD, 'These files passed the comparison but where whitelisted', RESET);
console.log([...compareDrops].sort().join('\n'));
}
if (compareFails.size) {
console.log(BOLD, 'These files failed the comparison and were not whitelisted', RESET);
console.log([...compareFails].sort().join('\n'));
}
console.log('\nNatural end of this script...\n');
})();