speed up `--reduce-test` (#3726)
authorAlex Lam S.L <alexlamsl@gmail.com>
Mon, 17 Feb 2020 15:35:07 +0000 (15:35 +0000)
committerGitHub <noreply@github.com>
Mon, 17 Feb 2020 15:35:07 +0000 (15:35 +0000)
- 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

test/mocha/reduce.js
test/reduce.js

index f55c278..c7d5a7d 100644 (file)
@@ -2,6 +2,7 @@ var assert = require("assert");
 var exec = require("child_process").exec;
 var fs = require("fs");
 var reduce_test = require("../reduce");
+var semver = require("semver");
 
 function read(path) {
     return fs.readFileSync(path, "utf8");
@@ -43,7 +44,6 @@ describe("test/reduce.js", function() {
             "// {",
             '//   "toplevel": true',
             "// }",
-            "",
         ].join("\n"));
     });
     it("Should handle test result of NaN", function() {
@@ -55,7 +55,6 @@ describe("test/reduce.js", function() {
             '//   "compress": {},',
             '//   "mangle": false',
             "// }",
-            "",
         ].join("\n"));
     });
     it("Should print correct output for irreducible test case", function() {
@@ -136,4 +135,33 @@ describe("test/reduce.js", function() {
             "// }",
         ].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"));
+    });
 });
index 0b31ea8..0d803aa 100644 (file)
@@ -1,3 +1,4 @@
+var crypto = require("crypto");
 var U = require("./node");
 var List = U.List;
 var sandbox = require("./sandbox");
@@ -17,21 +18,32 @@ var sandbox = require("./sandbox");
 // Returns a `minify` result object with an additonal boolean property `reduced`.
 
 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 };
     reduce_options = reduce_options || {};
     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 minify_options_json = JSON.stringify(minify_options, null, 2);
-    var timeout = 1000; // start with a low timeout
     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
-    if (differs = producesDifferentResultWhenMinified(result_cache, testcase, minify_options, max_timeout)) {
-        if (differs.error) return differs;
+    var differs = producesDifferentResultWhenMinified(result_cache, testcase, minify_options, max_timeout);
+
+    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
         // AST_Nodes as required.  Each AST_Node has its own permutation count,
         // 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.");
                     continue;
                 }
-                var diff = producesDifferentResultWhenMinified(result_cache, code, minify_options, timeout);
+                var diff = producesDifferentResultWhenMinified(result_cache, code, minify_options, max_timeout);
                 if (diff) {
                     if (diff.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.
-                        if (timeout < max_timeout) {
-                            timeout += 250;
-                            result_cache = Object.create(null);
-                        }
                     } else if (diff.error) {
                         // 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.
@@ -433,23 +441,23 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options)
                 console.error("// reduce test pass " + pass + ": " + testcase.length + " bytes");
             }
         }
-        testcase += "\n// output: " + to_comment(differs.unminified_result)
-            + "\n// minify: " + to_comment(differs.minified_result)
-            + "\n// options: " + to_comment(minify_options_json);
-    } else {
-        // same stdout result produced when minified
-        testcase = "// Can't reproduce test failure with minify options provided:"
-            + "\n// " + to_comment(minify_options_json);
+        testcase = U.minify(testcase, {
+            compress: false,
+            mangle: false,
+            output: {
+                beautify: true,
+                braces: true,
+                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) {
@@ -489,6 +497,10 @@ function is_error(result) {
     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) {
     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) {
-    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);
     if (minified.error) return minified;
 
     var toplevel = minify_options.toplevel;
-    var unminified_result = run_code(result_cache, code, toplevel, timeout);
-    if (/timed out/i.test(unminified_result)) return false;
-
+    var elapsed = Date.now();
+    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);
-    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,
         minified_result: minified_result,
-    } : false;
+        elapsed: elapsed,
+    };
 }
 Error.stackTraceLimit = Infinity;