tenko
Version:
A "pixel perfect" 100% spec compliant ES2021 JavaScript parser written in JS.
1,052 lines (970 loc) • 58.3 kB
JavaScript
#!/usr/bin/env node --experimental-modules
// Shorthand test runner api from project root through:
//
// ./t --help
//
if (!(process.version.slice(1, 3) >= 10)) throw new Error('Requires node 10+, did you forget `nvm use 10`?');
Error.stackTraceLimit = Infinity; // TODO: cut off at node boundary...
import fs from 'fs';
import path from 'path';
import {execSync} from 'child_process';
import {
ASSERT as _ASSERT,
astToString,
decodeUnicode,
encodeUnicode,
getTestFiles,
parseTestFile,
PROJECT_ROOT_DIR,
promiseToWriteFile,
readFiles,
Tob,
toPrint,
_LOG,
yn,
INPUT_HEADER,
OUTPUT_HEADER,
OUTPUT_HEADER_SLOPPY,
OUTPUT_HEADER_SLOPPY_ANNEXB,
OUTPUT_HEADER_STRICT,
// OUTPUT_HEADER_STRICT_ANNEXB,
OUTPUT_HEADER_MODULE,
OUTPUT_HEADER_MODULE_ANNEXB,
OUTPUT_QUINTICK,
OUTPUT_QUINTICKJS,
} from './utils.mjs';
import {
generateTestFile,
} from './generate_test_file.mjs';
import {
reduceAndExit,
} from './test_case_reducer.mjs';
import {walker} from "../src/tools/walker.mjs";
import {testPrinter} from "./run_printer.mjs";
let LOG = _LOG; // I want to be able to override this and imports are constants
let ASSERT = (...args) => {
if (NO_FATALS) try { _ASSERT(...args); } catch (e) { console.error('Assertion error (squashed by NO_FATALS):', e.stack); }
else _ASSERT(...args);
};
console.log('Start of Tenko test suite');
const INPUT_OVERRIDE = decodeUnicode(process.argv.includes('-F') ? fs.readFileSync(process.argv[process.argv.indexOf('-F') + 1], 'utf8') : process.argv.includes('-i') ? process.argv[process.argv.indexOf('-i') + 1] : '');
const TARGET_FILE = process.argv.includes('-f') ? process.argv[process.argv.indexOf('-f') + 1] : '';
const SEARCH = process.argv.includes('-s');
const TEST262 = process.argv.includes('-t');
const SKIP_TO = TEST262 ? 0 : 0; // skips the first n tests (saves me time)
const STOP_AFTER_TEST_FAIL = process.argv.includes('-q');
const STOP_AFTER_FILE_FAIL = process.argv.includes('-Q');
const TRUNC_STACK_TRACE = !process.argv.includes('-S');
const AUTO_UPDATE = process.argv.includes('-u') || process.argv.includes('-U');
const CONFIRMED_UPDATE = process.argv.includes('-U');
const AUTO_GENERATE = process.argv.includes('-g');
const AUTO_GENERATE_CONSERVATIVE = process.argv.includes('-G');
const REDUCING = process.argv.includes('--min');
const REDUCING_PRINTER = process.argv.includes('--min-printer');
const ALL_VARIANTS = process.argv.includes('--all');
let [a,b,c] = [process.argv.includes('--sloppy'), process.argv.includes('--strict'), process.argv.includes('--module')];
const DISABLE_VARIANTS_UNLESS_OVERRIDE = SEARCH || INPUT_OVERRIDE || REDUCING;
const RUN_SLOPPY = ALL_VARIANTS || (a || (!b && !c));
const RUN_STRICT = ALL_VARIANTS || b || (!DISABLE_VARIANTS_UNLESS_OVERRIDE && !a && !c);
const RUN_MODULE = ALL_VARIANTS || c || (!DISABLE_VARIANTS_UNLESS_OVERRIDE && !a && !b);
const ENABLE_ANNEXB = process.argv.includes('--annexb');
const TARGET_ES6 = process.argv.includes('--es6');
const TARGET_ES7 = process.argv.includes('--es7');
const TARGET_ES8 = process.argv.includes('--es8');
const TARGET_ES9 = process.argv.includes('--es9');
const TARGET_ES10 = process.argv.includes('--es10');
const TARGET_ES11 = process.argv.includes('--es11');
const TARGET_ES12 = process.argv.includes('--es12');
const TESTS_ONLY = process.argv.includes('-n'); // skip constructing updated test files, dont write anything. Used for code coverage
const RUN_VERBOSE_IN_SERIAL = process.argv.includes('--serial') || (!SEARCH && !TESTS_ONLY && (INPUT_OVERRIDE || TARGET_FILE || STOP_AFTER_TEST_FAIL || STOP_AFTER_FILE_FAIL));
const FORCE_WRITE = process.argv.includes('--force-write');
const ACORN_COMPAT = process.argv.includes('--acorn');
const BABEL_COMPAT = process.argv.includes('--babel');
const COMPARE_ACORN = process.argv.includes('--test-acorn');
const COMPARE_BABEL = process.argv.includes('--test-babel');
const COMPARE_NODE = process.argv.includes('--test-node');
const TEST_ACORN = COMPARE_ACORN && (!AUTO_UPDATE || CONFIRMED_UPDATE); // ignore this flag with -u, we dont want to record acorn deltas into test files
const TEST_BABEL = COMPARE_BABEL && (!AUTO_UPDATE || CONFIRMED_UPDATE); // ignore this flag with -u, we dont want to record babel deltas into test files
const NO_FATALS = process.argv.includes('--no-fatals'); // asserts should not stop a full auto run (dev tool, rely on git etc for recovery...)
const CONCISE = process.argv.includes('--concise');
const USE_BUILD = process.argv.includes('-b') || process.argv.includes('--build');
const SKIP_PRINTER = process.argv.includes('--no-printer'); // || USE_BUILD;
const EXPOSE_SCOPE = process.argv.includes('--expose-scope');
const TENKO_DEV_FILE = '../src/index.mjs';
const TENKO_PROD_FILE = '../build/tenko.prod.mjs';
if (TEST_BABEL && TEST_ACORN) throw new Error('Cannot test Babel and Acorn at the same time. Pick one.');
if (TEST_ACORN) console.log('Running in Acorn compat mode and comparing to actual Acorn Parser output');
if (TEST_BABEL) console.log('Running in Babel compat mode and comparing to actual Babel Parser output');
if (process.argv.includes('-?') || process.argv.includes('--help')) {
console.log(`
Tenko Test Runner
You probably want to use ./t for easy api access... But in case you really want details on this script, here you go :)
Usage:
\`tests/run_tests.mjs\` [options]
But for the time being:
\`node --experimental-modules tests/run_tests.mjs\`
And suggested if also testing builds:
\`node --experimental-modules cli/build.mjs; node --experimental-modules tests/run_tests.mjs\` [options]
Options:
-b --build Use prod build instead of dev source for Tenko in this call (assumes built in \`/build/...\`; \`./t z\`)
-f "path" Only test this file / dir
-F "path" Use file contents as input
-i "input" Test input only (sloppy, strict, module), implies --sloppy unless at least one mode explicitly given
-g Regenerate computed test case blocks (process all autogen.md files)
-G Same as -g except it skips existing files
-Q Stop after first fail, but test all four modes (sloppy/strict/module/web) regardless
-q Stop after first fail
-s Use HIT() in code and only print tests that execute at least one HIT(), implies -q
-t Run test262 suite
-u Unconditionally auto-update tests with the results (tests silently updated inline, use source control to diff)
-U Auto-update but confirm before each test case is updated inline (use with -q for controlled updating)
--sloppy Only run tests in sloppy mode (can be combined with other modes like --strict)
--strict Only run tests in strict mode (can be combined with other modes like --module)
--module Only run tests with module goal (can be combined with other modes like --strict)
--annexb Enable web compatibility extensions listed in Annex B in the specification
--acorn Run in Acorn compat mode (\`acornCompat=true\`)
--babel Run in Babel compat mode (\`babelCompat=true\`)
--test-acorn Also show diff with Acorn AST / pass/fail with test cases (not the same as --acorn !)
--test-babel Also show diff with Babel AST / pass/fail with test cases (not the same as --babel !)
--all Force to run all four modes (on input)
--esX Where X is one of 6 through 10, like --es6. For -i only, forces the code to run in that version
--serial Test all targeted files in serial, verbosely, instead of using parallel phases (which is faster)
(Note: -q, -i, and -f implicitly enable --serial)
--no-printer Skip running Printer on input
--min Brute-force simplify a test case that throws an error while maintaining the same error message, only with -f, implies --sloppy
-- write For reducer only; write result to new file
--min-printer Minimize a Printer-failing input case
--force-write Always write the test cases to disk, even when no change was detected
--no-fatals Do not treat (test) assertion errors as fatals (dev tools only, rely on git etc for recovery)
--concise Do not dump AST and printer output to stdout. Parse and stop. Only works with -i or -f or -F
--expose-scope Add generated scope objects as \`.$scope\` property for nodes that generate a scope. Good luck that that that...
`);
process.exit();
}
const FORCED_ES_TARGET = TARGET_ES6 ? 6 : TARGET_ES7 ? 7 : TARGET_ES8 ? 8 : TARGET_ES9 ? 9 : TARGET_ES10 ? 10 : TARGET_ES11 ? 11 : TARGET_ES12 ? 12 : undefined;
if (FORCED_ES_TARGET) console.log('Forcing target version: ES' + FORCED_ES_TARGET);
if (AUTO_UPDATE && (AUTO_GENERATE || AUTO_GENERATE_CONSERVATIVE)) throw new Error('Cannot use auto update and auto generate together');
if (AUTO_UPDATE && (a || b || c)) throw new Error('Cannot use --sloppy (etc) together with -u');
// Lazily loaded
let COLLECT_TOKENS_NONE;
let COLLECT_TOKENS_SOLID;
let COLLECT_TOKENS_ALL;
let COLLECT_TOKENS_TYPES;
let GOAL_MODULE;
let GOAL_SCRIPT;
let WEB_COMPAT_ON;
let WEB_COMPAT_OFF;
let toktypeToString;
// 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 BOLD = '\x1b[;1;1m';
const OVER = '\x1b[32;53m';
const DIM = '\x1b[30;1m';
const BLINK = '\x1b[;5;1m';
const RED = '\x1b[31m';
const GREEN = '\x1b[32m';
const RESET = '\x1b[0m';
const TEST_SLOPPY = 'sloppy';
const TEST_STRICT = 'strict';
const TEST_MODULE = 'module';
if ((REDUCING || REDUCING_PRINTER) && !TARGET_FILE && !INPUT_OVERRIDE) throw new Error('Can only use `--min` and `--min-parser` together with `-f` or `-i`');
if (NO_FATALS) console.log(BLINK + 'NO_FATALS enabled. Do not blindly commit result!!' + RESET);
if (USE_BUILD) console.log('Using PROD build of Tenko');
let stopAsap = false;
let skippedOtherParserList = [];
let unxepctedFails = [];
let unexpectedPass = [];
// use -s and call HIT in some part of the code to log all test cases that visit that particular branch(es)
let wasHits = [];
let hitsToReport;
let foundCache = new Set; // dont print multiples
let foundTest = (x) => wasHits.length || wasHits.push(x);
let foundTests = (x) => wasHits.push(x);
let PRINT_HIT = console.log;
if (SEARCH) {
global.HIT = foundTest; // faster to quickly search than exporting and having to uncomment the import...
global.HITS = foundTests; // this basically becomes console.log but ok, collect _all_ hits rather than just the first
console.log = () => {};
console.warn = () => {};
console.error = () => {};
// console.dir = () => {}; // This is usually my workaround goto method to circumvent console blocking ;)
LOG = () => {};
PRINT_HIT(BLINK + 'Suppressing __all__ further output, only printing hits...' + RESET);
} else {
global.HIT = ()=>{};
global.HITS = ()=>{};
}
// Babel is loaded async
let compareBabel;
let ignoreTenkoTestForBabel;
let processBabelResult;
// Babel is loaded async
let compareAcorn;
let ignoreTenkoTestForAcorn;
let processAcornResult;
async function extractFiles(list) {
if (!RUN_VERBOSE_IN_SERIAL) console.time('$$ Test file extraction time');
let bytes = 0;
list.forEach((tob/*: Tob */) => {
if (tob.oldData[0] === '@') generateTestFile(tob);
parseTestFile(tob);
bytes += tob.inputCode.length;
});
if (!RUN_VERBOSE_IN_SERIAL) console.timeEnd('$$ Test file extraction time');
console.log('Total input size:', bytes, 'bytes');
}
function coreTest(tob, tenko, testVariant, annexB, enableCodeFrame = false, code = tob.inputCode, verbose = !!(INPUT_OVERRIDE || TARGET_FILE)) {
wasHits = [];
hitsToReport = wasHits;
let r, e = '';
let stdout = [];
try {
if (verbose) {
console.time('Pure Tenko parse time');
console.log('Input size:', code.length, 'bytes');
}
r = tenko(
code,
{
goalMode: testVariant === TEST_MODULE ? GOAL_MODULE : GOAL_SCRIPT,
collectTokens: COLLECT_TOKENS_SOLID,
strictMode: testVariant === TEST_STRICT,
webCompat: ENABLE_ANNEXB || annexB ? WEB_COMPAT_ON : WEB_COMPAT_OFF,
targetEsVersion: FORCED_ES_TARGET || tob.inputOptions.es,
babelCompat: BABEL_COMPAT,
acornCompat: ACORN_COMPAT,
exposeScopes: tob.inputOptions.exposeScopes || EXPOSE_SCOPE,
ranges: tob.inputOptions.ranges || false,
nodeRange: tob.inputOptions.nodeRange || false,
locationTracking: tob.inputOptions.locationTracking || true,
astUids: tob.inputOptions.astUids || false,
errorCodeFrame: enableCodeFrame,
truncCodeFrame: true,
$log: verbose ? undefined : (...a) => stdout.push(a),
$warn: verbose ? undefined : (...a) => stdout.push(a),
$error: verbose ? undefined : (...a) => stdout.push(a),
},
);
wasHits = []; // Prevent other parse calls from adding more HITS
if (verbose) {
console.timeEnd('Pure Tenko parse time');
if (CONCISE) return;
}
if (tob.shouldFail) {
tob.continuePrint = BLINK + 'FILE ASSERTED TO FAIL' + RESET + ', but it passed';
}
// Test the ast printer
// We only really need to test it once for whatever run passes
if (!SKIP_PRINTER && !tob.printerOutput) {
tob.printerOutput = testPrinter(
code,
testVariant,
ENABLE_ANNEXB || annexB,
r.ast,
!INPUT_OVERRIDE && !TARGET_FILE && (AUTO_UPDATE && !CONFIRMED_UPDATE),
REDUCING_PRINTER,
!REDUCING_PRINTER || BABEL_COMPAT || ACORN_COMPAT,
verbose
);
if (tob.printerOutput[2] !== 'same' && tob.printerOutput[2] !== 'diff-same') {
tob.continuePrint = 'Printer output needs attention [' + tob.printerOutput[2] + ']';
}
// Assert that the walker works properly. The first phase adds a new key to each node and throws an error if
// that key already exists. The second phase "manually" walks each property of the AST and confirms that each
// node is properly visited. If not it will leave a dirty key behind which should show up in the AST test diff.
// Phase 1:
walker(r.ast, (node, parent, prop, index) => {
ASSERT(node && typeof node === 'object' && !(node instanceof RegExp), 'node should be a plain obj');
ASSERT(node.test_walked !== true, 'should not walk the same node twice');
if (parent !== undefined) {
// Root node has no parent
if (Array.isArray(parent[prop])) {
ASSERT(typeof index === 'number', 'index should be number if parent[prop] is an array', index);
ASSERT(parent[prop][index] === node, 'parent[prop][index] should be node', prop, index, parent);
} else {
ASSERT(typeof index === 'undefined', 'index should be undefined if parent[prop] is not an array', index);
ASSERT(parent[prop] === node, 'parent prop should be node', prop, parent);
}
}
node.test_walked = true;
});
// Phase 2:
function repeat(node, key, parent) {
if (key === '$scope') return; // Tenko can expose these optional, do not visit them here
if (key === 'range') return;
if (!Array.isArray(node)) {
// node must be a plain object (because don't use anything else besides arrays)
if (node.test_walked) {
delete node.test_walked;
} else {
node.ASSERTION_ERROR_WALKER_DID_NOT_VISIT_THIS_NODE = true;
}
}
for (let key in node) if (node.hasOwnProperty(key)
&& key !== 'loc'
&& !(node.type === 'Literal' && key === 'regex')
&& !(node.type === 'TemplateElement' && key === 'value')
) {
let v = node[key];
if (Array.isArray(v)) {
v.forEach(e => e && repeat(e, key, node));
} else if (typeof v === 'object' && v && !(v instanceof RegExp)) {
repeat(v, key, node);
}
}
}
// If the walker was incomplete then the AST will be modified with keys to the unvisited objects.
repeat(r.ast, 'root', {});
}
} catch (_e) {
if (verbose) {
console.timeEnd('Pure Tenko parse time');
}
e = _e;
if (tob.shouldPass) {
tob.continuePrint = BLINK + 'FILE ASSERTED TO PASS' + RESET + ', but it failed';
}
}
if (tob.continuePrint) {
if (!NO_FATALS && AUTO_UPDATE && tob.continuePrint && !CONFIRMED_UPDATE && !INPUT_OVERRIDE && !TARGET_FILE) {
console.error(BOLD + 'Test Assertion fail' + RESET + ': testVariant=' + testVariant + ', annexB=' + annexB + ', test ' + BOLD + tob.file + RESET + ' was explicitly marked to pass, but it failed somehow;\n' + RED + tob.continuePrint + RESET);
process.exit();
} else if (verbose) {
console.error(tob.continuePrint);
}
}
let babelOk, babelFail, zasb;
// Tests with specific versions should also have non-specific counter parts. Since Babel does not support targeting
// specific spec versions, we should just skip those variants because they lead to false positives.
if (TEST_BABEL && (!Number.isFinite(tob.inputOptions.es) || TARGET_FILE || INPUT_OVERRIDE)) {
[babelOk, babelFail, zasb] = compareBabel(code, !e, testVariant, ENABLE_ANNEXB || annexB, tob.file, INPUT_OVERRIDE || TARGET_FILE);
}
let acornOk, acornFail, zasa;
// Acorn does support version, but also always annexb and no strict mode option
if (TEST_ACORN) {
if (tob.fileShort.startsWith('tests/testcases/regexes/range_surrogate_head_end/')) {
acornOk = false;
acornFail = 'infinite loop';
} else {
[acornOk, acornFail, zasa] = compareAcorn(code, !e, testVariant, ENABLE_ANNEXB || annexB, tob.file, tob.inputOptions.es, INPUT_OVERRIDE || TARGET_FILE);
}
}
let nodeFail = undefined;
if (COMPARE_NODE) {
if (testVariant === TEST_STRICT || testVariant === TEST_SLOPPY) {
try {
Function((testVariant === TEST_STRICT ? '"use strict";' : '') + code);
nodeFail = false
} catch (e) {
nodeFail = e;
}
}
}
return {r, e, stdout, babelOk, babelFail, zasb, nodeFail, acornOk, acornFail, zasa};
}
async function postProcessResult(tob/*: Tob */, testVariant/*: "sloppy" | "strict" | "module" */, annexB) {
let {parserRawOutput: {[testVariant+annexB]: {r, e, stdout, babelOk, babelFail, zasb, nodeFail, acornOk, acornFail, zasa}}, file} = tob;
if (!r && !e) return; // no output for this variant
let errorMessage = '';
if (e) {
errorMessage = e.message;
if (errorMessage.includes('Assertion fail')) {
stdout.forEach(a => console.log.apply(console, a));
console.error('####\nAn ' + BLINK + 'assertion' + RESET + ' error was thrown in ' + BOLD + testVariant + RESET + ' mode with annexB=' + annexB + '\n');
console.error(BOLD + 'Input:' + RESET + '\n\n`````\n' + tob.inputCode + '\n`````\n\n' + BOLD + 'Error message:' + RESET + '\n');
console.error(errorMessage);
console.error('####');
console.error(e.stack);
if (!NO_FATALS) {
hardExit(tob, 'postProcessResult assertion error');
throw new Error('Assertion error. Mode = ' + testVariant + ', annexB=' + annexB + ', file = ' + file + '; ' + errorMessage.message);
}
}
else if (errorMessage.startsWith('Parser error!')) {
errorMessage = errorMessage.slice(0, 'Parser error!'.length) + '\n ' + errorMessage.slice('Parser error!'.length + 1);
}
else if (errorMessage.startsWith('Lexer error!')) {
errorMessage = errorMessage.slice(0, 'Lexer error!'.length) + '\n ' + errorMessage.slice('Lexer error!'.length + 1);
}
else {
stdout.forEach(a => console.log.apply(console, a));
// errorMessage = 'TEMP SKIPPED UNKNOWN ERROR';
console.error('####\nThe following ' + BLINK + 'unexpected' + RESET + ' error was thrown in ' + BOLD + testVariant + RESET + ' mode with annexB=' + annexB + ':\n');
console.error(errorMessage);
console.error(e.stack);
console.error('####');
if (!NO_FATALS) {
hardExit(tob, 'postProcessResult unknown error');
throw new Error('Non-graceful error, fixme. Mode = ' + testVariant + ', annexB=' + annexB + ', file = ' + file + '; ' + errorMessage.message);
}
}
}
let outputTestString = (
// throws: Parser error!
// throws: Lexer error!
(errorMessage ? 'throws: ' + errorMessage : '') +
// (r ? 'ast: ' + JSON.stringify(r.ast) + '\n\n' + formatTokens(r.tokens) : '')
// Using util.inspect makes the output formatting highly tightly bound to node's formatting rules
// At the same time, the same could be said for Prettier (although we can lock that down by package version,
// independent from node version). However, using prettier takes roughly 23 seconds, inspect half a second. Meh.
(r ? 'ast: ' + astToString(r.ast) + '\n\n' + formatTokens(r.tokens) : '')
);
let babelMatchError = '';
if (TEST_BABEL && annexB && testVariant !== 'strict') {
// Babel does not support sloppy/strict switch and always enables annexb rules, so do not check other combos
if (Number.isFinite(tob.inputOptions.es)) {
tob.compareSkippedExplicitVersion = true;
} else {
ASSERT(!babelOk !== !babelFail, 'babel should have run, should either pass or fail, not both, not neither [file = ' + file +' ]');
tob.compareWhiteListed = ignoreTenkoTestForBabel(tob.fileShort);
babelMatchError = processBabelResult(babelOk, babelFail, !!e, zasb, tob, TEST_BABEL, INPUT_OVERRIDE);
if (babelMatchError && TARGET_FILE) {
console.log('babelMatchError:', BOLD, babelMatchError, RESET)
// throw new Error(babelMatchError)
}
if (babelMatchError) tob.compareHadMatchFailure = true;
if (!INPUT_OVERRIDE && !TARGET_FILE && tob.compareWhiteListed) {
babelMatchError = '';
}
}
}
let acornMatchError = '';
if (TEST_ACORN && annexB && testVariant !== 'strict') {
ASSERT(!acornOk !== !acornFail, 'acorn should have run, should either pass or fail, not both, not neither');
// Acorn does not support sloppy/strict switch and always enables annexb rules, so do not check other combos
tob.compareWhiteListed = ignoreTenkoTestForAcorn(tob.fileShort);
acornMatchError = processAcornResult(acornOk, acornFail, !!e, zasa, tob, TEST_ACORN, INPUT_OVERRIDE);
if (acornMatchError && TARGET_FILE) {
console.log('acornMatchError:', BOLD, acornMatchError, RESET)
// throw new Error(babelMatchError)
}
if (acornMatchError) tob.compareHadMatchFailure = true;
if (!INPUT_OVERRIDE && !TARGET_FILE && tob.compareWhiteListed) {
acornMatchError = '';
}
}
let nodeOutput = (
nodeFail === undefined ? '' :
testVariant === TEST_STRICT ?
((!nodeFail && e) ? 'Node compiled a function with a strictmode header and this input without error\n\n' : (nodeFail && !e) ? 'Node threw an error while compiling a function with a strictmode header and this input\n\n' : '')
:
((!nodeFail && e) ? 'Node compiled a function with this input without error\n\n' : (nodeFail && !e) ? 'Node threw an error while compiling a function with this input:\n' + nodeFail + '\n\n' : '')
);
// Caching it like this takes up more memory but makes deduping other modes so-much-easier
switch (testVariant) {
case TEST_SLOPPY:
if (annexB) {
tob.newOutputSloppyAnnexB = outputTestString;
tob.newOutputSloppyAnnexBBabel = babelMatchError;
tob.newOutputSloppyAnnexBNode = nodeOutput;
tob.newOutputSloppyAnnexBAcorn = acornMatchError;
} else {
tob.newOutputSloppy = outputTestString;
tob.newOutputSloppyBabel = babelMatchError;
tob.newOutputSloppyNode = nodeOutput;
tob.newOutputSloppyAcorn = acornMatchError;
}
break;
case TEST_STRICT:
if (annexB) {
tob.newOutputStrictAnnexB = outputTestString;
tob.newOutputStrictAnnexBBabel = babelMatchError;
tob.newOutputStrictAnnexBNode = nodeOutput;
tob.newOutputStrictAnnexBAcorn = '';
} else {
tob.newOutputStrict = outputTestString;
tob.newOutputStrictBabel = babelMatchError;
tob.newOutputStrictNode = nodeOutput;
tob.newOutputStrictAcorn = '';
}
break;
case TEST_MODULE:
if (annexB) {
tob.newOutputModuleAnnexB = outputTestString;
tob.newOutputModuleAnnexBBabel = babelMatchError;
tob.newOutputModuleAnnexBAcorn = acornMatchError;
} else {
tob.newOutputModule = outputTestString;
tob.newOutputModuleBabel = babelMatchError;
tob.newOutputModuleAcorn = acornMatchError;
}
break;
default: FIXME;
}
}
async function runTest(list, tenko, testVariant/*: "sloppy" | "strict" | "module" */, annexB) {
if (!RUN_VERBOSE_IN_SERIAL) console.log(' - Now testing', INPUT_OVERRIDE ? 'for:' : 'all cases for:', testVariant, 'annexb=', annexB);
if (!RUN_VERBOSE_IN_SERIAL) console.time(' $$ Batch for ' + testVariant + ' (annexB='+annexB+')');
let bytes = 0;
let ok = 0;
let fail = 0;
if (!RUN_VERBOSE_IN_SERIAL) console.log(' Parsing all inputs');
if (!RUN_VERBOSE_IN_SERIAL) console.time(' $$ Parse time for all tests');
await Promise.all(list.map(async (tob/*: Tob */) => {
let {inputCode, inputOptions} = tob;
bytes += inputCode.length;
if (REDUCING) {
// Note: we disable code frame generation because it leads to a very noisy error message that includes the input
// code and line numbers. The test case minifier relies purely on the output error staying the same.
reduceAndExit(tob.inputCode, code => coreTest(tob, tenko, testVariant, annexB, false, code, false).e || false, `./t --${testVariant} ${annexB ? '--annexb' : ''} ${Number.isFinite(FORCED_ES_TARGET || tob.inputOptions.es) ? FORCED_ES_TARGET || tob.inputOptions.es : ''}`, tob.file);
}
// This is quite memory expensive but much easier to work with
tob.parserRawOutput[testVariant+annexB] = coreTest(tob, tenko, testVariant, annexB, true);
if (CONCISE) return;
let rawOutput = tob.parserRawOutput[testVariant+annexB];
if (SEARCH) {
let e = rawOutput.e;
// If you use -q -i then you just want to know whether or not some codepath hits some code
if (INPUT_OVERRIDE) {
PRINT_HIT('# ' + testVariant + ' (annexb = ' + annexB + ')')
if (hitsToReport.length) {
let box = `[${(e&&e.message.includes('TODO')?'T':e?RED+'x':GREEN+'v')+RESET}]`;
let root = `${box} Input ${BOLD + 'WAS' + RESET} hit` + (hitsToReport.length > 1 ? ` (${hitsToReport.length}x)` : '') + ' ';
hitsToReport.forEach((hit, i) => {
if (i === 0) PRINT_HIT(root + '(' + hit + ')')
else PRINT_HIT(' ' + '('.padStart(root.length - ((box.length - 3) + BOLD.length + RESET.length), ' ') + hit + ')');
})
} else {
PRINT_HIT(`[${(e&&e.message.includes('TODO')?'T':e?RED+'x':GREEN+'v')+RESET}] Input ${'was ' + BOLD + 'NOT' + RESET} hit`);
}
} else if (hitsToReport.length) {
if (!foundCache.has(inputCode)) {
let box = `[${(e && e.message.includes('TODO')?'T':e?RED+'x':GREEN+'v')+RESET}]`;
let root = `// ${box}: \`${toPrint(inputCode)}\`` + (hitsToReport.length > 1 ? ' (' + hitsToReport.length + 'x)' : '') + ' ';
hitsToReport.forEach((hit, i) => {
if (i === 0) PRINT_HIT(root + '(' + hit + ')')
else PRINT_HIT(' ' + '('.padStart(root.length-(box.length - 3), ' ') + hit + ')');
});
foundCache.add(tob.inputCode); // dedupe tests with the same input
}
}
return;
}
if (rawOutput.e) ++fail;
else if (rawOutput.r) ++ok;
else throw new Error('invariant');
}));
if (!RUN_VERBOSE_IN_SERIAL) console.log(' Have', list.length, 'results, totaling', bytes, 'bytes, ok = ', ok, ', fail =', fail);
if (!RUN_VERBOSE_IN_SERIAL) console.timeEnd(' $$ Parse time for all tests');
if (SEARCH) return;
if (CONCISE) return;
if (TESTS_ONLY) {
if (!RUN_VERBOSE_IN_SERIAL) console.log(' Skipping post process step because -n was given');
} else {
if (!RUN_VERBOSE_IN_SERIAL) console.log(' Processing', list.length, 'result for all tests');
if (!RUN_VERBOSE_IN_SERIAL) console.time(' $$ Parse result post processing time');
await Promise.all(list.map(async (tob/*: Tob*/) => await postProcessResult(tob, testVariant, annexB)));
if (!RUN_VERBOSE_IN_SERIAL) console.timeEnd(' $$ Parse result post processing time');
}
if (!RUN_VERBOSE_IN_SERIAL) console.timeEnd(' $$ Batch for ' + testVariant + ' (annexB='+annexB+')');
}
function showDiff(tob) {
console.log(
'\n' +
BOLD + '######' + RESET + '\n' +
BOLD + '## ' + RESET + 'Now showing diff' + '\n' +
BOLD + '## ' + RESET + 'File:', tob.file, '\n' +
BOLD + '###### Input:' + RESET + '\n' +
tob.inputCode, '\n' +
BOLD + '######' + RESET + '\n'
);
// For diffing sake, dont encode the unicode stuff when doing acorn/babel runs
let newData = TEST_ACORN || TEST_BABEL
? tob.newData
: encodeUnicode(tob.newData);
// We omit some test-boiler plate from the diff because we don't care about that in the diff
// (This is visual to the test runner only, actual test cases will still have this stuff)
execSync(
// Use base64 to prevent shell interpretation of input. Final `cat` is to suppress `diff`'s exit code when diff.
`colordiff -a -y -w -W200 <(
cat "${tob.file}" |
grep -v "_Note: the whole output block is auto-generated. Manual changes will be overwritten!_" |
grep -v "Below follow outputs in four parsing modes: sloppy mode, strict mode script goal, module goal, web compat mode (always sloppy)." |
grep -v "Below follow outputs in five parsing modes: sloppy, sloppy+annexb, strict script, module, module+annexb" |
grep -v "Note that the output parts are auto-generated by the test runner to reflect actual result." |
grep -v "Parsed with script goal and as if the code did not start with strict mode header." |
grep -v "Parsed with script goal but as if it was starting with \\\`\\"use strict\\"\\\` at the top." |
grep -v "Parsed with script goal with AnnexB rules enabled and as if the code did not start with strict mode header." |
grep -v "Parsed with script goal with AnnexB rules enabled but as if it was starting with \\\`\\"use strict\\"\\\` at the top." |
grep -v "Parsed with the module goal." |
grep -v "Parsed in sloppy script mode but with the web compat flag enabled." |
sed '/^$/N;/^\\n$/D'
) <(
echo '${Buffer.from(newData).toString('base64')}' | base64 -d - |
grep -v "_Note: the whole output block is auto-generated. Manual changes will be overwritten!_" |
grep -v "Below follow outputs in four parsing modes: sloppy mode, strict mode script goal, module goal, web compat mode (always sloppy)." |
grep -v "Below follow outputs in five parsing modes: sloppy, sloppy+annexb, strict script, module, module+annexb" |
grep -v "Note that the output parts are auto-generated by the test runner to reflect actual result." |
grep -v "Parsed with script goal and as if the code did not start with strict mode header." |
grep -v "Parsed with script goal with AnnexB rules enabled and as if the code did not start with strict mode header." |
grep -v "Parsed with script goal but as if it was starting with \\\`\\"use strict\\"\\\` at the top." |
grep -v "Parsed with script goal with AnnexB rules enabled but as if it was starting with \\\`\\"use strict\\"\\\` at the top." |
grep -v "Parsed with the module goal." |
grep -v "Parsed in sloppy script mode but with the web compat flag enabled." |
sed '/^$/N;/^\\n$/D'
) |
cat`,
{stdio: 'inherit', shell: '/bin/bash', encoding: 'utf8'}
);
}
function hardExit(tob, msg) {
stopAsap = true;
if (!msg) FIXME
if (tob) console.log(RED + 'FAIL' + RESET + ' ' + DIM + tob.fileShort + RESET);
if (!NO_FATALS) {
console.log('Hard exit() node now because: ' + msg);
process.exit();
}
}
async function runTests(list, tenko) {
if (!RUN_VERBOSE_IN_SERIAL) console.time('$$ Total runtime');
if (!RUN_VERBOSE_IN_SERIAL) console.log('Now actually running all', list.length, 'test cases... 4x! Single threaded! This may take some time (~20s on my machine)');
if (RUN_SLOPPY) await runTest(list, tenko, TEST_SLOPPY, false);
if (RUN_STRICT) await runTest(list, tenko, TEST_STRICT, false);
if (RUN_MODULE) await runTest(list, tenko, TEST_MODULE, false);
if (RUN_SLOPPY) await runTest(list, tenko, TEST_SLOPPY, true);
if (RUN_MODULE) await runTest(list, tenko, TEST_MODULE, true);
if (!RUN_VERBOSE_IN_SERIAL) console.timeEnd('$$ Total runtime');
if (SEARCH) return;
if (RUN_VERBOSE_IN_SERIAL && !AUTO_UPDATE && !INPUT_OVERRIDE) {
for (let i=0; i<list.length; ++i) {
let tob = list[i];
let oldOutput = tob.oldData;
let newOutput = generateOutputBlock(tob);
if (newOutput !== oldOutput) {
console.log('\nTest output change detected!\n');
// dump outputs
if (RUN_SLOPPY) {
console.log(BOLD + '### Terminal output for sloppy annexb=false run:' + RESET);
tob.parserRawOutput.sloppyfalse.stdout.forEach((a) => console.log(...a));
console.log(BOLD + '### Terminal output for sloppy annexb=true run:' + RESET);
tob.parserRawOutput.sloppytrue.stdout.forEach((a) => console.log(...a));
}
if (RUN_STRICT) {
console.log(BOLD + '### Terminal output for strict run:' + RESET);
tob.parserRawOutput.strictfalse.stdout.forEach((a) => console.log(...a));
}
if (RUN_MODULE) {
console.log(BOLD + '### Terminal output for module annexb=false run:' + RESET);
tob.parserRawOutput.modulefalse.stdout.forEach((a) => console.log(...a));
console.log(BOLD + '### Terminal output for module annexb=true run:' + RESET);
tob.parserRawOutput.moduletrue.stdout.forEach((a) => console.log(...a));
}
if (!TARGET_FILE && !INPUT_OVERRIDE) {
showDiff(tob);
}
console.log('\n' + DIM + tob.fileShort + RESET);
console.log(BOLD + '\n./t f "' + tob.file + '"'+(TEST_BABEL ? ' --test-babel' : '')+(TEST_ACORN ? ' --test-acorn' : '')+'\n');
if (!TARGET_FILE && !INPUT_OVERRIDE) {
if (tob.continuePrint) console.error(tob.continuePrint);
let cont = await yn('Continue?');
if (!cont) hardExit(tob, 'Test output change detected. Aborting early.');
}
}
}
}
}
function constructNewOutput(list) {
if (!RUN_VERBOSE_IN_SERIAL) console.time('$$ New output construction time');
list.forEach((tob/*: Tob */) => {
generateOutputBlock(tob);
// TODO: create a compat table; "what do other parsers do with this input?"
});
if (!RUN_VERBOSE_IN_SERIAL) console.timeEnd('$$ New output construction time');
}
async function writeNewOutput(list) {
if (!RUN_VERBOSE_IN_SERIAL) console.time('$$ Write updated test files');
let updated = 0;
if (CONFIRMED_UPDATE) {
// This is slower but must process files in serial...
for (let i=0; i<list.length; ++i) {
let tob = list[i];
const {newData, oldData, file} = tob;
if (newData !== oldData || FORCE_WRITE) {
if (tob.continuePrint) console.error(tob.continuePrint);
console.log('\n' + DIM + tob.fileShort + RESET);
console.log(DIM + '\n./t f "' + tob.file + '"'+(TEST_BABEL ? ' --test-babel' : '')+(TEST_ACORN ? ' --test-acorn' : '')+'\n' + RESET);
let cont = await yn('Continue to overwrite test output?');
if (USE_BUILD) {
// Never write build output to test files ...
console.log('Did NOT write to file because using prod builds to test');
} else if (cont) {
++updated;
await promiseToWriteFile(file, newData);
}
} else {
if (tob.continuePrint) {
console.log('\n' + DIM + tob.fileShort + RESET);
console.log(DIM + '\n./t f "' + tob.file + '"'+(TEST_BABEL ? ' --test-babel' : '')+(TEST_ACORN ? ' --test-acorn' : '')+'\n' + RESET);
if (!await yn('File was not changed, invariant was broken and written anyways. Continue testing?')) process.exit();
}
}
}
} else {
await Promise.all(list.map((tob/*: Tob */) => {
const {newData, oldData, file} = tob;
if (newData !== oldData || FORCE_WRITE) {
if (AUTO_UPDATE) {
if (STOP_AFTER_TEST_FAIL) stopAsap = true;
++updated;
return promiseToWriteFile(file, newData);
} else {
console.log('\n' + DIM + tob.fileShort + RESET);
console.log(DIM + '\n./t f "' + tob.file + '"'+(TEST_BABEL ? ' --test-babel' : '')+(TEST_ACORN ? ' --test-acorn' : '')+'\n' + RESET);
console.error('Output mismatch for', file);
return Promise.resolve();
}
}
}));
if (!NO_FATALS && updated && (STOP_AFTER_TEST_FAIL || STOP_AFTER_FILE_FAIL)) {
stopAsap = true;
hardExit(undefined, 'updated at least one file in writeNewOutput()');
}
}
if (!RUN_VERBOSE_IN_SERIAL) console.timeEnd('$$ Write updated test files');
if (!RUN_VERBOSE_IN_SERIAL) console.log('Updated', updated, 'files');
}
async function loadTenkoAsync() {
let Tenko;
if (!RUN_VERBOSE_IN_SERIAL) console.time('$$ Parser load');
({
Tenko,
COLLECT_TOKENS_SOLID,
COLLECT_TOKENS_NONE ,
COLLECT_TOKENS_ALL,
COLLECT_TOKENS_TYPES,
GOAL_MODULE,
GOAL_SCRIPT,
WEB_COMPAT_ON,
WEB_COMPAT_OFF,
toktypeToString,
} = await import(path.join(dirname, USE_BUILD ? TENKO_PROD_FILE : TENKO_DEV_FILE)));
if (!RUN_VERBOSE_IN_SERIAL) console.timeEnd('$$ Parser load');
return Tenko;
}
async function runAndRegenerateList(list, tenko) {
await runTests(list, tenko);
if (TESTS_ONLY) {
if (!RUN_VERBOSE_IN_SERIAL) console.log('Skipping write of updated test files because -n was given');
}
else if (!SEARCH) {
constructNewOutput(list);
if (RUN_VERBOSE_IN_SERIAL && list[0].oldData !== list[0].newData) {
try {
showDiff(list[0]);
} catch (e) {
console.log('Unable to show the diff... ' + e);
}
}
if (TEST_BABEL || TEST_ACORN) {
// Not writing new test output at all
} else {
await writeNewOutput(list);
}
}
}
async function cli(tenko) {
let tob = new Tob('<cli>', INPUT_OVERRIDE);
tob.inputCode = INPUT_OVERRIDE;
tob.inputOptions.es = FORCED_ES_TARGET;
let list = [tob];
await runTests(list, tenko);
if (!SEARCH && !CONCISE) {
console.log('=============================================');
if (RUN_SLOPPY) {
ASSERT(list[0].newOutputSloppy !== false || list[0].newOutputSloppyAnnexB !== false, 'should update');
if (list[0].newOutputSloppy !== false) {
console.log('### Sloppy mode, annexB = false:');
console.log(list[0].newOutputSloppy);
console.log('=============================================\n');
}
if (list[0].newOutputSloppyAnnexB !== false) {
console.log('### Sloppy mode, annexB = true:');
if (list[0].newOutputSloppyAnnexB === list[0].newOutputSloppy) console.log('Same as sloppy without annexB');
else console.log(list[0].newOutputSloppyAnnexB);
console.log('=============================================\n');
}
}
if (RUN_STRICT) {
ASSERT(list[0].newOutputStrict !== false || list[0].newOutputStrictAnnexB !== false, 'should update');
if (list[0].newOutputStrict !== false) {
console.log('### Strict mode, annexB = false:');
if (RUN_SLOPPY && list[0].newOutputStrict === list[0].newOutputSloppy) console.log('Same as sloppy');
else if (RUN_SLOPPY && list[0].newOutputStrict === list[0].newOutputSloppyAnnexB) console.log('Same as sloppy with annexB');
else console.log(list[0].newOutputStrict);
console.log('=============================================\n');
}
if (list[0].newOutputStrictAnnexB !== false) {
console.log('### Strict mode, annexB = true:');
if (RUN_SLOPPY && list[0].newOutputStrictAnnexB === list[0].newOutputSloppy) console.log('Same as sloppy');
else if (RUN_SLOPPY && list[0].newOutputStrictAnnexB === list[0].newOutputSloppyAnnexB) console.log('Same as sloppy with annexB');
else if (RUN_SLOPPY && list[0].newOutputStrictAnnexB === list[0].newOutputStrict) console.log('Same as strict without annexB');
else console.log(list[0].newOutputStrictAnnexB);
console.log('=============================================\n');
}
}
if (RUN_MODULE) {
ASSERT(list[0].newOutputModule !== false || list[0].newOutputModuleAnnexB !== false, 'should update');
if (list[0].newOutputModule !== false) {
console.log('### Module goal, annexB = false:');
if (RUN_SLOPPY && list[0].newOutputModule === list[0].newOutputSloppy) console.log('Same as sloppy');
else if (RUN_SLOPPY && list[0].newOutputModule === list[0].newOutputSloppyAnnexB) console.log('Same as sloppy with annexB');
else if (RUN_STRICT && list[0].newOutputModule === list[0].newOutputStrict) console.log('Same as strict');
else if (RUN_STRICT && list[0].newOutputModule === list[0].newOutputStrictAnnexB) console.log('Same as strict with annexB');
else console.log(list[0].newOutputModule);
console.log('=============================================\n');
}
if (list[0].newOutputModuleAnnexB !== false) {
console.log('### Module goal, annexB = true:');
if (RUN_SLOPPY && list[0].newOutputModuleAnnexB === list[0].newOutputSloppy) console.log('Same as sloppy');
else if (RUN_SLOPPY && list[0].newOutputModuleAnnexB === list[0].newOutputSloppyAnnexB) console.log('Same as sloppy with annexB');
else if (RUN_STRICT && list[0].newOutputModuleAnnexB === list[0].newOutputStrict) console.log('Same as strict');
else if (RUN_STRICT && list[0].newOutputModuleAnnexB === list[0].newOutputStrictAnnexB) console.log('Same as strict with annexB');
else if (RUN_SLOPPY && list[0].newOutputModuleAnnexB === list[0].newOutputModule) console.log('Same as module without annexB');
else console.log(list[0].newOutputModuleAnnexB);
console.log('=============================================\n');
}
}
if (tob.printerOutput) console.log(tob.printerOutput[1]);
}
}
async function main(tenko) {
if (TARGET_FILE) {
console.log('Using explicit file:', TARGET_FILE);
files = [TARGET_FILE];
} else {
files = files.filter(f => !f.endsWith('autogen.md'));
}
if (ACORN_COMPAT) console.log('Forcing Acorn compat AST');
if (BABEL_COMPAT) console.log('Forcing Babel compat AST');
if (!RUN_VERBOSE_IN_SERIAL) console.time('$$ Test file read time');
let list = await readFiles(files);
if (!TARGET_FILE) list = list.filter(tob => !tob.fileShort.startsWith('tests/testcases/todo/')); // Skip todo dir unless explicitly asking for it
if (!RUN_VERBOSE_IN_SERIAL) console.timeEnd('$$ Test file read time');
console.log('Read', list.length, 'files');
await extractFiles(list);
let beforeLen = list.length;
if (!TARGET_FILE) list = list.filter(tob => !tob.aboveTheFold.toLowerCase().includes('\n## skip\n'));
console.log('Filtered', beforeLen - list.length,'skipped tests (containing `## skip`)');
if (RUN_VERBOSE_IN_SERIAL) {
for (let i=0; i<list.length && !stopAsap; ++i) {
let tob = list[i];
await runAndRegenerateList([tob], tenko);
let count = String(i+1).padStart(String(list.length).length, ' ') + ' / ' + list.length;
if (tob.compareHadMatchFailure && !tob.compareWhiteListed) {
unxepctedFails.push(tob.fileShort);
console.log(BOLD + RED + 'BAD!' + RESET + ' ' + count + ' ' + DIM + tob.fileShort + RESET + ' (file not whitelisted to fail but it failed anyways, investigate)');
} else if (tob.compareHadMatchFailure) {
ASSERT(tob.compareWhiteListed, 'then it is whitelisted');
skippedOtherParserList.push(tob.fileShort);
console.log(BOLD + GREEN + 'SKIP' + RESET + ' ' + count + ' ' + DIM + tob.fileShort + RESET + ' (file whitelisted to fail and it failed)');
} else if (tob.compareWhiteListed) {
ASSERT(!tob.compareHadMatchFailure, 'then it is whitelisted but did not fail');
unexpectedPass.push(tob.fileShort);
console.log(BOLD + RED + 'DROP' + RESET + ' ' + count + ' ' + DIM + tob.fileShort + RESET + ' (file whitelisted to fail but did not fail, remove from list)');
} else if (tob.compareSkippedExplicitVersion) {
skippedOtherParserList.push(tob.fileShort);
console.log(BOLD + GREEN + 'SKIP' + RESET + ' ' + count + ' ' + DIM + tob.fileShort + RESET + ' (file skipped because it targets a specific ES version and we dont care about those cases here)');
} else if (tob.oldData !== tob.newData) {
console.log(BOLD + RED + 'FAIL' + RESET + ' ' + count + ' ' + DIM + tob.fileShort + RESET);
} else {
console.log(BOLD + GREEN + 'PASS' + RESET + ' ' + count + ' ' + DIM + tob.fileShort + RESET);
}
}
if (skippedOtherParserList.length) {
console.log(BOLD + 'Properly ignored these files:' + RESET);
// console.log(skippedOtherParserList.sort().join('\n'))
}
if (unexpectedPass.length) {
console.log(BOLD + 'These files were whitelisted but they already match:' + RESET);
console.log(unexpectedPass.sort().join('\n'))
}
if (unxepctedFails.length) {
console.log(BOLD + 'These files did not match and were not whitelisted:' + RESET);
console.log(unxepctedFails.sort().join('\n'))
}
console.log('Match status: whitelisted: ' + skippedOtherParserList.length + 'x, unexpected misses: ' + unxepctedFails.length + 'x, unexpected passes: ' + unexpectedPass.length + 'x');
if (unxepctedFails.length || unexpectedPass.length) console.log('Use -q to stop immediately on an unexpected miss/match');
} else {
await runAndRegenerateList(list, tenko);
}
console.timeEnd('$$ Whole test run');
}
function sanitize(dir) {
return dir
.trim()
.replace(/(?:\s|;|,)+/g, '_')
.replace(/\.\.\./g, 'rest') // or spread, but bla
.replace(/[^a-zA-Z0-9_-]/g, s => s.charCodeAt(0).toString(16));
}
async function gen(tenko) {
const CASE_HEAD = '### Cases';
const TPL_HEAD = '### Templates';
const OUT_HEAD = '## Output';
files = files.filter(f => f.endsWith('autogen.md'));
let list = await readFiles(files);
for (let ti=0; ti<list.length; ++ti) {
let tob/*: Tob */ = list[ti];
let genDir = path.join(path.dirname(tob.file), 'gen');
if (fs.existsSync(genDir)) {
if (!AUTO_GENERATE_CONSERVATIVE) {
// Drop all files in this dir (this is `gen`, should be fine to fully regenerate anything in here at any time)
let oldFiles = [];
getTestFiles(genDir, '', oldFiles, true, true);
// Note: the folder should only contain generated files and folders which should delete just fine
oldFiles.forEach(file => { try { fs.unlinkSync(file); } catch (e) { fs.rmdirSync(file); } });
}
} else {
fs.mkdirSync(genDir, {recursive: true});
}
let caseOffset = tob.oldData.indexOf(CASE_HEAD);
let templateOffset = tob.oldData.indexOf(TPL_HEAD, CASE_HEAD);
let outputOffset = tob.oldData.indexOf(OUT_HEAD, TPL_HEAD);
ASSERT(caseOffset >= 0 || templateOffset >= 0 || outputOffset >= 0, 'missing required parts of autogen', tob.file);
let cases = tob.oldData
.slice(caseOffset + CASE_HEAD.length, templateOffset)
.split('> `````js\n')
.slice(1) // first element is the header
.map(s => {
// Note: code blocks start with > ``js and end with > `` and are (markdown) "quoted" throughout, + single space
// So search for the quint -``js and cut up to the next quint-``, then scrub the quoting prefix `> `
return s
.split('\n> `````')[0] // Only get the code block, don't care about the rest
.split('\n')
.map(s => {
ASSERT(s[0] === '>' && s[1] === ' ', 'cases should be md quoted entirely, with one space', tob.file, s);
return s.slice(2);
})
.join('\n'); // Not likely to be multi line but why not
})
;
let params = tob.oldData
.slice(templateOffset + TPL_HEAD.length, tob.oldData.indexOf('####', templateOffset + TPL_HEAD.length))
.split('\n')
.map(s => s.trim())
.filter(s => s[0] === '-')
.reduce((obj, s) => {
ASSERT(s[1] === ' ' && s[2] === '`' && s[s.length - 1] === '`', 'param composition', obj.file, s);
let [k, v] = s.slice(3, -1).split(' = ');
if (String(parseInt(v, 10)) === v) v = parseInt(k, 10);
else if (v === 'true') v = true;
else if (v === 'false') v = false;
else if (v === 'null') v = null;
obj[k] = v;
return obj;
}, {});
// Temlates have a header and also have a ``js codeblock
let templates = tob.oldData
.slice(templateOffset + TPL_HEAD.length, outputOffset)
.split('\n#### ')
.slice(1) // first element is the header
.map(s => {
// We split on the #### so the title should be at the start of `s` now
let title = s.split('\n')[0].trim();
// Get everything inside the js code block
let code = s.split('`````js\n')[1].split('\n`````')[0];
return {title, code};
})
;
// Now generate all cases with each # in the p