mirror of
https://github.com/swc-project/swc.git
synced 2024-12-27 15:42:51 +03:00
681 lines
26 KiB
JavaScript
681 lines
26 KiB
JavaScript
import * as AST from "../lib/ast.js";
|
|
import { Compressor } from "../lib/compress/index.js";
|
|
import { OutputStream } from "../lib/output.js";
|
|
import { parse } from "../lib/parse.js";
|
|
import {
|
|
mangle_properties,
|
|
reserve_quoted_keys,
|
|
mangle_private_properties,
|
|
} from "../lib/propmangle.js";
|
|
import { base54 } from "../lib/scope.js";
|
|
import { defaults, string_template } from "../lib/utils/index.js";
|
|
import { minify } from "../main.js";
|
|
import * as sandbox from "./sandbox.js";
|
|
import assert from "assert";
|
|
import fs, { mkdirSync } from "fs";
|
|
import path from "path";
|
|
import semver from "semver";
|
|
import { fileURLToPath } from "url";
|
|
|
|
/* globals module, __dirname, console */
|
|
import "source-map-support/register.js";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
var tests_dir = __dirname;
|
|
var failed_files = {};
|
|
var minify_options = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, "ufuzz.json"), "utf-8")
|
|
).map(JSON.stringify);
|
|
|
|
run_compress_tests().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|
|
|
|
/* -----[ utils ]----- */
|
|
|
|
function HOP(obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
}
|
|
|
|
function tmpl() {
|
|
return string_template.apply(this, arguments);
|
|
}
|
|
|
|
function log() {
|
|
var txt = tmpl.apply(this, arguments);
|
|
console.log("%s", txt);
|
|
}
|
|
|
|
function log_directory(dir) {
|
|
log("*** Entering [{dir}]", { dir: dir });
|
|
}
|
|
|
|
function log_start_file(file) {
|
|
log("--- {file}", { file: file });
|
|
}
|
|
|
|
function log_test(name) {
|
|
log(" Running test [{name}]", { name: name });
|
|
}
|
|
|
|
function find_test_files(dir) {
|
|
var files = fs.readdirSync(dir).filter(function (name) {
|
|
return /\.js$/i.test(name);
|
|
});
|
|
if (process.argv.length > 2) {
|
|
var x = process.argv.slice(2);
|
|
files = files.filter(function (f) {
|
|
return x.includes(f);
|
|
});
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function test_directory(dir) {
|
|
return path.resolve(tests_dir, dir);
|
|
}
|
|
|
|
function as_toplevel(input, mangle_options) {
|
|
if (!(input instanceof AST.AST_BlockStatement))
|
|
throw new Error("Unsupported input syntax");
|
|
for (var i = 0; i < input.body.length; i++) {
|
|
var stat = input.body[i];
|
|
if (
|
|
stat instanceof AST.AST_SimpleStatement &&
|
|
stat.body instanceof AST.AST_String
|
|
)
|
|
input.body[i] = new AST.AST_Directive(stat.body);
|
|
else break;
|
|
}
|
|
var toplevel = new AST.AST_Toplevel(input);
|
|
toplevel.figure_out_scope(mangle_options);
|
|
return toplevel;
|
|
}
|
|
|
|
async function run_compress_tests() {
|
|
var failures = 0;
|
|
var dir = test_directory("compress");
|
|
log_directory("compress");
|
|
var files = find_test_files(dir);
|
|
async function test_file(file) {
|
|
log_start_file(file);
|
|
async function test_case(test) {
|
|
log_test(test.name);
|
|
const dir = path.join(
|
|
__dirname,
|
|
`../output/${file.replace(".js", "")}/${test.name}`
|
|
);
|
|
console.log(dir);
|
|
mkdirSync(dir, { recursive: true });
|
|
var output_options = test.beautify || test.format || {};
|
|
var expect;
|
|
if (test.expect) {
|
|
expect = make_code(
|
|
as_toplevel(test.expect, test.mangle),
|
|
output_options
|
|
);
|
|
} else {
|
|
expect = test.expect_exact;
|
|
}
|
|
fs.writeFileSync(path.join(dir, "output.js"), expect || "");
|
|
if (expect) {
|
|
fs.writeFileSync(
|
|
path.join(dir, "output.terser.js"),
|
|
expect || ""
|
|
);
|
|
}
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.json"),
|
|
JSON.stringify(test.options, undefined, 4)
|
|
);
|
|
if (
|
|
test.expect_error &&
|
|
(test.expect || test.expect_exact || test.expect_stdout)
|
|
) {
|
|
log(
|
|
"!!! Test cannot have an `expect_error` with other expect clauses\n",
|
|
{}
|
|
);
|
|
return false;
|
|
}
|
|
if (
|
|
test.input instanceof AST.AST_SimpleStatement &&
|
|
test.input.body instanceof AST.AST_TemplateString
|
|
) {
|
|
if (test.input.body.segments.length == 1) {
|
|
try {
|
|
var input = parse(test.input.body.segments[0].value);
|
|
} catch (ex) {
|
|
if (!test.expect_error) {
|
|
log(
|
|
"!!! Test is missing an `expect_error` clause\n",
|
|
{}
|
|
);
|
|
return false;
|
|
}
|
|
if (
|
|
test.expect_error instanceof
|
|
AST.AST_SimpleStatement &&
|
|
test.expect_error.body instanceof AST.AST_Object
|
|
) {
|
|
var expect_error = eval(
|
|
"(" +
|
|
test.expect_error.body.print_to_string() +
|
|
")"
|
|
);
|
|
var ex_normalized = JSON.parse(JSON.stringify(ex));
|
|
ex_normalized.name = ex.name;
|
|
for (var prop in expect_error) {
|
|
if (prop == "name" || HOP(expect_error, prop)) {
|
|
if (
|
|
expect_error[prop] !=
|
|
ex_normalized[prop]
|
|
) {
|
|
log(
|
|
"!!! Failed `expect_error` property `{prop}`:\n\n---expect_error---\n{expect_error}\n\n---ACTUAL exception--\n{actual_ex}\n\n",
|
|
{
|
|
prop: prop,
|
|
expect_error: JSON.stringify(
|
|
expect_error,
|
|
null,
|
|
2
|
|
),
|
|
actual_ex: JSON.stringify(
|
|
ex_normalized,
|
|
null,
|
|
2
|
|
),
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
log(
|
|
"!!! Test `expect_error` clause must be an object literal\n---expect_error---\n{expect_error}\n\n",
|
|
{
|
|
expect_error:
|
|
test.expect_error.print_to_string(),
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
var input_code = make_code(input, output_options);
|
|
var input_formatted = test.input.body.segments[0].value;
|
|
} else {
|
|
log(
|
|
"!!! Test input template string cannot use unescaped ${} expressions\n---INPUT---\n{input}\n\n",
|
|
{
|
|
input: test.input.body.print_to_string(),
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
} else if (test.expect_error) {
|
|
log(
|
|
"!!! Test cannot have an `expect_error` clause without a template string `input`\n",
|
|
{}
|
|
);
|
|
return false;
|
|
} else {
|
|
var input = as_toplevel(test.input, test.mangle);
|
|
var input_code = make_code(input, output_options);
|
|
var input_formatted = make_code(test.input, {
|
|
ecma: 2015,
|
|
beautify: true,
|
|
quote_style: 3,
|
|
keep_quoted_props: true,
|
|
});
|
|
}
|
|
try {
|
|
fs.writeFileSync(path.join(dir, "input.js"), input_code);
|
|
|
|
parse(input_code);
|
|
} catch (ex) {
|
|
log(
|
|
"!!! Cannot parse input\n---INPUT---\n{input}\n--PARSE ERROR--\n{error}\n\n",
|
|
{
|
|
input: input_formatted,
|
|
error: ex,
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
if (!test.no_mozilla_ast) {
|
|
var ast = input.to_mozilla_ast();
|
|
var mozilla_options = {
|
|
ecma: output_options.ecma,
|
|
ascii_only: output_options.ascii_only,
|
|
comments: false,
|
|
};
|
|
var ast_as_string =
|
|
AST.AST_Node.from_mozilla_ast(ast).print_to_string(
|
|
mozilla_options
|
|
);
|
|
var input_string = input.print_to_string(mozilla_options);
|
|
if (input_string !== ast_as_string) {
|
|
log(
|
|
"!!! Mozilla AST I/O corrupted input\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n\n",
|
|
{
|
|
input: input_string,
|
|
output: ast_as_string,
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
var options = defaults(test.options, {});
|
|
if (test.mangle) {
|
|
fs.writeFileSync(
|
|
path.join(dir, "mangle.json"),
|
|
JSON.stringify(test.mangle)
|
|
);
|
|
}
|
|
if (
|
|
test.mangle &&
|
|
test.mangle.properties &&
|
|
test.mangle.properties.keep_quoted
|
|
) {
|
|
var quoted_props = test.mangle.properties.reserved;
|
|
if (!Array.isArray(quoted_props)) quoted_props = [];
|
|
test.mangle.properties.reserved = quoted_props;
|
|
if (test.mangle.properties.keep_quoted !== "strict") {
|
|
reserve_quoted_keys(input, quoted_props);
|
|
}
|
|
}
|
|
if (test.rename) {
|
|
input.figure_out_scope(test.mangle);
|
|
input.expand_names(test.mangle);
|
|
}
|
|
var cmp = new Compressor(options, {
|
|
false_by_default:
|
|
options.defaults === undefined ? true : !options.defaults,
|
|
mangle_options: test.mangle,
|
|
});
|
|
var output = cmp.compress(input);
|
|
output.figure_out_scope(test.mangle);
|
|
if (test.mangle) {
|
|
output.compute_char_frequency(test.mangle);
|
|
(function (cache) {
|
|
if (!cache) return;
|
|
if (!("props" in cache)) {
|
|
cache.props = new Map();
|
|
} else if (!(cache.props instanceof Map)) {
|
|
const props = new Map();
|
|
for (const key in cache.props) {
|
|
if (
|
|
HOP(cache.props, key) &&
|
|
key.charAt(0) === "$"
|
|
) {
|
|
props.set(key.substr(1), cache.props[key]);
|
|
}
|
|
}
|
|
cache.props = props;
|
|
}
|
|
})(test.mangle.cache);
|
|
output.mangle_names(test.mangle);
|
|
mangle_private_properties(output, test.mangle);
|
|
if (test.mangle.properties) {
|
|
output = mangle_properties(output, test.mangle.properties);
|
|
}
|
|
}
|
|
output = make_code(output, output_options);
|
|
if (test.expect_stdout && typeof expect == "undefined") {
|
|
fs.writeFileSync(
|
|
path.join(dir, "expected.stdout"),
|
|
test.expect_stdout
|
|
);
|
|
// Do not verify generated `output` against `expect` or `expect_exact`.
|
|
// Rely on the pending `expect_stdout` check below.
|
|
} else if (expect != output && !process.env.TEST_NO_COMPARE) {
|
|
log(
|
|
"!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED---\n{expected}\n\n",
|
|
{
|
|
input: input_formatted,
|
|
output: output,
|
|
expected: expect,
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
try {
|
|
parse(output);
|
|
} catch (ex) {
|
|
log(
|
|
"!!! Test matched expected result but cannot parse output\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--REPARSE ERROR--\n{error}\n\n",
|
|
{
|
|
input: input_formatted,
|
|
output: output,
|
|
error: ex.stack,
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
if (
|
|
test.expect_stdout &&
|
|
(!test.node_version ||
|
|
semver.satisfies(process.version, test.node_version)) &&
|
|
!process.env.TEST_NO_SANDBOX
|
|
) {
|
|
var stdout = sandbox.run_code(input_code, test.prepend_code);
|
|
if (test.expect_stdout === true) {
|
|
test.expect_stdout = stdout;
|
|
}
|
|
if (!sandbox.same_stdout(test.expect_stdout, stdout)) {
|
|
log(
|
|
"!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n",
|
|
{
|
|
input: input_formatted,
|
|
expected_type:
|
|
typeof test.expect_stdout == "string"
|
|
? "STDOUT"
|
|
: "ERROR",
|
|
expected: test.expect_stdout,
|
|
actual_type:
|
|
typeof stdout == "string" ? "STDOUT" : "ERROR",
|
|
actual: stdout,
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
stdout = sandbox.run_code(output, test.prepend_code);
|
|
if (!sandbox.same_stdout(test.expect_stdout, stdout)) {
|
|
log(
|
|
"!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n",
|
|
{
|
|
input: input_formatted,
|
|
output: output,
|
|
expected_type:
|
|
typeof test.expect_stdout == "string"
|
|
? "STDOUT"
|
|
: "ERROR",
|
|
expected: test.expect_stdout,
|
|
actual_type:
|
|
typeof stdout == "string" ? "STDOUT" : "ERROR",
|
|
actual: stdout,
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
if (
|
|
test.reminify &&
|
|
!(await reminify(test, input_code, input_formatted))
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
var tests = parse_test(path.resolve(dir, file));
|
|
for (var i in tests)
|
|
if (tests.hasOwnProperty(i)) {
|
|
if (!(await test_case(tests[i]))) {
|
|
failures++;
|
|
failed_files[file] = 1;
|
|
if (process.env.TEST_FAIL_FAST) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
for (const file of files) {
|
|
if (!(await test_file(file))) {
|
|
break;
|
|
}
|
|
}
|
|
if (failures) {
|
|
console.error("\n!!! Failed " + failures + " test cases.");
|
|
console.error("!!! " + Object.keys(failed_files).join(", "));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function parse_test(file) {
|
|
var script = fs.readFileSync(file, "utf8");
|
|
// TODO try/catch can be removed after fixing https://github.com/mishoo/UglifyJS2/issues/348
|
|
try {
|
|
var ast = parse(script, {
|
|
filename: file,
|
|
});
|
|
} catch (e) {
|
|
console.log("Caught error while parsing tests in " + file + "\n");
|
|
console.log(e);
|
|
throw e;
|
|
}
|
|
var tests = {};
|
|
var tw = new AST.TreeWalker(function (node, descend) {
|
|
if (
|
|
node instanceof AST.AST_LabeledStatement &&
|
|
tw.parent() instanceof AST.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 AST.AST_Directive) return true;
|
|
if (!(node instanceof AST.AST_Toplevel)) croak(node);
|
|
});
|
|
ast.walk(tw);
|
|
|
|
const only_tests = Object.entries(tests).filter(
|
|
([_name, test]) => test.only
|
|
);
|
|
return only_tests.length > 0 ? Object.fromEntries(only_tests) : 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_boolean(stat) {
|
|
if (stat.TYPE == "SimpleStatement") {
|
|
var body = stat.body;
|
|
if (body instanceof AST.AST_Boolean) {
|
|
return body.value;
|
|
}
|
|
}
|
|
throw new Error("Should be boolean");
|
|
}
|
|
|
|
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: {},
|
|
reminify: true,
|
|
only: false,
|
|
};
|
|
var tw = new AST.TreeWalker(function (node, descend) {
|
|
if (node instanceof AST.AST_Assign) {
|
|
if (!(node.left instanceof AST.AST_SymbolRef)) {
|
|
croak(node);
|
|
}
|
|
var name = node.left.name;
|
|
test[name] = evaluate(node.right);
|
|
return true;
|
|
}
|
|
if (node instanceof AST.AST_LabeledStatement) {
|
|
var label = node.label;
|
|
assert.ok(
|
|
[
|
|
"input",
|
|
"prepend_code",
|
|
"expect",
|
|
"expect_error",
|
|
"expect_exact",
|
|
"expect_stdout",
|
|
"node_version",
|
|
"reminify",
|
|
].includes(label.name),
|
|
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 == "reminify") {
|
|
var value = read_boolean(stat);
|
|
test.reminify = value == null || value;
|
|
} else if (label.name == "expect_stdout") {
|
|
var body = stat.body;
|
|
if (body instanceof AST.AST_Boolean) {
|
|
test[label.name] = body.value;
|
|
} else if (body instanceof AST.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 AST.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 if (label.name === "prepend_code") {
|
|
test[label.name] = read_string(stat);
|
|
} else {
|
|
test[label.name] = stat;
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
block.walk(tw);
|
|
return test;
|
|
}
|
|
}
|
|
|
|
function make_code(ast, options) {
|
|
var stream = OutputStream(options);
|
|
ast.print(stream);
|
|
return stream.get();
|
|
}
|
|
|
|
function evaluate(code) {
|
|
if (code instanceof AST.AST_Node)
|
|
code = make_code(code, { beautify: true });
|
|
return new Function("return(" + code + ")")();
|
|
}
|
|
|
|
// Try to reminify original input with standard options
|
|
// to see if it matches expect_stdout.
|
|
async function reminify(test, input_code, input_formatted) {
|
|
if (process.env.TEST_NO_REMINIFY) return true;
|
|
const { options: orig_options, expect_stdout } = test;
|
|
for (var i = 0; i < minify_options.length; i++) {
|
|
var options = JSON.parse(minify_options[i]);
|
|
options.keep_fnames = orig_options.keep_fnames;
|
|
options.keep_classnames = orig_options.keep_classnames;
|
|
if (orig_options.compress) {
|
|
options.compress.keep_classnames =
|
|
orig_options.compress.keep_classnames;
|
|
options.compress.keep_fargs = orig_options.compress.keep_fargs;
|
|
options.compress.keep_fnames = orig_options.compress.keep_fnames;
|
|
}
|
|
if (orig_options.mangle) {
|
|
options.mangle.keep_classnames =
|
|
orig_options.mangle.keep_classnames;
|
|
options.mangle.keep_fnames = orig_options.mangle.keep_fnames;
|
|
}
|
|
var options_formatted = JSON.stringify(options, null, 4);
|
|
var result = await minify(input_code, options);
|
|
if (result.error) {
|
|
log(
|
|
"!!! failed input reminify\n---INPUT---\n{input}\n--ERROR---\n{error}\n\n",
|
|
{
|
|
input: input_formatted,
|
|
error: result.error.stack,
|
|
}
|
|
);
|
|
return false;
|
|
} else if (!process.env.TEST_NO_SANDBOX) {
|
|
var stdout = sandbox.run_code(result.code, test.prepend_code);
|
|
if (
|
|
typeof expect_stdout != "string" &&
|
|
typeof stdout != "string" &&
|
|
expect_stdout.name == stdout.name
|
|
) {
|
|
stdout = expect_stdout;
|
|
}
|
|
if (!sandbox.same_stdout(expect_stdout, stdout)) {
|
|
log(
|
|
"!!! failed running reminified input\n---INPUT---\n{input}\n---OPTIONS---\n{options}\n---OUTPUT---\n{output}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n",
|
|
{
|
|
input: input_formatted,
|
|
options: options_formatted,
|
|
output: result.code,
|
|
expected_type:
|
|
typeof expect_stdout == "string"
|
|
? "STDOUT"
|
|
: "ERROR",
|
|
expected: expect_stdout,
|
|
actual_type:
|
|
typeof stdout == "string" ? "STDOUT" : "ERROR",
|
|
actual: stdout,
|
|
}
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|