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 }