const { installForTests } = require('./esbuild');
const childProcess = require('child_process');
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const repoDir = path.dirname(__dirname);
const testDir = path.join(repoDir, 'scripts', '.uglify-tests');
const uglifyDir = path.join(repoDir, 'demo', 'uglify');
const SKIP = {};
let U;
main().catch(e => setTimeout(() => { throw e }));
async function main() {
childProcess.execSync('make demo/uglify', { cwd: repoDir, stdio: 'pipe' });
U = require(path.join(uglifyDir, 'test', 'node'));
childProcess.execSync(`rm -fr "${testDir}"`);
fs.mkdirSync(testDir)
const esbuild = installForTests();
const compressDir = path.join(uglifyDir, 'test', 'compress');
const files = fs.readdirSync(compressDir).filter(name => name.endsWith('.js'));
let passedTotal = 0;
let failedTotal = 0;
let skippedTotal = 0;
const runTest = file => test_file(esbuild, path.join(compressDir, file))
.then(({ passed, failed, skipped }) => {
passedTotal += passed;
failedTotal += failed;
skippedTotal += skipped;
});
await Promise.all(files.map(runTest));
childProcess.execSync(`rm -fr "${testDir}"`);
console.log(`${failedTotal} failed out of ${passedTotal + failedTotal}, with ${skippedTotal} skipped`);
if (failedTotal) {
process.exit(1);
}
}
async function test_file(esbuild, file) {
let passed = 0;
let failed = 0;
let skipped = 0;
const tests = parse_test(file);
const runTest = name => test_case(esbuild, tests[name], path.basename(file))
.then(x => {
if (x === SKIP) {
skipped++;
} else {
passed++;
}
})
.catch(e => {
failed++;
console.error(`❌ ${file}: ${name}: ${(e && e.message || e).trim()}\n`);
pass = false;
});
await Promise.all(Object.keys(tests).map(runTest));
return { passed, failed, skipped };
}
async function test_case(esbuild, test, basename) {
const sandbox = require(path.join(uglifyDir, 'test', 'sandbox'));
const log = (format, args) => { throw new Error(tmpl(format, args)); };
var semver = require(path.join(uglifyDir, 'node_modules', 'semver'));
var input = to_toplevel(test.input, test.mangle);
var input_code = make_code(input);
var input_formatted = make_code(test.input, {
beautify: true,
comments: "all",
keep_quoted_props: true,
quote_style: 3,
});
try {
U.parse(input_code);
} catch (ex) {
log([
"!!! Cannot parse input",
"---INPUT---",
"{input}",
"--PARSE ERROR--",
"{error}",
"",
"",
].join("\n"), {
input: input_formatted,
error: ex,
});
}
if (test.node_version && !semver.satisfies(process.version, test.node_version)) {
console.error("*** skipping test %j with node_version %j", test.name, test.node_version);
return SKIP;
}
try {
var { code: output } = await esbuild.transform(input_code, {
minify: true,
target: 'esnext',
});
} catch (e) {
if ([
'const.js: issue_4290_1',
'const.js: issue_4305_2',
'const.js: retain_catch',
'const.js: skip_braces',
'exports.js: defaults',
'exports.js: drop_unused',
'exports.js: hoist_exports_1',
'exports.js: hoist_exports_2',
'exports.js: keep_return_values',
'exports.js: mangle_rename',
'exports.js: mangle',
'exports.js: refs',
'imports.js: issue_4708_1',
'imports.js: issue_4708_2',
'let.js: issue_4290_1',
'let.js: issue_4305_2',
'let.js: retain_catch',
'let.js: skip_braces',
'reduce_vars.js: defun_catch_4',
'reduce_vars.js: defun_catch_5',
'templates.js: malformed_evaluate_1',
'templates.js: malformed_evaluate_2',
'templates.js: malformed_evaluate_3',
'varify.js: issue_4290_1_const',
'varify.js: issue_4290_1_let',
].indexOf(`${basename}: ${test.name}`) >= 0) {
console.error(`*** skipping test with known syntax error: ${basename}: ${test.name}`);
return SKIP;
}
if ([
'awaits.js: defun_name',
'awaits.js: drop_fname',
'awaits.js: functions_anonymous',
'awaits.js: functions_inner_var',
'awaits.js: issue_4335_1',
'awaits.js: keep_fname',
'classes.js: await',
].indexOf(`${basename}: ${test.name}`) >= 0) {
console.error(`*** skipping test with top-level await as identifier: ${basename}: ${test.name}`);
return SKIP;
}
if ([
'const.js: issue_4212_1',
'const.js: issue_4212_2',
].indexOf(`${basename}: ${test.name}`) >= 0) {
console.error(`*** skipping test with assignment to an inlined constant: ${basename}: ${test.name}`);
return SKIP;
}
log("!!! esbuild failed\n---INPUT---\n{input}\n---ERROR---\n{error}\n", {
input: input_code,
error: e && e.message || e,
});
}
try {
U.parse(output);
} catch (ex) {
log([
"!!! Test matched expected result but cannot parse output",
"---INPUT---",
"{input}",
"---OUTPUT---",
"{output}",
"--REPARSE ERROR--",
"{error}",
"",
"",
].join("\n"), {
input: input_formatted,
output: output,
error: ex && ex.stack || ex,
});
}
if (test.expect_stdout && (!test.node_version || semver.satisfies(process.version, test.node_version))) {
var stdout = [run_code(input_code), run_code(input_code, true)];
var toplevel = sandbox.has_toplevel({
compress: test.options,
mangle: test.mangle
});
var actual = stdout[toplevel ? 1 : 0];
if (test.expect_stdout === true) {
test.expect_stdout = actual;
}
actual = run_code(output, toplevel);
const isExpectingFailure = !process.env.CI ? false : [
'classes.js: issue_5015_2',
'const.js: issue_4225',
'const.js: issue_4229',
'const.js: issue_4245',
'const.js: use_before_init_3',
'destructured.js: funarg_side_effects_2',
'destructured.js: funarg_side_effects_3',
'let.js: issue_4225',
'let.js: issue_4229',
'let.js: issue_4245',
'let.js: use_before_init_3',
'dead-code.js: dead_code_2_should_warn',
].indexOf(`${basename}: ${test.name}`) >= 0
if (!sandbox.same_stdout(test.expect_stdout, actual)) {
if (isExpectingFailure) {
console.error(`*** skipping test with known esbuild failure: ${basename}: ${test.name}`);
return SKIP;
}
log([
"!!! failed",
"---INPUT---",
"{input}",
"---EXPECTED {expected_type}---",
"{expected}",
"---ACTUAL {actual_type}---",
"{actual}",
"",
"",
].join("\n"), {
input: input_formatted,
expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR",
expected: test.expect_stdout,
actual_type: typeof actual == "string" ? "STDOUT" : "ERROR",
actual: actual,
});
} else if (isExpectingFailure) {
throw new Error(`UPDATE NEEDED: expected failure for ${basename}: ${test.name}, please remove this test from known failure list`);
}
}
}
function evaluate(code) {
if (code instanceof U.AST_Node) code = make_code(code, { beautify: true });
return new Function("return(" + code + ")")();
}
function make_code(ast, options) {
var stream = U.OutputStream(options);
ast.print(stream);
return stream.get();
}
function parse_test(file) {
var script = fs.readFileSync(file, "utf8");
try {
var ast = U.parse(script, {
filename: file
});
} catch (e) {
console.error("Caught error while parsing tests in " + file);
console.error(e);
process.exit(1);
}
var tests = Object.create(null);
var tw = new U.TreeWalker(function (node, descend) {
if (node instanceof U.AST_LabeledStatement
&& tw.parent() instanceof U.AST_Toplevel) {
var name = node.label.name;
if (name in tests) {
throw new Error('Duplicated test name "' + name + '" in ' + file);
}
tests[name] = get_one_test(name, node.body);
return true;
}
if (!(node instanceof U.AST_Toplevel)) croak(node);
});
ast.walk(tw);
return tests;
function croak(node) {
throw new Error(tmpl("Can't understand test file {file} [{line},{col}]\n{code}", {
file: file,
line: node.start.line,
col: node.start.col,
code: make_code(node, { beautify: false })
}));
}
function read_string(stat) {
if (stat.TYPE == "SimpleStatement") {
var body = stat.body;
switch (body.TYPE) {
case "String":
return body.value;
case "Array":
return body.elements.map(function (element) {
if (element.TYPE !== "String")
throw new Error("Should be array of strings");
return element.value;
}).join("\n");
}
}
throw new Error("Should be string or array of strings");
}
function get_one_test(name, block) {
var test = { name: name, options: {} };
var tw = new U.TreeWalker(function (node, descend) {
if (node instanceof U.AST_Assign) {
if (!(node.left instanceof U.AST_SymbolRef)) {
croak(node);
}
var name = node.left.name;
test[name] = evaluate(node.right);
return true;
}
if (node instanceof U.AST_LabeledStatement) {
var label = node.label;
assert.ok([
"input",
"expect",
"expect_exact",
"expect_warnings",
"expect_stdout",
"node_version",
].indexOf(label.name) >= 0, tmpl("Unsupported label {name} [{line},{col}]", {
name: label.name,
line: label.start.line,
col: label.start.col
}));
var stat = node.body;
if (label.name == "expect_exact" || label.name == "node_version") {
test[label.name] = read_string(stat);
} else if (label.name == "expect_stdout") {
var body = stat.body;
if (body instanceof U.AST_Boolean) {
test[label.name] = body.value;
} else if (body instanceof U.AST_Call) {
var ctor = global[body.expression.name];
assert.ok(ctor === Error || ctor.prototype instanceof Error, tmpl("Unsupported expect_stdout format [{line},{col}]", {
line: label.start.line,
col: label.start.col
}));
test[label.name] = ctor.apply(null, body.args.map(function (node) {
assert.ok(node instanceof U.AST_Constant, tmpl("Unsupported expect_stdout format [{line},{col}]", {
line: label.start.line,
col: label.start.col
}));
return node.value;
}));
} else {
test[label.name] = read_string(stat) + "\n";
}
} else {
test[label.name] = stat;
}
return true;
}
});
block.walk(tw);
return test;
}
}
function run_code(code, toplevel) {
const sandbox = require(path.join(uglifyDir, 'test', 'sandbox'));
var result = sandbox.run_code(code, toplevel);
return typeof result == "string" ? result.replace(/\u001b\[\d+m/g, "") : result;
}
function tmpl() {
return U.string_template.apply(null, arguments);
}
function to_toplevel(input, mangle_options) {
if (!(input instanceof U.AST_BlockStatement)) throw new Error("Unsupported input syntax");
var directive = true;
var offset = input.start.line;
var tokens = [];
var toplevel = new U.AST_Toplevel(input.transform(new U.TreeTransformer(function (node) {
if (U.push_uniq(tokens, node.start)) node.start.line -= offset;
if (!directive || node === input) return;
if (node instanceof U.AST_SimpleStatement && node.body instanceof U.AST_String) {
return new U.AST_Directive(node.body);
} else {
directive = false;
}
})));
toplevel.figure_out_scope(mangle_options);
return toplevel;
}