const startTime = Date.now(); const fs = require("fs"); const path = require("path"); const os = require("os"); const v8 = require("v8"); const WASI = require("wasi").WASI; const glob = require("glob"); const colorsUtil = require("../cli/util/colors"); const optionsUtil = require("../cli/util/options"); const diff = require("./util/diff"); const asc = require("../cli/asc.js"); const { Rtrace } = require("../lib/rtrace/umd"); const cluster = require("cluster"); const coreCount = require("physical-cpu-count"); const config = { "create": { "description": [ "Recreates the fixture for the specified test(s)", "or all the fixtures if no specific test is given." ], "type": "b" }, "createBinary": { "description": [ "Also creates the respective .wasm binaries." ], "type": "b" }, "noDiff": { "description": [ "Disables output of detailed fixture differences." ], "type": "b" }, "rtraceVerbose": { "description": [ "Enables verbose rtrace output." ] }, "parallel": { "description": [ "Runs tests in parallel." ] }, "help": { "description": "Prints this message and exits.", "type": "b", "alias": "h" } }; const opts = optionsUtil.parse(process.argv.slice(2), config); const args = opts.options; const argv = opts.arguments; if (args.help) { console.log([ colorsUtil.white("SYNTAX"), " " + colorsUtil.cyan("npm run test:compiler --") + " [test1, test2 ...] [options]", "", colorsUtil.white("OPTIONS"), optionsUtil.help(config) ].join(os.EOL) + os.EOL); process.exit(0); } const features = process.env.ASC_FEATURES ? process.env.ASC_FEATURES.split(",") : []; const featuresConfig = require("./features.json"); var failedTests = new Set(); var failedMessages = new Map(); var skippedTests = new Set(); var skippedMessages = new Map(); const basedir = path.join(__dirname, "compiler"); // Gets a list of all relevant tests function getTests() { var tests = glob.sync("**/!(_*).ts", { cwd: basedir }).map(name => name.replace(/\.ts$/, "")); if (argv.length) { // run matching tests only tests = tests.filter(filename => argv.indexOf(filename.replace(/\.ts$/, "")) >= 0); if (!tests.length) { console.log(colorsUtil.red("FAILURE: ") + colorsUtil.white("No matching tests: " + argv.join(" ") + "\n")); process.exit(1); } } return tests; } // Starts a new section within a test function section(title) { var start = process.hrtime(); console.log("- " + title); return { title, end: function(code) { const times = process.hrtime(start); const time = asc.formatTime(times[0] * 1e9 + times[1]); switch (code) { case SUCCESS: console.log(" " + colorsUtil.green ("SUCCESS") + " (" + time + ")\n"); break; default: console.log(" " + colorsUtil.red("FAILURE") + " (" + time + ")\n"); break; case SKIPPED: console.log(" " + colorsUtil.yellow("SKIPPED") + " (" + time + ")\n"); break; } } }; } const SUCCESS = 0; const FAILURE = 1; const SKIPPED = 2; // Runs a single test function runTest(basename) { console.log(colorsUtil.white("# compiler/" + basename) + "\n"); const configPath = path.join(basedir, basename + ".json"); const config = fs.existsSync(configPath) ? require(configPath) : {}; const stdout = asc.createMemoryStream(); const stderr = asc.createMemoryStream(chunk => process.stderr.write(chunk.toString().replace(/^(?!$)/mg, " "))); stderr.isTTY = true; var asc_flags = []; var asc_rtrace = !!config.asc_rtrace; var v8_flags = ""; var v8_no_flags = ""; var missing_features = []; if (config.features) { config.features.forEach(feature => { if (!features.includes(feature) && !features.includes("*")) { missing_features.push(feature); return; } var featureConfig = featuresConfig[feature]; if (featureConfig.asc_flags) { featureConfig.asc_flags.forEach(flag => { Array.prototype.push.apply(asc_flags, flag.split(" ")); }); } if (featureConfig.v8_flags) { featureConfig.v8_flags.forEach(flag => { if (v8_flags) v8_flags += " "; v8_flags += flag; if (v8_no_flags) v8_no_flags += " "; v8_no_flags += "--no-" + flag.substring(2); }); v8.setFlagsFromString(v8_flags); } }); if (missing_features.length) { console.log("- " + colorsUtil.yellow("feature SKIPPED") + " (" + missing_features.join(", ") + ")\n"); skippedTests.add(basename); skippedMessages.set(basename, "feature not enabled"); if (cluster.isWorker) process.send({ cmd: "skipped", message: skippedMessages.get(basename) }); return; } } if (config.asc_flags) { config.asc_flags.forEach(flag => { Array.prototype.push.apply(asc_flags, flag.split(" ")); }); } var failed = false; // Build untouched var cmd = [ basename + ".ts", "--baseDir", basedir, "--debug", "--textFile" // -> stdout ]; if (asc_flags) Array.prototype.push.apply(cmd, asc_flags); cmd.push("--binaryFile", basename + ".untouched.wasm"); const compileUntouched = section("compile untouched"); asc.main(cmd, { stdout: stdout, stderr: stderr }, err => { let expectStderr = config.stderr; if (err) { stderr.write("---\n"); stderr.write(err.stack); stderr.write("\n---\n"); if (expectStderr) { compileUntouched.end(SKIPPED); } else { compileUntouched.end(FAILURE); } } else { compileUntouched.end(SUCCESS); } // check expected stderr patterns in order if (expectStderr) { const compareStderr = section("compare stderr"); const stderrString = stderr.toString(); if (typeof expectStderr === "string") expectStderr = [ expectStderr ]; let lastIndex = 0; expectStderr.forEach((substr, i) => { var index = stderrString.indexOf(substr, lastIndex); if (index < 0) { console.log(" missing pattern #" + (i + 1) + " '" + substr + "' in stderr at " + lastIndex + "+."); failed = true; } else { lastIndex = index + substr.length; } }); if (failed) { failedTests.add(basename); failedMessages.set(basename, "stderr mismatch"); compareStderr.end(FAILURE); } else { compareStderr.end(SUCCESS); } return 1; } const compareFixture = section("compare fixture"); var actual = stdout.toString().replace(/\r\n/g, "\n"); if (args.create) { fs.writeFileSync(path.join(basedir, basename + ".untouched.wat"), actual, { encoding: "utf8" }); console.log(" " + colorsUtil.yellow("Created fixture")); compareFixture.end(SKIPPED); } else { let expected = fs.readFileSync(path.join(basedir, basename + ".untouched.wat"), { encoding: "utf8" }).replace(/\r\n/g, "\n"); if (args.noDiff) { if (expected != actual) { failed = true; failedTests.add(basename); compareFixture.end(FAILURE); } else { compareFixture.end(SUCCESS); } } else { let diffs = diff(basename + ".untouched.wat", expected, actual); if (diffs !== null) { console.log(diffs); failed = true; failedTests.add(basename); compareFixture.end(FAILURE); } else { compareFixture.end(SUCCESS); } } } stdout.length = 0; stderr.length = 0; // Build optimized var cmd = [ basename + ".ts", "--baseDir", basedir, "--binaryFile", // -> stdout "-O" ]; if (asc_flags) Array.prototype.push.apply(cmd, asc_flags); if (args.create) cmd.push("--textFile", basename + ".optimized.wat"); const compileOptimized = section("compile optimized"); asc.main(cmd, { stdout: stdout, stderr: stderr }, err => { if (err) { stderr.write("---\n"); stderr.write(err.stack); stderr.write("\n---\n"); failed = true; failedMessages.set(basename, err.message); failedTests.add(basename); compileOptimized.end(FAILURE); return 1; } else { compileOptimized.end(SUCCESS); } let untouchedBuffer = fs.readFileSync(path.join(basedir, basename + ".untouched.wasm")); let optimizedBuffer = stdout.toBuffer(); const gluePath = path.join(basedir, basename + ".js"); var glue = {}; if (fs.existsSync(gluePath)) glue = require(gluePath); const instantiateUntouched = section("instantiate untouched"); if (!config.skipInstantiate) { if (!testInstantiate(basename, untouchedBuffer, glue, stderr, config.asc_wasi)) { failed = true; failedTests.add(basename); instantiateUntouched.end(FAILURE); } else { instantiateUntouched.end(SUCCESS); const instantiateOptimized = section("instantiate optimized"); if (!testInstantiate(basename, optimizedBuffer, glue, stderr, config.asc_wasi)) { failed = true; failedTests.add(basename); instantiateOptimized.end(FAILURE); } else { instantiateOptimized.end(SUCCESS); } } } else { instantiateUntouched.end(SKIPPED); } if (!asc_rtrace) return; stdout.length = 0; stderr.length = 0; // Build rtraced var cmd = [ basename + ".ts", "--baseDir", basedir, "--binaryFile", // -> stdout "--debug", "--use", "ASC_RTRACE=1", "--explicitStart", // "--runPasses", "instrument-memory" ]; if (asc_flags) { Array.prototype.push.apply(cmd, asc_flags); } const compileRtraced = section("compile rtraced"); asc.main(cmd, { stdout: stdout, stderr: stderr }, err => { if (err) { stderr.write("---\n"); stderr.write(err.stack); stderr.write("\n---\n"); failed = true; failedMessages.set(basename, err.message); failedTests.add(basename); compileRtraced.end(FAILURE); return 1; } else { compileRtraced.end(SUCCESS); } let rtracedBuffer = stdout.toBuffer(); const instantiateRtrace = section("instantiate rtrace"); v8.setFlagsFromString("--experimental-wasm-bigint"); if (!testInstantiate(basename, rtracedBuffer, glue, stderr, config.asc_wasi)) { failed = true; failedTests.add(basename); instantiateRtrace.end(FAILURE); } else { instantiateRtrace.end(SUCCESS); } v8.setFlagsFromString("--no-experimental-wasm-bigint"); }); }); if (failed) return 1; }); if (v8_no_flags) v8.setFlagsFromString(v8_no_flags); if (!args.createBinary) fs.unlink(path.join(basedir, basename + ".untouched.wasm"), err => { /* nop */ }); if (cluster.isWorker) process.send({ cmd: "done", failed: failed, message: failedMessages.get(basename) }); } // Tests if instantiation of a module succeeds function testInstantiate(basename, binaryBuffer, glue, stderr, wasiOptions) { var failed = false; try { let memory = new WebAssembly.Memory({ initial: 10 }); let exports = {}; function getString(ptr) { const RUNTIME_HEADER_SIZE = 16; if (!ptr) return "null"; var U32 = new Uint32Array(exports.memory ? exports.memory.buffer : memory.buffer); var U16 = new Uint16Array(exports.memory ? exports.memory.buffer : memory.buffer); var len16 = U32[(ptr - RUNTIME_HEADER_SIZE + 12) >>> 2] >>> 1; var ptr16 = ptr >>> 1; return String.fromCharCode.apply(String, U16.subarray(ptr16, ptr16 + len16)); } let rtrace = new Rtrace({ onerror(err, info) { console.log(" ERROR: " + err.stack); failed = true; failedMessages.set(basename, err.message); }, oninfo(msg, info) { if (!args.rtraceVerbose) return; console.log(" " + msg); }, getMemory() { return instance.exports.memory; } }); var imports = rtrace.install({ env: { memory, abort: function(msg, file, line, column) { console.log(colorsUtil.red(" abort: " + getString(msg) + " in " + getString(file) + "(" + line + ":" + column + ")")); }, trace: function(msg, n) { console.log(" trace: " + getString(msg) + (n ? " " : "") + Array.prototype.slice.call(arguments, 2, 2 + n).join(", ")); }, seed: function() { return 0xA5534817; // make tests deterministic }, visit: function() { // override in tests } }, Math, Date, Reflect }); if (glue.preInstantiate) { console.log(" [call preInstantiate]"); glue.preInstantiate(imports, exports); } var wasi = null; if (wasiOptions) { wasi = new WASI(wasiOptions); imports.wasi_snapshot_preview1 = wasi.wasiImport; } var instance = new WebAssembly.Instance(new WebAssembly.Module(binaryBuffer), imports); Object.setPrototypeOf(exports, instance.exports); if (glue.postInstantiate) { console.log(" [call postInstantiate]"); glue.postInstantiate(instance); } if (wasi) { console.log(" [wasi start]"); let code = wasi.start(instance); console.log(" [wasi exit] code=" + code); } else if (exports._start) { console.log(" [call start]"); exports._start(); } if (glue.postStart) { console.log(" [call postStart]"); glue.postStart(instance); } let leakCount = rtrace.check(); if (leakCount) { let msg = "memory leak detected: " + leakCount + " leaking"; failed = true; failedMessages.set(basename, msg); console.log(" " + msg); } if (!failed) { if (rtrace.active) { console.log(" " + rtrace.allocCount + " allocs, " + rtrace.freeCount + " frees, " + rtrace.resizeCount + " resizes, " + rtrace.moveCount + " moves" ); } return true; } } catch (e) { stderr.write("---\n"); stderr.write(e.stack); stderr.write("\n---\n"); failed = true; failedMessages.set(basename, e.message); } return false; } // Evaluates the overall test result function evaluateResult() { if (skippedTests.size) { console.log(colorsUtil.yellow("WARNING: ") + colorsUtil.white(skippedTests.size + " compiler tests have been skipped:\n")); skippedTests.forEach(name => { var message = skippedMessages.has(name) ? colorsUtil.gray("[" + skippedMessages.get(name) + "]") : ""; console.log(" " + name + " " + message); }); console.log(); } if (failedTests.size) { process.exitCode = 1; console.log(colorsUtil.red("FAILURE: ") + colorsUtil.white(failedTests.size + " compiler tests had failures:\n")); failedTests.forEach(name => { var message = failedMessages.has(name) ? colorsUtil.gray("[" + failedMessages.get(name) + "]") : ""; console.log(" " + name + " " + message); }); console.log(); } console.log("Time: " + (Date.now() - startTime) + " ms\n"); if (!process.exitCode) { console.log("[ " + colorsUtil.white("SUCCESS") + " ]"); } } asc.ready.then(() => { // Run tests in parallel if requested if (args.parallel && coreCount > 1) { if (cluster.isWorker) { colorsUtil.supported = true; process.on("message", msg => { if (msg.cmd != "run") throw Error("invalid command: " + msg.cmd); try { runTest(msg.test); } catch (e) { process.send({ cmd: "done", failed: true, message: e.message }); } }); process.send({ cmd: "ready" }); } else { const tests = getTests(); // const sizes = new Map(); // tests.forEach(name => sizes.set(name, fs.statSync(path.join(basedir, name + ".ts")).size)); // tests.sort((a, b) => sizes.get(b) - sizes.get(a)); const workers = []; const current = []; const outputs = []; let numWorkers = Math.min(coreCount - 1, tests.length); console.log("Spawning " + numWorkers + " workers ..."); cluster.settings.silent = true; let index = 0; for (let i = 0; i < numWorkers; ++i) { let worker = cluster.fork(); workers[i] = worker; current[i] = null; outputs[i] = []; worker.process.stdout.on("data", buf => outputs[i].push(buf)); worker.process.stderr.on("data", buf => outputs[i].push(buf)); worker.on("message", msg => { if (msg.cmd == "done") { process.stdout.write(Buffer.concat(outputs[i]).toString()); if (msg.failed) failedTests.add(current[i]); if (msg.message) failedMessages.set(current[i], msg.message); } else if (msg.cmd == "skipped") { process.stdout.write(Buffer.concat(outputs[i]).toString()); skippedTests.add(current[i]); if (msg.message) skippedMessages.set(current[i], msg.message); } else if (msg.cmd != "ready") { throw Error("invalid command: " + msg.cmd); } if (index >= tests.length) { workers[i] = null; worker.kill(); return; } current[i] = tests[index++]; outputs[i] = []; worker.send({ cmd: "run", test: current[i] }); }); worker.on("disconnect", () => { if (workers[i]) throw Error("worker#" + i + " died unexpectedly"); if (!--numWorkers) evaluateResult(); }); } } // Otherwise run tests sequentially } else { getTests().forEach(runTest); evaluateResult(); } });