github.com/evanw/esbuild@v0.21.4/scripts/terser-tests.js (about)

     1  const { installForTests } = require('./esbuild');
     2  const childProcess = require('child_process');
     3  const assert = require('assert');
     4  const path = require('path');
     5  const fs = require('fs');
     6  
     7  const repoDir = path.dirname(__dirname);
     8  const testDir = path.join(repoDir, 'scripts', '.terser-tests');
     9  const terserDir = path.join(repoDir, 'demo', 'terser');
    10  let U;
    11  
    12  main().catch(e => setTimeout(() => { throw e }));
    13  
    14  async function main() {
    15    // Terser's stdout comparisons fail if this is true since stdout contains
    16    // terminal color escape codes
    17    process.stdout.isTTY = false;
    18  
    19    // Make sure the tests are installed
    20    console.log('Downloading terser...');
    21    childProcess.execSync('make demo/terser', { cwd: repoDir, stdio: 'pipe' });
    22    U = require(terserDir);
    23  
    24    // Create a fresh test directory
    25    childProcess.execSync(`rm -fr "${testDir}"`);
    26    fs.mkdirSync(testDir)
    27  
    28    // Start the esbuild service
    29    const esbuild = installForTests();
    30  
    31    // Find test files
    32    const compressDir = path.join(terserDir, 'test', 'compress');
    33    const files = fs.readdirSync(compressDir).filter(name => name.endsWith('.js'));
    34  
    35    // Run all tests concurrently
    36    let passedTotal = 0;
    37    let failedTotal = 0;
    38    const runTest = file => test_file(esbuild, path.join(compressDir, file))
    39      .then(({ passed, failed }) => {
    40        passedTotal += passed;
    41        failedTotal += failed;
    42      });
    43    await Promise.all(files.map(runTest));
    44  
    45    // Clean up test output
    46    childProcess.execSync(`rm -fr "${testDir}"`);
    47  
    48    console.log(`${failedTotal} failed out of ${passedTotal + failedTotal}`);
    49    if (failedTotal) {
    50      process.exit(1);
    51    }
    52  }
    53  
    54  async function test_file(esbuild, file) {
    55    let passed = 0;
    56    let failed = 0;
    57    const tests = parse_test(file);
    58    const runTest = name => test_case(esbuild, tests[name])
    59      .then(() => passed++)
    60      .catch(e => {
    61        failed++;
    62        console.error(`❌ ${file}: ${name}: ${(e && e.message || e).trim()}\n`);
    63        pass = false;
    64      });
    65    await Promise.all(Object.keys(tests).map(runTest));
    66    return { passed, failed };
    67  }
    68  
    69  // Modified from "terser/demo/test/compress.js"
    70  async function test_case(esbuild, test) {
    71    const sandbox = require(path.join(terserDir, 'test', 'sandbox'));
    72    const log = (format, args) => { throw new Error(tmpl(format, args)); };
    73  
    74    var semver = require(path.join(terserDir, 'node_modules', 'semver'));
    75    var output_options = test.beautify || {};
    76  
    77    // Generate the input code
    78    if (test.input instanceof U.AST_SimpleStatement
    79      && test.input.body instanceof U.AST_TemplateString) {
    80      try {
    81        var input = U.parse(test.input.body.segments[0].value);
    82      } catch (ex) {
    83        return false;
    84      }
    85      var input_code = make_code(input, output_options);
    86      var input_formatted = test.input.body.segments[0].value;
    87    } else {
    88      var input = as_toplevel(test.input, test.mangle);
    89      var input_code = make_code(input, output_options);
    90      var input_formatted = make_code(test.input, {
    91        ecma: 2015,
    92        beautify: true,
    93        quote_style: 3,
    94        keep_quoted_props: true
    95      });
    96    }
    97  
    98    // Make sure it's valid
    99    try {
   100      U.parse(input_code);
   101    } catch (ex) {
   102      log("!!! Cannot parse input\n---INPUT---\n{input}\n--PARSE ERROR--\n{error}\n\n", {
   103        input: input_formatted,
   104        error: ex,
   105      });
   106      return false;
   107    }
   108  
   109    // Pretty-print it
   110    var ast = input.to_mozilla_ast();
   111    var mozilla_options = {
   112      ecma: output_options.ecma,
   113      ascii_only: output_options.ascii_only,
   114      comments: false,
   115    };
   116    var ast_as_string = U.AST_Node.from_mozilla_ast(ast).print_to_string(mozilla_options);
   117  
   118    // Run esbuild as a minifier
   119    try {
   120      var { code: output } = await esbuild.transform(ast_as_string, {
   121        minify: true,
   122        keepNames: test.options.keep_fnames,
   123      });
   124    } catch (e) {
   125      const formatError = ({ text, location }) => {
   126        if (!location) return `\nerror: ${text}`;
   127        const { file, line, column } = location;
   128        return `\n${file}:${line}:${column}: ERROR: ${text}`;
   129      }
   130      log("!!! esbuild failed\n---INPUT---\n{input}\n---ERROR---\n{error}\n", {
   131        input: ast_as_string,
   132        error: (e && e.message || e) + '' + (e.errors ? e.errors.map(formatError) : ''),
   133      });
   134      return false;
   135    }
   136  
   137    // Make sure esbuild generates valid JavaScript
   138    try {
   139      U.parse(output);
   140    } catch (ex) {
   141      log("!!! Test matched expected result but cannot parse output\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--REPARSE ERROR--\n{error}\n\n", {
   142        input: input_formatted,
   143        output: output,
   144        error: ex.stack,
   145      });
   146      return false;
   147    }
   148  
   149    // Verify that the stdout matches our expectations
   150    if (test.expect_stdout
   151      && (!test.node_version || semver.satisfies(process.version, test.node_version))
   152      && !process.env.TEST_NO_SANDBOX
   153    ) {
   154      if (test.expect_stdout === true) {
   155        test.expect_stdout = sandbox.run_code(input_code, test.prepend_code);
   156      }
   157      var stdout = sandbox.run_code(output, test.prepend_code);
   158      if (!sandbox.same_stdout(test.expect_stdout, stdout)) {
   159        log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", {
   160          input: input_formatted,
   161          output: output,
   162          expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR",
   163          expected: test.expect_stdout,
   164          actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR",
   165          actual: stdout,
   166        });
   167        return false;
   168      }
   169    }
   170    return true;
   171  }
   172  
   173  ////////////////////////////////////////////////////////////////////////////////
   174  // The code below was copied verbatim from "terser/demo/test/compress.js"
   175  //
   176  // UglifyJS is released under the BSD license:
   177  //
   178  // Copyright 2012-2019 (c) Mihai Bazon <mihai.bazon@gmail.com>
   179  //
   180  // Redistribution and use in source and binary forms, with or without
   181  // modification, are permitted provided that the following conditions
   182  // are met:
   183  //
   184  //     * Redistributions of source code must retain the above
   185  //       copyright notice, this list of conditions and the following
   186  //       disclaimer.
   187  //
   188  //     * Redistributions in binary form must reproduce the above
   189  //       copyright notice, this list of conditions and the following
   190  //       disclaimer in the documentation and/or other materials
   191  //       provided with the distribution.
   192  //
   193  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
   194  // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   195  // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
   196  // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
   197  // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
   198  // OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
   199  // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
   200  // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
   201  // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
   202  // TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
   203  // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
   204  // SUCH DAMAGE.
   205  
   206  function tmpl() {
   207    return U.string_template.apply(this, arguments);
   208  }
   209  
   210  function as_toplevel(input, mangle_options) {
   211    if (!(input instanceof U.AST_BlockStatement))
   212      throw new Error("Unsupported input syntax");
   213    for (var i = 0; i < input.body.length; i++) {
   214      var stat = input.body[i];
   215      if (stat instanceof U.AST_SimpleStatement && stat.body instanceof U.AST_String)
   216        input.body[i] = new U.AST_Directive(stat.body);
   217      else break;
   218    }
   219    var toplevel = new U.AST_Toplevel(input);
   220    toplevel.figure_out_scope(mangle_options);
   221    return toplevel;
   222  }
   223  
   224  function parse_test(file) {
   225    var script = fs.readFileSync(file, "utf8");
   226    // TODO try/catch can be removed after fixing https://github.com/mishoo/UglifyJS2/issues/348
   227    try {
   228      var ast = U.parse(script, {
   229        filename: file
   230      });
   231    } catch (e) {
   232      console.log("Caught error while parsing tests in " + file + "\n");
   233      console.log(e);
   234      throw e;
   235    }
   236    var tests = {};
   237    var tw = new U.TreeWalker(function (node, descend) {
   238      if (node instanceof U.AST_LabeledStatement
   239        && tw.parent() instanceof U.AST_Toplevel) {
   240        var name = node.label.name;
   241        if (name in tests) {
   242          throw new Error('Duplicated test name "' + name + '" in ' + file);
   243        }
   244        tests[name] = get_one_test(name, node.body);
   245        return true;
   246      }
   247      if (!(node instanceof U.AST_Toplevel)) croak(node);
   248    });
   249    ast.walk(tw);
   250    return tests;
   251  
   252    function croak(node) {
   253      throw new Error(tmpl("Can't understand test file {file} [{line},{col}]\n{code}", {
   254        file: file,
   255        line: node.start.line,
   256        col: node.start.col,
   257        code: make_code(node, { beautify: false })
   258      }));
   259    }
   260  
   261    function read_boolean(stat) {
   262      if (stat.TYPE == "SimpleStatement") {
   263        var body = stat.body;
   264        if (body instanceof U.AST_Boolean) {
   265          return body.value;
   266        }
   267      }
   268      throw new Error("Should be boolean");
   269    }
   270  
   271    function read_string(stat) {
   272      if (stat.TYPE == "SimpleStatement") {
   273        var body = stat.body;
   274        switch (body.TYPE) {
   275          case "String":
   276            return body.value;
   277          case "Array":
   278            return body.elements.map(function (element) {
   279              if (element.TYPE !== "String")
   280                throw new Error("Should be array of strings");
   281              return element.value;
   282            }).join("\n");
   283        }
   284      }
   285      throw new Error("Should be string or array of strings");
   286    }
   287  
   288    function get_one_test(name, block) {
   289      var test = {
   290        name: name,
   291        options: {},
   292        reminify: true,
   293      };
   294      var tw = new U.TreeWalker(function (node, descend) {
   295        if (node instanceof U.AST_Assign) {
   296          if (!(node.left instanceof U.AST_SymbolRef)) {
   297            croak(node);
   298          }
   299          var name = node.left.name;
   300          test[name] = evaluate(node.right);
   301          return true;
   302        }
   303        if (node instanceof U.AST_LabeledStatement) {
   304          var label = node.label;
   305          assert.ok(
   306            [
   307              "input",
   308              "prepend_code",
   309              "expect",
   310              "expect_error",
   311              "expect_exact",
   312              "expect_warnings",
   313              "expect_stdout",
   314              "node_version",
   315              "reminify",
   316            ].includes(label.name),
   317            tmpl("Unsupported label {name} [{line},{col}]", {
   318              name: label.name,
   319              line: label.start.line,
   320              col: label.start.col
   321            })
   322          );
   323          var stat = node.body;
   324          if (label.name == "expect_exact" || label.name == "node_version") {
   325            test[label.name] = read_string(stat);
   326          } else if (label.name == "reminify") {
   327            var value = read_boolean(stat);
   328            test.reminify = value == null || value;
   329          } else if (label.name == "expect_stdout") {
   330            var body = stat.body;
   331            if (body instanceof U.AST_Boolean) {
   332              test[label.name] = body.value;
   333            } else if (body instanceof U.AST_Call) {
   334              var ctor = global[body.expression.name];
   335              assert.ok(ctor === Error || ctor.prototype instanceof Error, tmpl("Unsupported expect_stdout format [{line},{col}]", {
   336                line: label.start.line,
   337                col: label.start.col
   338              }));
   339              test[label.name] = ctor.apply(null, body.args.map(function (node) {
   340                assert.ok(node instanceof U.AST_Constant, tmpl("Unsupported expect_stdout format [{line},{col}]", {
   341                  line: label.start.line,
   342                  col: label.start.col
   343                }));
   344                return node.value;
   345              }));
   346            } else {
   347              test[label.name] = read_string(stat) + "\n";
   348            }
   349          } else if (label.name === "prepend_code") {
   350            test[label.name] = read_string(stat);
   351          } else {
   352            test[label.name] = stat;
   353          }
   354          return true;
   355        }
   356      });
   357      block.walk(tw);
   358      return test;
   359    }
   360  }
   361  
   362  function make_code(ast, options) {
   363    var stream = U.OutputStream(options);
   364    ast.print(stream);
   365    return stream.get();
   366  }
   367  
   368  function evaluate(code) {
   369    if (code instanceof U.AST_Node)
   370      code = make_code(code, { beautify: true });
   371    return new Function("return(" + code + ")")();
   372  }