speed up --reduce-test (#3726)
- avoid pathological test case branches via adaptive time-out - use initial test case elapsed time to adjust maximum time-out - index output cache using hash instead of raw source
This commit is contained in:
@@ -2,6 +2,7 @@ var assert = require("assert");
|
|||||||
var exec = require("child_process").exec;
|
var exec = require("child_process").exec;
|
||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
var reduce_test = require("../reduce");
|
var reduce_test = require("../reduce");
|
||||||
|
var semver = require("semver");
|
||||||
|
|
||||||
function read(path) {
|
function read(path) {
|
||||||
return fs.readFileSync(path, "utf8");
|
return fs.readFileSync(path, "utf8");
|
||||||
@@ -43,7 +44,6 @@ describe("test/reduce.js", function() {
|
|||||||
"// {",
|
"// {",
|
||||||
'// "toplevel": true',
|
'// "toplevel": true',
|
||||||
"// }",
|
"// }",
|
||||||
"",
|
|
||||||
].join("\n"));
|
].join("\n"));
|
||||||
});
|
});
|
||||||
it("Should handle test result of NaN", function() {
|
it("Should handle test result of NaN", function() {
|
||||||
@@ -55,7 +55,6 @@ describe("test/reduce.js", function() {
|
|||||||
'// "compress": {},',
|
'// "compress": {},',
|
||||||
'// "mangle": false',
|
'// "mangle": false',
|
||||||
"// }",
|
"// }",
|
||||||
"",
|
|
||||||
].join("\n"));
|
].join("\n"));
|
||||||
});
|
});
|
||||||
it("Should print correct output for irreducible test case", function() {
|
it("Should print correct output for irreducible test case", function() {
|
||||||
@@ -136,4 +135,33 @@ describe("test/reduce.js", function() {
|
|||||||
"// }",
|
"// }",
|
||||||
].join("\n"));
|
].join("\n"));
|
||||||
});
|
});
|
||||||
|
it("Should reduce infinite loops with reasonable performance", function() {
|
||||||
|
if (semver.satisfies(process.version, "0.10")) return;
|
||||||
|
this.timeout(120000);
|
||||||
|
var code = [
|
||||||
|
"var a = 9007199254740992, b = 1;",
|
||||||
|
"",
|
||||||
|
"while (a++ + (1 - b) < a) {",
|
||||||
|
" 0;",
|
||||||
|
"}",
|
||||||
|
].join("\n");
|
||||||
|
var result = reduce_test(code, {
|
||||||
|
compress: {
|
||||||
|
unsafe_math: true,
|
||||||
|
},
|
||||||
|
mangle: false,
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
assert.strictEqual(result.code.replace(/ timed out after [0-9]+ms/, " timed out."), [
|
||||||
|
code,
|
||||||
|
"// output: ",
|
||||||
|
"// minify: Error: Script execution timed out.",
|
||||||
|
"// options: {",
|
||||||
|
'// "compress": {',
|
||||||
|
'// "unsafe_math": true',
|
||||||
|
"// },",
|
||||||
|
'// "mangle": false',
|
||||||
|
"// }",
|
||||||
|
].join("\n"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
var crypto = require("crypto");
|
||||||
var U = require("./node");
|
var U = require("./node");
|
||||||
var List = U.List;
|
var List = U.List;
|
||||||
var sandbox = require("./sandbox");
|
var sandbox = require("./sandbox");
|
||||||
@@ -17,21 +18,32 @@ var sandbox = require("./sandbox");
|
|||||||
// Returns a `minify` result object with an additonal boolean property `reduced`.
|
// Returns a `minify` result object with an additonal boolean property `reduced`.
|
||||||
|
|
||||||
module.exports = function reduce_test(testcase, minify_options, reduce_options) {
|
module.exports = function reduce_test(testcase, minify_options, reduce_options) {
|
||||||
|
if (testcase instanceof U.AST_Node) testcase = testcase.print_to_string();
|
||||||
minify_options = minify_options || { compress: {}, mangle: false };
|
minify_options = minify_options || { compress: {}, mangle: false };
|
||||||
reduce_options = reduce_options || {};
|
reduce_options = reduce_options || {};
|
||||||
var max_iterations = reduce_options.max_iterations || 1000;
|
var max_iterations = reduce_options.max_iterations || 1000;
|
||||||
var max_timeout = reduce_options.max_timeout || 15000;
|
var max_timeout = reduce_options.max_timeout || 10000;
|
||||||
var verbose = reduce_options.verbose;
|
var verbose = reduce_options.verbose;
|
||||||
var minify_options_json = JSON.stringify(minify_options, null, 2);
|
var minify_options_json = JSON.stringify(minify_options, null, 2);
|
||||||
var timeout = 1000; // start with a low timeout
|
|
||||||
var result_cache = Object.create(null);
|
var result_cache = Object.create(null);
|
||||||
var differs;
|
|
||||||
|
|
||||||
if (testcase instanceof U.AST_Node) testcase = testcase.print_to_string();
|
|
||||||
|
|
||||||
// the initial timeout to assess the viability of the test case must be large
|
// the initial timeout to assess the viability of the test case must be large
|
||||||
if (differs = producesDifferentResultWhenMinified(result_cache, testcase, minify_options, max_timeout)) {
|
var differs = producesDifferentResultWhenMinified(result_cache, testcase, minify_options, max_timeout);
|
||||||
if (differs.error) return differs;
|
|
||||||
|
if (!differs) {
|
||||||
|
// same stdout result produced when minified
|
||||||
|
return {
|
||||||
|
code: "// Can't reproduce test failure with minify options provided:"
|
||||||
|
+ "\n// " + to_comment(minify_options_json)
|
||||||
|
};
|
||||||
|
} else if (differs.timed_out) {
|
||||||
|
return {
|
||||||
|
code: "// Can't reproduce test failure within " + max_timeout + "ms:"
|
||||||
|
+ "\n// " + to_comment(minify_options_json)
|
||||||
|
};
|
||||||
|
} else if (differs.error) {
|
||||||
|
return differs;
|
||||||
|
} else {
|
||||||
|
max_timeout = Math.min(100 * differs.elapsed, max_timeout);
|
||||||
// Replace expressions with constants that will be parsed into
|
// Replace expressions with constants that will be parsed into
|
||||||
// AST_Nodes as required. Each AST_Node has its own permutation count,
|
// AST_Nodes as required. Each AST_Node has its own permutation count,
|
||||||
// so these replacements can't be shared.
|
// so these replacements can't be shared.
|
||||||
@@ -400,15 +412,11 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options)
|
|||||||
console.error("*** Discarding permutation and continuing.");
|
console.error("*** Discarding permutation and continuing.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var diff = producesDifferentResultWhenMinified(result_cache, code, minify_options, timeout);
|
var diff = producesDifferentResultWhenMinified(result_cache, code, minify_options, max_timeout);
|
||||||
if (diff) {
|
if (diff) {
|
||||||
if (diff.timed_out) {
|
if (diff.timed_out) {
|
||||||
// can't trust the validity of `code_ast` and `code` when timed out.
|
// can't trust the validity of `code_ast` and `code` when timed out.
|
||||||
// no harm done - just ignore latest change and continue iterating.
|
// no harm done - just ignore latest change and continue iterating.
|
||||||
if (timeout < max_timeout) {
|
|
||||||
timeout += 250;
|
|
||||||
result_cache = Object.create(null);
|
|
||||||
}
|
|
||||||
} else if (diff.error) {
|
} else if (diff.error) {
|
||||||
// something went wrong during minify() - could be malformed AST or genuine bug.
|
// something went wrong during minify() - could be malformed AST or genuine bug.
|
||||||
// no harm done - just log code & error, ignore latest change and continue iterating.
|
// no harm done - just log code & error, ignore latest change and continue iterating.
|
||||||
@@ -433,23 +441,23 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options)
|
|||||||
console.error("// reduce test pass " + pass + ": " + testcase.length + " bytes");
|
console.error("// reduce test pass " + pass + ": " + testcase.length + " bytes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testcase += "\n// output: " + to_comment(differs.unminified_result)
|
testcase = U.minify(testcase, {
|
||||||
+ "\n// minify: " + to_comment(differs.minified_result)
|
compress: false,
|
||||||
+ "\n// options: " + to_comment(minify_options_json);
|
mangle: false,
|
||||||
} else {
|
output: {
|
||||||
// same stdout result produced when minified
|
beautify: true,
|
||||||
testcase = "// Can't reproduce test failure with minify options provided:"
|
braces: true,
|
||||||
+ "\n// " + to_comment(minify_options_json);
|
comments: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
testcase.code += [
|
||||||
|
"",
|
||||||
|
"// output: " + to_comment(differs.unminified_result),
|
||||||
|
"// minify: " + to_comment(differs.minified_result),
|
||||||
|
"// options: " + to_comment(minify_options_json),
|
||||||
|
].join("\n").replace(/\u001b\[\d+m/g, "");
|
||||||
|
return testcase;
|
||||||
}
|
}
|
||||||
return U.minify(testcase.replace(/\u001b\[\d+m/g, ""), {
|
|
||||||
compress: false,
|
|
||||||
mangle: false,
|
|
||||||
output: {
|
|
||||||
beautify: true,
|
|
||||||
braces: true,
|
|
||||||
comments: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function to_comment(value) {
|
function to_comment(value) {
|
||||||
@@ -489,6 +497,10 @@ function is_error(result) {
|
|||||||
return typeof result == "object" && typeof result.name == "string" && typeof result.message == "string";
|
return typeof result == "object" && typeof result.name == "string" && typeof result.message == "string";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function is_timed_out(result) {
|
||||||
|
return is_error(result) && /timed out/.test(result);
|
||||||
|
}
|
||||||
|
|
||||||
function is_statement(node) {
|
function is_statement(node) {
|
||||||
return node instanceof U.AST_Statement && !(node instanceof U.AST_Function);
|
return node instanceof U.AST_Statement && !(node instanceof U.AST_Function);
|
||||||
}
|
}
|
||||||
@@ -519,23 +531,30 @@ function to_statement(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function run_code(result_cache, code, toplevel, timeout) {
|
function run_code(result_cache, code, toplevel, timeout) {
|
||||||
return result_cache[code] || (result_cache[code] = sandbox.run_code(code, toplevel, timeout));
|
var key = crypto.createHash("sha1").update(code).digest("base64");
|
||||||
|
return result_cache[key] || (result_cache[key] = sandbox.run_code(code, toplevel, timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
function producesDifferentResultWhenMinified(result_cache, code, minify_options, timeout) {
|
function producesDifferentResultWhenMinified(result_cache, code, minify_options, max_timeout) {
|
||||||
var minified = U.minify(code, minify_options);
|
var minified = U.minify(code, minify_options);
|
||||||
if (minified.error) return minified;
|
if (minified.error) return minified;
|
||||||
|
|
||||||
var toplevel = minify_options.toplevel;
|
var toplevel = minify_options.toplevel;
|
||||||
var unminified_result = run_code(result_cache, code, toplevel, timeout);
|
var elapsed = Date.now();
|
||||||
if (/timed out/i.test(unminified_result)) return false;
|
var unminified_result = run_code(result_cache, code, toplevel, max_timeout);
|
||||||
|
elapsed = Date.now() - elapsed;
|
||||||
|
var timeout = Math.min(100 * elapsed, max_timeout);
|
||||||
var minified_result = run_code(result_cache, minified.code, toplevel, timeout);
|
var minified_result = run_code(result_cache, minified.code, toplevel, timeout);
|
||||||
if (/timed out/i.test(minified_result)) return { timed_out: true };
|
|
||||||
|
|
||||||
return !sandbox.same_stdout(unminified_result, minified_result) ? {
|
if (sandbox.same_stdout(unminified_result, minified_result)) {
|
||||||
|
return is_timed_out(unminified_result) && is_timed_out(minified_result) && {
|
||||||
|
timed_out: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
unminified_result: unminified_result,
|
unminified_result: unminified_result,
|
||||||
minified_result: minified_result,
|
minified_result: minified_result,
|
||||||
} : false;
|
elapsed: elapsed,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Error.stackTraceLimit = Infinity;
|
Error.stackTraceLimit = Infinity;
|
||||||
|
|||||||
Reference in New Issue
Block a user