#!/usr/bin/env node "use strict"; process.on("unhandledRejection", exn => { throw exn; }); // Mapping between signals and x86 exceptions: // "Program received signal SIGILL, Illegal instruction." -> #UD (6) // "Program received signal SIGFPE, Arithmetic exception." -> #DE (0) // to be determined -> #GP // to be determined -> #NM // to be determined -> #TS // to be determined -> #NP // to be determined -> #SS // to be determined -> #PF // A #UD might indicate a bug in the test generation const fs = require("fs"); const path = require("path"); const os = require("os"); const cluster = require("cluster"); const MAX_PARALLEL_TESTS = +process.env.MAX_PARALLEL_TESTS || 99; const TEST_DIR = __dirname + "/build/"; const DONE_MSG = "DONE"; const TERMINATE_MSG = "DONE"; const FORCE_JIT = process.argv.includes("--force-jit"); const MASK_ARITH = 1 | 1 << 2 | 1 << 4 | 1 << 6 | 1 << 7 | 1 << 11; try { var V86 = require("../../build/libv86-debug.js").V86; } catch(e) { console.error(e); console.error("Failed to import build/libv86-debug.js. Run " + "`make build/libv86-debug.js` first."); process.exit(1); } if(cluster.isMaster) { function extract_json(name, fixture_text) { if(fixture_text.includes("(signal SIGFPE)")) { return { exception: "DE", }; } if(fixture_text.includes("(signal SIGILL)")) { return { exception: "UD", }; } if(fixture_text.includes("Program received signal") || fixture_text.includes("SIGILL")) { throw new Error("Test was killed during execution by gdb: " + name + "\n" + fixture_text); } const json_regex = /---BEGIN JSON---([\s\[\]\w":\-,]*)---END JSON---/; const regex_match = json_regex.exec(fixture_text); if (!regex_match || regex_match.length < 2) { throw new Error("Could not find JSON in fixture text: " + fixture_text + "\nTest: " + name); } try { let array = JSON.parse(regex_match[1]); return { array: array }; } catch (e) { throw e; } } function send_work_to_worker(worker, message) { if(current_test < tests.length) { const test = tests[current_test]; worker.send(test); current_test++; } else { worker.send(TERMINATE_MSG); worker.disconnect(); setTimeout(() => { // The emulator currently doesn't cleanly exit, so this is necessary console.log("Worker killed"); worker.kill(); }, 100); finished_workers++; if(finished_workers === nr_of_cpus) { test_finished(); } } } const dir_files = fs.readdirSync(TEST_DIR); const files = dir_files.filter((name) => { return name.endsWith(".asm"); }).map(name => { return name.slice(0, -4); }); const tests = files.map(name => { let fixture_name = name + ".fixture"; let img_name = name + ".img"; let fixture_text = fs.readFileSync(TEST_DIR + fixture_name); let fixture = extract_json(name, fixture_text); return { img_name: img_name, fixture: fixture, }; }); const nr_of_cpus = Math.min( os.cpus().length || 1, tests.length, MAX_PARALLEL_TESTS ); console.log("Using %d cpus", nr_of_cpus); let current_test = 0; let failed_tests = []; let finished_workers = 0; for(let i = 0; i < nr_of_cpus; i++) { let worker = cluster.fork(); worker.on("message", function(message) { if (message !== DONE_MSG) { failed_tests.push(message); } send_work_to_worker(this); }); worker.on("online", send_work_to_worker.bind(null, worker)); worker.on("exit", function(code, signal) { if(code !== 0 && code !== null) { console.log("Worker error code:", code); process.exit(code); } }); worker.on("error", function(error) { console.error("Worker error: ", error.toString(), error); process.exit(1); }); } function test_finished() { console.log( "\n[+] Passed %d/%d tests.", tests.length - failed_tests.length, tests.length ); if (failed_tests.length > 0) { console.log("[-] Failed %d test(s).", failed_tests.length); failed_tests.forEach(function(test_failure) { console.error("\n[-] %s:", test_failure.img_name); test_failure.failures.forEach(function(failure) { function format_value(v) { if(typeof v === "number") return "0x" + (v >>> 0).toString(16); else return String(v); } console.error("\n\t" + failure.name); console.error("\tActual: " + format_value(failure.actual)); console.error("\tExpected: " + format_value(failure.expected)); }); }); process.exit(1); } } } else { function run_test(test) { if(!loaded) { first_test = test; return; } waiting_for_test = false; current_test = test; console.info("Testing", test.img_name); var cpu = emulator.v86.cpu; console.assert(!emulator.running); cpu.reset(); cpu.reset_memory(); cpu.load_multiboot(fs.readFileSync(TEST_DIR + current_test.img_name).buffer); if(FORCE_JIT) { cpu.jit_force_generate_unsafe(cpu.instruction_pointer[0]); cpu.test_hook_did_finalize_wasm = function() { cpu.test_hook_did_finalize_wasm = null; // don't synchronously call into the emulator from this callback setTimeout(() => { emulator.run(); }, 0); }; } else { emulator.run(); } } let loaded = false; let current_test = undefined; let first_test = undefined; let waiting_for_test = false; let emulator = new V86({ autostart: false, memory_size: 2 * 1024 * 1024, log_level: 0, }); emulator.add_listener("emulator-loaded", function() { loaded = true; if(first_test) { run_test(first_test); } }); emulator.cpu_exception_hook = function(n) { if(waiting_for_test) { return true; } waiting_for_test = true; emulator.stop(); const exceptions = { 0: "DE", 6: "UD", }; const exception = exceptions[n]; if(exception === undefined) { console.error("Unexpected CPU exception: " + n); process.exit(1); } if(current_test.fixture.exception !== exception) { process.send({ failures: [{ name: "Exception", actual: exception, expected: current_test.fixture.exception || "(none)", }], img_name: current_test.img_name, }); } else { process.send(DONE_MSG); } return true; }; emulator.bus.register("cpu-event-halt", function() { console.assert(!waiting_for_test); waiting_for_test = true; emulator.stop(); var cpu = emulator.v86.cpu; const filename = TEST_DIR + current_test.img_name; const evaluated_mmxs = cpu.reg_mmxs; const evaluated_xmms = cpu.reg_xmm32s; const esp = cpu.reg32s[4]; const evaluated_memory = new Int32Array(cpu.mem8.slice(0x120000 - 16 * 4, 0x120000).buffer); let individual_failures = []; if(current_test.exception) { throw "TODO: Handle exceptions"; } console.assert(current_test.fixture.array); if(current_test.fixture.array) { let offset = 0; const expected_reg32s = current_test.fixture.array.slice(offset, offset += 8); const expected_mmx_registers = current_test.fixture.array.slice(offset, offset += 16); const expected_xmm_registers = current_test.fixture.array.slice(offset, offset += 32); const expected_memory = current_test.fixture.array.slice(offset, offset += 16); const expected_eflags = current_test.fixture.array[offset] & MASK_ARITH; for (let i = 0; i < cpu.reg32s.length; i++) { let reg = cpu.reg32s[i]; if (reg !== expected_reg32s[i]) { individual_failures.push({ name: "cpu.reg32s[" + i + "]", expected: expected_reg32s[i], actual: reg, }); } } for (let i = 0; i < evaluated_mmxs.length; i++) { if (evaluated_mmxs[i] !== expected_mmx_registers[i]) { individual_failures.push({ name: "mm" + (i >> 1) + ".int32[" + (i & 1) + "] (cpu.reg_mmx[" + i + "])", expected: expected_mmx_registers[i], actual: evaluated_mmxs[i], }); } } for (let i = 0; i < evaluated_xmms.length; i++) { if (evaluated_xmms[i] !== expected_xmm_registers[i]) { individual_failures.push({ name: "xmm" + (i >> 2) + ".int32[" + (i & 3) + "] (cpu.reg_xmm[" + i + "])", expected: expected_xmm_registers[i], actual: evaluated_xmms[i], }); } } for (let i = 0; i < evaluated_memory.length; i++) { if (evaluated_memory[i] !== expected_memory[i]) { individual_failures.push({ name: "mem[" + i + "]", expected: expected_memory[i], actual: evaluated_memory[i], }); } } const seen_eflags = cpu.get_eflags() & MASK_ARITH; if(seen_eflags !== expected_eflags) { individual_failures.push({ name: "eflags", expected: expected_eflags, actual: seen_eflags, }); } } if (individual_failures.length > 0) { process.send({ failures: individual_failures, img_name: current_test.img_name }); } else { process.send(DONE_MSG); } }); cluster.worker.on("message", function(message) { if(message === TERMINATE_MSG) { emulator.stop(); emulator = null; } else { run_test(message); } }); }