mirror of
https://github.com/zhigang1992/esbuild.git
synced 2026-01-12 17:13:19 +08:00
384 lines
12 KiB
JavaScript
384 lines
12 KiB
JavaScript
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');
|
|
let U;
|
|
|
|
main().catch(e => setTimeout(() => { throw e }));
|
|
|
|
async function main() {
|
|
// // Terser's stdout comparisons fail if this is true since stdout contains
|
|
// // terminal color escape codes
|
|
// process.stdout.isTTY = false;
|
|
|
|
// Make sure the tests are installed
|
|
childProcess.execSync('make demo/uglify', { cwd: repoDir, stdio: 'pipe' });
|
|
U = require(path.join(uglifyDir, 'test', 'node'));
|
|
|
|
// Create a fresh test directory
|
|
childProcess.execSync(`rm -fr "${testDir}"`);
|
|
fs.mkdirSync(testDir)
|
|
|
|
// Start the esbuild service
|
|
const esbuild = installForTests();
|
|
|
|
// Find test files
|
|
const compressDir = path.join(uglifyDir, 'test', 'compress');
|
|
const files = fs.readdirSync(compressDir).filter(name => name.endsWith('.js'));
|
|
|
|
// Run all tests concurrently
|
|
let passedTotal = 0;
|
|
let failedTotal = 0;
|
|
const runTest = file => test_file(esbuild, path.join(compressDir, file))
|
|
.then(({ passed, failed }) => {
|
|
passedTotal += passed;
|
|
failedTotal += failed;
|
|
});
|
|
await Promise.all(files.map(runTest));
|
|
|
|
// Clean up test output
|
|
childProcess.execSync(`rm -fr "${testDir}"`);
|
|
|
|
console.log(`${failedTotal} failed out of ${passedTotal + failedTotal}`);
|
|
if (failedTotal) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function test_file(esbuild, file) {
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const tests = parse_test(file);
|
|
const runTest = name => test_case(esbuild, tests[name])
|
|
.then(() => 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 };
|
|
}
|
|
|
|
// Modified from "uglify/demo/test/compress.js"
|
|
async function test_case(esbuild, test) {
|
|
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'));
|
|
|
|
// Generate the input code
|
|
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,
|
|
});
|
|
|
|
// Make sure it's valid
|
|
try {
|
|
U.parse(input_code);
|
|
} catch (ex) {
|
|
log([
|
|
"!!! Cannot parse input",
|
|
"---INPUT---",
|
|
"{input}",
|
|
"--PARSE ERROR--",
|
|
"{error}",
|
|
"",
|
|
"",
|
|
].join("\n"), {
|
|
input: input_formatted,
|
|
error: ex,
|
|
});
|
|
}
|
|
|
|
// Ignore tests that no longer pass in modern versions of node. These tests
|
|
// contain code that is now considered a syntax error. The relevant code is
|
|
// this:
|
|
//
|
|
// try{throw 42}catch(a){console.log(a);function a(){}}
|
|
//
|
|
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;
|
|
}
|
|
|
|
// Run esbuild as a minifier
|
|
try {
|
|
var { code: output } = await esbuild.transform(input_code, {
|
|
minify: true,
|
|
target: 'es5',
|
|
});
|
|
} catch (e) {
|
|
// These two tests fail because they contain setters without arguments,
|
|
// which is a syntax error. These test failures do not indicate anything
|
|
// wrong with esbuild so the failures are ignored. Here is one of the
|
|
// tests:
|
|
//
|
|
// function f(){var a={get b(){},set b(){}};return{a:a}}
|
|
//
|
|
if (test.name === 'unsafe_object_accessor' || test.name === 'keep_name_of_setter') {
|
|
console.error("*** skipping test with known syntax error:", test.name);
|
|
return;
|
|
}
|
|
|
|
const formatError = ({ text, location }) => {
|
|
if (!location) return `\nerror: ${text}`;
|
|
const { file, line, column } = location;
|
|
return `\n${file}:${line}:${column}: error: ${text}`;
|
|
}
|
|
log("!!! esbuild failed\n---INPUT---\n{input}\n---ERROR---\n{error}\n", {
|
|
input: input_code,
|
|
error: (e && e.message || e) + '' + (e.errors ? e.errors.map(formatError) : ''),
|
|
});
|
|
}
|
|
|
|
// Make sure esbuild generates valid JavaScript
|
|
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,
|
|
});
|
|
}
|
|
|
|
// Verify that the stdout matches our expectations
|
|
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);
|
|
if (!sandbox.same_stdout(test.expect_stdout, actual)) {
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// The code below was copied verbatim from "uglify/demo/test/compress.js"
|
|
//
|
|
// UglifyJS is released under the BSD license:
|
|
//
|
|
// Copyright 2012-2019 (c) Mihai Bazon <mihai.bazon@gmail.com>
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions
|
|
// are met:
|
|
//
|
|
// * Redistributions of source code must retain the above
|
|
// copyright notice, this list of conditions and the following
|
|
// disclaimer.
|
|
//
|
|
// * Redistributions in binary form must reproduce the above
|
|
// copyright notice, this list of conditions and the following
|
|
// disclaimer in the documentation and/or other materials
|
|
// provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
|
// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
|
// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
|
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
// SUCH DAMAGE.
|
|
|
|
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");
|
|
// TODO try/catch can be removed after fixing https://github.com/mishoo/UglifyJS/issues/348
|
|
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;
|
|
}
|