Files
esbuild/scripts/es6-fuzzer.js
2020-12-20 21:05:26 -08:00

145 lines
4.6 KiB
JavaScript

// This fuzzer attempts to find issues with the "scope hoisting" optimizations
// for ES6-style imports and exports. It compares esbuild's behavior with the
// behavior of node's experimental module support.
(async () => {
// Make sure this script runs as an ES6 module so we can import both ES6 modules and CommonJS modules
if (typeof require !== 'undefined') {
const childProcess = require('child_process')
const child = childProcess.spawn('node', ['--experimental-modules', '--input-type=module'], {
cwd: __dirname,
stdio: ['pipe', 'inherit', 'inherit'],
})
child.stdin.write(require('fs').readFileSync(__filename))
child.stdin.end()
child.on('close', code => process.exit(code))
return
}
const { default: { buildBinary, dirname, removeRecursiveSync } } = await import('./esbuild.js');
const childProcess = await import('child_process');
const util = await import('util');
const path = await import('path');
const fs = await import('fs');
const esbuildPath = buildBinary();
let failureCount = 0;
let nextTest = 0;
function reportFailure(testDir, files, kind, error) {
failureCount++;
console.log(`❌ FAILURE ${kind}: ${error}\n DIR: ${testDir}` +
Object.keys(files).map(x => `\n ${x} => ${files[x]}`).join(''));
}
function circularObjectToString(root) {
let map = new Map();
let counter = 0;
let visit = obj => {
if (typeof obj !== 'object') return JSON.stringify(obj);
if (map.has(obj)) return `$${map.get(obj)}`;
map.set(obj, counter++);
const keys = Object.keys(obj).sort();
return `$${map.get(obj)} = {${keys.map(key =>
`${JSON.stringify(key)}: ${visit(obj[key])}`).join(', ')}}`;
};
return visit(root);
}
function checkSameExportObject(a, b) {
a = circularObjectToString(a);
b = circularObjectToString(b);
if (a !== b) throw new Error(`Different exports:\n ${a}\n ${b}`);
}
async function fuzzOnce(parentDir) {
const mjs_or_cjs = () => Math.random() < 0.1 ? 'cjs' : 'mjs';
const names = [
'a.' + mjs_or_cjs(),
'b.' + mjs_or_cjs(),
'c.' + mjs_or_cjs(),
'd.' + mjs_or_cjs(),
'e.' + mjs_or_cjs(),
];
const randomName = () => names[Math.random() * names.length | 0];
const files = {};
for (const name of names) {
if (name.endsWith('.cjs')) {
files[name] = `module.exports = 123`;
} else {
switch (Math.random() * 5 | 0) {
case 0:
files[name] = `export const foo = 123`;
break;
case 1:
files[name] = `export default 123`;
break;
case 2:
files[name] = `export * from "./${randomName()}"`;
break;
case 3:
files[name] = `export * as foo from "./${randomName()}"`;
break;
case 4:
files[name] = `import * as foo from "./${randomName()}"; export {foo}`;
break;
}
}
}
// Write the files to the file system
const testDir = path.join(parentDir, (nextTest++).toString());
fs.mkdirSync(testDir);
for (const name in files) {
fs.writeFileSync(path.join(testDir, name), files[name]);
}
if (nextTest % 100 === 0) console.log(`Checked ${nextTest} test cases`);
// Load the raw module using node
const entryPoint = path.join(testDir, names[0]);
let realExports = await import(entryPoint);
if (entryPoint.endsWith('.cjs')) realExports = realExports.default;
// Bundle to a CommonJS module using esbuild
const cjsFile = path.join(testDir, 'out.cjs');
await util.promisify(childProcess.execFile)(esbuildPath, [
'--bundle',
'--outfile=' + cjsFile,
'--format=cjs',
entryPoint,
], { stdio: 'pipe' });
// Validate the CommonJS module bundle
try {
let { default: cjsExports } = await import(cjsFile);
checkSameExportObject(realExports, cjsExports);
} catch (e) {
reportFailure(testDir, files, 'cjs', e + '');
return;
}
// Remove data for successful tests
removeRecursiveSync(testDir);
}
const parentDir = path.join(dirname, '.es6-fuzzer');
removeRecursiveSync(parentDir);
fs.mkdirSync(parentDir);
// Run a set number of tests in parallel
let promises = [];
for (let i = 0; i < 10; i++) {
let promise = fuzzOnce(parentDir);
for (let j = 0; j < 100; j++) {
promise = promise.then(() => fuzzOnce(parentDir));
}
promises.push(promise);
}
await Promise.all(promises);
// Remove everything if all tests passed
if (failureCount === 0) {
removeRecursiveSync(parentDir);
}
})().catch(e => setTimeout(() => { throw e }));