<NME> run.js
<BEF> #!/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 assert = require("assert").strict;
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_NAME = process.env.TEST_NAME;
const SINGLE_TEST_TIMEOUT = 10000;
const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD;

const TEST_DIR = __dirname + "/build/";
const DONE_MSG = "DONE";
const TERMINATE_MSG = "DONE";

const FORCE_JIT = process.argv.includes("--force-jit");

// see --section-start= in makefile
const V86_TEXT_OFFSET = 0x8000;
const NASM_TEXT_OFFSET = 0x800000;

// alternative representation for infinity for json
const JSON_POS_INFINITY = "+INFINITY";
const JSON_NEG_INFINITY = "-INFINITY";
const JSON_POS_NAN = "+NAN";
const JSON_NEG_NAN = "-NAN";

const MASK_ARITH = 1 | 1 << 2 | 1 << 4 | 1 << 6 | 1 << 7 | 1 << 11;
const FPU_TAG_ALL_INVALID = 0xAAAA;
const FPU_STATUS_MASK = 0xFFFF & ~(1 << 9 | 1 << 5 | 1 << 3); // bits that are not correctly implemented by v86
const FP_COMPARISON_SIGNIFICANT_DIGITS = 7;

try {
    var V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "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);
}

function float_equal(x, y)
{
    assert(typeof x === "number");
    assert(typeof y === "number");

    if(x === Infinity && y === Infinity || x === -Infinity && y === -Infinity || isNaN(x) && isNaN(y))
    {
        return true;
    }

    const epsilon = Math.pow(10, -FP_COMPARISON_SIGNIFICANT_DIGITS);
    return Math.abs(x - y) < epsilon;
}

function format_value(v)
{
    if(typeof v === "number")
    {

        if(fixture_text.includes("Program received signal") || fixture_text.includes("SIGILL"))
        {
            throw new Error("Test was killed during execution by gdb: " + name);
        }

        const json_regex = /---BEGIN JSON---([\s\[\]\w":\-,]*)---END JSON---/;
        }
    }
    else
    {
        return String(v);
    }
}

if(cluster.isMaster)
{
    function extract_json(name, fixture_text)
    {
        let exception;

        if(fixture_text.includes("(signal SIGFPE)"))
        {
            exception = "DE";
        }

        if(fixture_text.includes("(signal SIGILL)"))
        {
            exception = "UD";
        }

        if(fixture_text.includes("(signal SIGSEGV)"))
        {
            exception = "GP";
        }

        if(fixture_text.includes("(signal SIGBUS)"))
        {
            exception = "PF";
        }

        if(!exception && fixture_text.includes("Program received signal"))
        {
            throw new Error("Test was killed during execution by gdb: " + name + "\n" + fixture_text);
        }

        fixture_text = fixture_text.toString()
            .replace(/-inf\b/g, JSON.stringify(JSON_NEG_INFINITY))
            .replace(/\binf\b/g, JSON.stringify(JSON_POS_INFINITY))
            .replace(/-nan\b/g, JSON.stringify(JSON_NEG_NAN))
            .replace(/\bnan\b/g, JSON.stringify(JSON_POS_NAN));

        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);
        }

        let array = JSON.parse(regex_match[1]);
        return {
            array: array,
            exception,
        };
    }


    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);
    }).filter(name => {
        return !TEST_NAME || name === TEST_NAME;
    });

    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) {
                    console.error("\n\t" + failure.name);
                    console.error("\tActual:   " + failure.actual);
                    console.error("\tExpected: " + failure.expected);
                });
            });
            process.exit(1);
        }
    }
}
else {
    function run_test(test)
    {
        if(!loaded)
        {
            first_test = test;
            return;
        }

        waiting_to_receive_next_test = false;
        current_test = test;
        console.info("Testing", test.img_name);

        var cpu = emulator.v86.cpu;

        assert(!emulator.running);

        cpu.reboot_internal();
        cpu.reset_memory();
        cpu.load_multiboot(fs.readFileSync(TEST_DIR + current_test.img_name).buffer);

        test_timeout = setTimeout(() => {
            console.error("Test " + test.img_name + " timed out after " + (SINGLE_TEST_TIMEOUT / 1000) + " seconds.");
            process.exit(2);
        }, SINGLE_TEST_TIMEOUT);

        if(FORCE_JIT)
        {
            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);
            };

            cpu.jit_force_generate(cpu.instruction_pointer[0]);
        }
        else
        {
            emulator.run();
        }
    }

    let loaded = false;
    let current_test = undefined;
    let first_test = undefined;
    let waiting_to_receive_next_test = false;
    let recorded_exceptions = [];
    let test_timeout;

    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)
    {
        emulator.v86.cpu.instruction_counter[0] += 100000; // always make progress

        if(waiting_to_receive_next_test)
        {
            return true;
        }

        const exceptions = {
            0: "DE",
            6: "UD",
            13: "GP",
        };

        const exception = exceptions[n];

        if(exception === undefined)
        {
            console.error("Unexpected CPU exception: " + n);
            process.exit(1);
        }

        const eip = emulator.v86.cpu.instruction_pointer[0];
        emulator.v86.cpu.write32(emulator.v86.cpu.translate_address_system_read(eip), 0xF4F4F4F4); // hlt

        // XXX: On gdb execution is stopped at this point. On v86 we
        // currently don't have this ability, so we record the exception
        // and continue execution
        recorded_exceptions.push({ exception, eip });
        finish_test();
        return true;
    };

    emulator.bus.register("cpu-event-halt", function() {
        finish_test();
    });

    function finish_test()
    {
        if(waiting_to_receive_next_test)
        {
            return;
        }

        waiting_to_receive_next_test = true;
        clearTimeout(test_timeout);

        emulator.stop();
        var cpu = emulator.v86.cpu;

        const evaluated_fpu_regs = new Float64Array(8).map((_, i) => cpu.fpu_get_sti_f64(i));
        const evaluated_mmxs = new Int32Array(16).map((_, i) => cpu.fpu_st[(i & ~1) << 1 | (i & 1)]);
        const evaluated_xmms = cpu.reg_xmm32s;
        const evaluated_memory = new Int32Array(cpu.mem8.slice(0x120000 - 16 * 4, 0x120000).buffer);
        const evaluated_fpu_tag = cpu.fpu_load_tag_word();
        const evaluated_fpu_status = cpu.fpu_load_status_word() & FPU_STATUS_MASK;

        let individual_failures = [];

        assert(current_test.fixture.array);

        const FLOAT_TRANSLATION = {
            [JSON_POS_INFINITY]: Infinity,
            [JSON_NEG_INFINITY]: -Infinity,
            [JSON_POS_NAN]: NaN,
            [JSON_NEG_NAN]: NaN, // XXX: Ignore sign of NaN
        };

        let offset = 0;
        const expected_reg32 = current_test.fixture.array.slice(offset, offset += 8);
        const expected_eip = current_test.fixture.array[offset++];
        const expected_fpu_regs =
            current_test.fixture.array.slice(offset, offset += 8) .map(x => x in FLOAT_TRANSLATION ? FLOAT_TRANSLATION[x] : x);
        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;
        const fpu_tag = current_test.fixture.array[offset++];
        const fpu_status = current_test.fixture.array[offset++] & FPU_STATUS_MASK;

        if(!current_test.fixture.exception)
        {
            for (let i = 0; i < cpu.reg32.length; i++) {
                let reg = cpu.reg32[i];
                if (reg !== expected_reg32[i]) {
                    individual_failures.push({
                        name: "cpu.reg32[" + i + "]",
                        expected: expected_reg32[i],
                        actual: reg,
                    });
                }
            }

            if(fpu_tag !== FPU_TAG_ALL_INVALID)
            {
                for (let i = 0; i < evaluated_fpu_regs.length; i++) {
                    if (expected_fpu_regs[i] !== "invalid" &&
                            !float_equal(evaluated_fpu_regs[i], expected_fpu_regs[i])) {
                        individual_failures.push({
                            name: "st" + i,
                            expected: expected_fpu_regs[i],
                            actual: evaluated_fpu_regs[i],
                        });
                    }
                }

                if(fpu_status !== evaluated_fpu_status)
                {
                    individual_failures.push({
                        name: "fpu status word",
                        expected: fpu_status,
                        actual: evaluated_fpu_status,
                    });
                }
            }
            else
            {
                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) + "]",
                            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(current_test.fixture.exception)
        {
            const seen_eip = (recorded_exceptions[0] || {}).eip;
            if(seen_eip - V86_TEXT_OFFSET !== expected_eip - NASM_TEXT_OFFSET)
            {
                individual_failures.push({
                    name: "exception eip",
                    expected: expected_eip - NASM_TEXT_OFFSET,
                    actual: seen_eip === undefined ? "(none)" : seen_eip - V86_TEXT_OFFSET,
                });
            }
        }

        const seen_exception = (recorded_exceptions[0] || {}).exception;
        if(current_test.fixture.exception !== seen_exception)
        {
            individual_failures.push({
                name: "Exception",
                actual: seen_exception || "(none)",
                expected: current_test.fixture.exception,
            });
        }

        individual_failures = individual_failures.map(({ name, actual, expected }) => {
            return {
                name,
                actual: format_value(actual),
                expected: format_value(expected),
            };
        });

        recorded_exceptions = [];

        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);
        }
    });
}

<MSG> Log fixture test when test was killed during execution

<DFF> @@ -78,7 +78,7 @@ if(cluster.isMaster)
 
         if(fixture_text.includes("Program received signal") || fixture_text.includes("SIGILL"))
         {
-            throw new Error("Test was killed during execution by gdb: " + name);
+            throw new Error("Test was killed during execution by gdb: " + name + "\n" + fixture_text);
         }
 
         const json_regex = /---BEGIN JSON---([\s\[\]\w":\-,]*)---END JSON---/;

{'commit': 'diff --git a/tests/nasm/run.js b/tests/nasm/run.js\nindex de8abd55..ce5cdf76 100755\n--- a/tests/nasm/run.js\n+++ b/tests/nasm/run.js\n@@ -78,7 +78,7 @@ if(cluster.isMaster)\n \n         if(fixture_text.includes("Program received signal") || fixture_text.includes("SIGILL"))\n         {\n-            throw new Error("Test was killed during execution by gdb: " + name);\n+            throw new Error("Test was killed during execution by gdb: " + name + "\\n" + fixture_text);\n         }\n \n         const json_regex = /---BEGIN JSON---([\\s\\[\\]\\w":\\-,]*)---END JSON---/;\n', 'commit_subject': 'Log fixture test when test was killed during execution', 'repo_name': 'copy/v86', 'committer': {'name': 'Fabian', 'email': 'f84e2e2dadd87384fb55f25886926b777e8378f1@copy.sh', 'time_sec': '1595380213', 'tz_offset': '-300', 'date': {'seconds': '1595380213'}}, 'commit_tree': '8971b2348649bede1f1b630c8c2b7727564b8e56', 'commit_message': 'Log fixture test when test was killed during execution\n', 'language_name': 'C++', 'license': 'bsd-2-clause', 'hunks': '@@ -78,7 +78,7 @@ if(cluster.isMaster)\n \n         if(fixture_text.includes("Program received signal") || fixture_text.includes("SIGILL"))\n         {\n-            throw new Error("Test was killed during execution by gdb: " + name);\n+            throw new Error("Test was killed during execution by gdb: " + name + "\\n" + fixture_text);\n         }\n \n         const json_regex = /---BEGIN JSON---([\\s\\[\\]\\w":\\-,]*)---END JSON---/;\n', 'addition_count': 1, 'deletion_count': 1, 'src_file': 'a/tests/nasm/run.js', 'tgt_file': 'b/tests/nasm/run.js', 'file_extension': '.js', 'before_file': '#!/usr/bin/env node\n"use strict";\n\nprocess.on("unhandledRejection", exn => { throw exn; });\n\n// Mapping between signals and x86 exceptions:\n// "Program received signal SIGILL, Illegal instruction." -> #UD (6)\n// "Program received signal SIGFPE, Arithmetic exception." -> #DE (0)\n// to be determined -> #GP\n// to be determined -> #NM\n// to be determined -> #TS\n// to be determined -> #NP\n// to be determined -> #SS\n// to be determined -> #PF\n\n// A #UD might indicate a bug in the test generation\n\nconst assert = require("assert").strict;\nconst fs = require("fs");\nconst path = require("path");\nconst os = require("os");\nconst cluster = require("cluster");\n\nconst MAX_PARALLEL_TESTS = +process.env.MAX_PARALLEL_TESTS || 99;\nconst TEST_NAME = process.env.TEST_NAME;\nconst SINGLE_TEST_TIMEOUT = 10000;\nconst TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD;\n\nconst TEST_DIR = __dirname + "/build/";\nconst DONE_MSG = "DONE";\nconst TERMINATE_MSG = "DONE";\n\nconst FORCE_JIT = process.argv.includes("--force-jit");\n\n// see --section-start= in makefile\nconst V86_TEXT_OFFSET = 0x8000;\nconst NASM_TEXT_OFFSET = 0x800000;\n\n// alternative representation for infinity for json\nconst JSON_POS_INFINITY = "+INFINITY";\nconst JSON_NEG_INFINITY = "-INFINITY";\nconst JSON_POS_NAN = "+NAN";\nconst JSON_NEG_NAN = "-NAN";\n\nconst MASK_ARITH = 1 | 1 << 2 | 1 << 4 | 1 << 6 | 1 << 7 | 1 << 11;\nconst FPU_TAG_ALL_INVALID = 0xAAAA;\nconst FPU_STATUS_MASK = 0xFFFF & ~(1 << 9 | 1 << 5 | 1 << 3); // bits that are not correctly implemented by v86\nconst FP_COMPARISON_SIGNIFICANT_DIGITS = 7;\n\ntry {\n    var V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86;\n}\ncatch(e) {\n    console.error(e);\n    console.error("Failed to import build/libv86-debug.js. Run " +\n                  "`make build/libv86-debug.js` first.");\n    process.exit(1);\n}\n\nfunction float_equal(x, y)\n{\n    assert(typeof x === "number");\n    assert(typeof y === "number");\n\n    if(x === Infinity && y === Infinity || x === -Infinity && y === -Infinity || isNaN(x) && isNaN(y))\n    {\n        return true;\n    }\n\n    const epsilon = Math.pow(10, -FP_COMPARISON_SIGNIFICANT_DIGITS);\n    return Math.abs(x - y) < epsilon;\n}\n\nfunction format_value(v)\n{\n    if(typeof v === "number")\n    {\n\n        if(fixture_text.includes("Program received signal") || fixture_text.includes("SIGILL"))\n        {\n            throw new Error("Test was killed during execution by gdb: " + name);\n        }\n\n        const json_regex = /---BEGIN JSON---([\\s\\[\\]\\w":\\-,]*)---END JSON---/;\n        }\n    }\n    else\n    {\n        return String(v);\n    }\n}\n\nif(cluster.isMaster)\n{\n    function extract_json(name, fixture_text)\n    {\n        let exception;\n\n        if(fixture_text.includes("(signal SIGFPE)"))\n        {\n            exception = "DE";\n        }\n\n        if(fixture_text.includes("(signal SIGILL)"))\n        {\n            exception = "UD";\n        }\n\n        if(fixture_text.includes("(signal SIGSEGV)"))\n        {\n            exception = "GP";\n        }\n\n        if(fixture_text.includes("(signal SIGBUS)"))\n        {\n            exception = "PF";\n        }\n\n        if(!exception && fixture_text.includes("Program received signal"))\n        {\n            throw new Error("Test was killed during execution by gdb: " + name + "\\n" + fixture_text);\n        }\n\n        fixture_text = fixture_text.toString()\n            .replace(/-inf\\b/g, JSON.stringify(JSON_NEG_INFINITY))\n            .replace(/\\binf\\b/g, JSON.stringify(JSON_POS_INFINITY))\n            .replace(/-nan\\b/g, JSON.stringify(JSON_NEG_NAN))\n            .replace(/\\bnan\\b/g, JSON.stringify(JSON_POS_NAN));\n\n        const json_regex = /---BEGIN JSON---([\\s\\[\\]\\.\\+\\w":\\-,]*)---END JSON---/;\n        const regex_match = json_regex.exec(fixture_text);\n        if (!regex_match || regex_match.length < 2) {\n            throw new Error("Could not find JSON in fixture text: " + fixture_text + "\\nTest: " + name);\n        }\n\n        let array = JSON.parse(regex_match[1]);\n        return {\n            array: array,\n            exception,\n        };\n    }\n\n\n    function send_work_to_worker(worker, message) {\n        if(current_test < tests.length) {\n            const test = tests[current_test];\n            worker.send(test);\n            current_test++;\n        }\n        else {\n            worker.send(TERMINATE_MSG);\n            worker.disconnect();\n\n            setTimeout(() => {\n                // The emulator currently doesn\'t cleanly exit, so this is necessary\n                console.log("Worker killed");\n                worker.kill();\n            }, 100);\n\n            finished_workers++;\n            if(finished_workers === nr_of_cpus)\n            {\n                test_finished();\n            }\n        }\n    }\n\n    const dir_files = fs.readdirSync(TEST_DIR);\n    const files = dir_files.filter((name) => {\n        return name.endsWith(".asm");\n    }).map(name => {\n        return name.slice(0, -4);\n    }).filter(name => {\n        return !TEST_NAME || name === TEST_NAME;\n    });\n\n    const tests = files.map(name => {\n        let fixture_name = name + ".fixture";\n        let img_name = name + ".img";\n        let fixture_text = fs.readFileSync(TEST_DIR + fixture_name);\n        let fixture = extract_json(name, fixture_text);\n\n        return {\n            img_name: img_name,\n            fixture: fixture,\n        };\n    });\n\n    const nr_of_cpus = Math.min(\n        os.cpus().length || 1,\n        tests.length,\n        MAX_PARALLEL_TESTS\n    );\n    console.log("Using %d cpus", nr_of_cpus);\n\n    let current_test = 0;\n\n    let failed_tests = [];\n    let finished_workers = 0;\n\n    for(let i = 0; i < nr_of_cpus; i++)\n    {\n        let worker = cluster.fork();\n\n        worker.on("message", function(message) {\n            if (message !== DONE_MSG) {\n                failed_tests.push(message);\n            }\n            send_work_to_worker(this);\n        });\n\n        worker.on("online", send_work_to_worker.bind(null, worker));\n\n        worker.on("exit", function(code, signal) {\n            if(code !== 0 &&  code !== null) {\n                console.log("Worker error code:", code);\n                process.exit(code);\n            }\n        });\n\n        worker.on("error", function(error) {\n            console.error("Worker error: ", error.toString(), error);\n            process.exit(1);\n        });\n    }\n\n    function test_finished()\n    {\n        console.log(\n            "\\n[+] Passed %d/%d tests.",\n            tests.length - failed_tests.length,\n            tests.length\n        );\n        if (failed_tests.length > 0) {\n            console.log("[-] Failed %d test(s).", failed_tests.length);\n            failed_tests.forEach(function(test_failure) {\n\n                console.error("\\n[-] %s:", test_failure.img_name);\n\n                test_failure.failures.forEach(function(failure) {\n                    console.error("\\n\\t" + failure.name);\n                    console.error("\\tActual:   " + failure.actual);\n                    console.error("\\tExpected: " + failure.expected);\n                });\n            });\n            process.exit(1);\n        }\n    }\n}\nelse {\n    function run_test(test)\n    {\n        if(!loaded)\n        {\n            first_test = test;\n            return;\n        }\n\n        waiting_to_receive_next_test = false;\n        current_test = test;\n        console.info("Testing", test.img_name);\n\n        var cpu = emulator.v86.cpu;\n\n        assert(!emulator.running);\n\n        cpu.reboot_internal();\n        cpu.reset_memory();\n        cpu.load_multiboot(fs.readFileSync(TEST_DIR + current_test.img_name).buffer);\n\n        test_timeout = setTimeout(() => {\n            console.error("Test " + test.img_name + " timed out after " + (SINGLE_TEST_TIMEOUT / 1000) + " seconds.");\n            process.exit(2);\n        }, SINGLE_TEST_TIMEOUT);\n\n        if(FORCE_JIT)\n        {\n            cpu.test_hook_did_finalize_wasm = function()\n            {\n                cpu.test_hook_did_finalize_wasm = null;\n\n                // don\'t synchronously call into the emulator from this callback\n                setTimeout(() => {\n                    emulator.run();\n                }, 0);\n            };\n\n            cpu.jit_force_generate(cpu.instruction_pointer[0]);\n        }\n        else\n        {\n            emulator.run();\n        }\n    }\n\n    let loaded = false;\n    let current_test = undefined;\n    let first_test = undefined;\n    let waiting_to_receive_next_test = false;\n    let recorded_exceptions = [];\n    let test_timeout;\n\n    let emulator = new V86({\n        autostart: false,\n        memory_size: 2 * 1024 * 1024,\n        log_level: 0,\n    });\n\n    emulator.add_listener("emulator-loaded", function()\n        {\n            loaded = true;\n\n            if(first_test)\n            {\n                run_test(first_test);\n            }\n        });\n\n    emulator.cpu_exception_hook = function(n)\n    {\n        emulator.v86.cpu.instruction_counter[0] += 100000; // always make progress\n\n        if(waiting_to_receive_next_test)\n        {\n            return true;\n        }\n\n        const exceptions = {\n            0: "DE",\n            6: "UD",\n            13: "GP",\n        };\n\n        const exception = exceptions[n];\n\n        if(exception === undefined)\n        {\n            console.error("Unexpected CPU exception: " + n);\n            process.exit(1);\n        }\n\n        const eip = emulator.v86.cpu.instruction_pointer[0];\n        emulator.v86.cpu.write32(emulator.v86.cpu.translate_address_system_read(eip), 0xF4F4F4F4); // hlt\n\n        // XXX: On gdb execution is stopped at this point. On v86 we\n        // currently don\'t have this ability, so we record the exception\n        // and continue execution\n        recorded_exceptions.push({ exception, eip });\n        finish_test();\n        return true;\n    };\n\n    emulator.bus.register("cpu-event-halt", function() {\n        finish_test();\n    });\n\n    function finish_test()\n    {\n        if(waiting_to_receive_next_test)\n        {\n            return;\n        }\n\n        waiting_to_receive_next_test = true;\n        clearTimeout(test_timeout);\n\n        emulator.stop();\n        var cpu = emulator.v86.cpu;\n\n        const evaluated_fpu_regs = new Float64Array(8).map((_, i) => cpu.fpu_get_sti_f64(i));\n        const evaluated_mmxs = new Int32Array(16).map((_, i) => cpu.fpu_st[(i & ~1) << 1 | (i & 1)]);\n        const evaluated_xmms = cpu.reg_xmm32s;\n        const evaluated_memory = new Int32Array(cpu.mem8.slice(0x120000 - 16 * 4, 0x120000).buffer);\n        const evaluated_fpu_tag = cpu.fpu_load_tag_word();\n        const evaluated_fpu_status = cpu.fpu_load_status_word() & FPU_STATUS_MASK;\n\n        let individual_failures = [];\n\n        assert(current_test.fixture.array);\n\n        const FLOAT_TRANSLATION = {\n            [JSON_POS_INFINITY]: Infinity,\n            [JSON_NEG_INFINITY]: -Infinity,\n            [JSON_POS_NAN]: NaN,\n            [JSON_NEG_NAN]: NaN, // XXX: Ignore sign of NaN\n        };\n\n        let offset = 0;\n        const expected_reg32 = current_test.fixture.array.slice(offset, offset += 8);\n        const expected_eip = current_test.fixture.array[offset++];\n        const expected_fpu_regs =\n            current_test.fixture.array.slice(offset, offset += 8) .map(x => x in FLOAT_TRANSLATION ? FLOAT_TRANSLATION[x] : x);\n        const expected_mmx_registers = current_test.fixture.array.slice(offset, offset += 16);\n        const expected_xmm_registers = current_test.fixture.array.slice(offset, offset += 32);\n        const expected_memory = current_test.fixture.array.slice(offset, offset += 16);\n        const expected_eflags = current_test.fixture.array[offset++] & MASK_ARITH;\n        const fpu_tag = current_test.fixture.array[offset++];\n        const fpu_status = current_test.fixture.array[offset++] & FPU_STATUS_MASK;\n\n        if(!current_test.fixture.exception)\n        {\n            for (let i = 0; i < cpu.reg32.length; i++) {\n                let reg = cpu.reg32[i];\n                if (reg !== expected_reg32[i]) {\n                    individual_failures.push({\n                        name: "cpu.reg32[" + i + "]",\n                        expected: expected_reg32[i],\n                        actual: reg,\n                    });\n                }\n            }\n\n            if(fpu_tag !== FPU_TAG_ALL_INVALID)\n            {\n                for (let i = 0; i < evaluated_fpu_regs.length; i++) {\n                    if (expected_fpu_regs[i] !== "invalid" &&\n                            !float_equal(evaluated_fpu_regs[i], expected_fpu_regs[i])) {\n                        individual_failures.push({\n                            name: "st" + i,\n                            expected: expected_fpu_regs[i],\n                            actual: evaluated_fpu_regs[i],\n                        });\n                    }\n                }\n\n                if(fpu_status !== evaluated_fpu_status)\n                {\n                    individual_failures.push({\n                        name: "fpu status word",\n                        expected: fpu_status,\n                        actual: evaluated_fpu_status,\n                    });\n                }\n            }\n            else\n            {\n                for (let i = 0; i < evaluated_mmxs.length; i++) {\n                    if (evaluated_mmxs[i] !== expected_mmx_registers[i]) {\n                        individual_failures.push({\n                            name: "mm" + (i >> 1) + ".int32[" + (i & 1) + "]",\n                            expected: expected_mmx_registers[i],\n                            actual: evaluated_mmxs[i],\n                        });\n                    }\n                }\n            }\n\n            for (let i = 0; i < evaluated_xmms.length; i++) {\n                if (evaluated_xmms[i] !== expected_xmm_registers[i]) {\n                    individual_failures.push({\n                        name: "xmm" + (i >> 2) + ".int32[" + (i & 3) + "] (cpu.reg_xmm[" + i + "])",\n                        expected: expected_xmm_registers[i],\n                        actual: evaluated_xmms[i],\n                    });\n                }\n            }\n\n            for (let i = 0; i < evaluated_memory.length; i++) {\n                if (evaluated_memory[i] !== expected_memory[i]) {\n                    individual_failures.push({\n                        name: "mem[" + i + "]",\n                        expected: expected_memory[i],\n                        actual: evaluated_memory[i],\n                    });\n                }\n            }\n\n            const seen_eflags = cpu.get_eflags() & MASK_ARITH;\n            if(seen_eflags !== expected_eflags)\n            {\n                individual_failures.push({\n                    name: "eflags",\n                    expected: expected_eflags,\n                    actual: seen_eflags,\n                });\n            }\n        }\n\n        if(current_test.fixture.exception)\n        {\n            const seen_eip = (recorded_exceptions[0] || {}).eip;\n            if(seen_eip - V86_TEXT_OFFSET !== expected_eip - NASM_TEXT_OFFSET)\n            {\n                individual_failures.push({\n                    name: "exception eip",\n                    expected: expected_eip - NASM_TEXT_OFFSET,\n                    actual: seen_eip === undefined ? "(none)" : seen_eip - V86_TEXT_OFFSET,\n                });\n            }\n        }\n\n        const seen_exception = (recorded_exceptions[0] || {}).exception;\n        if(current_test.fixture.exception !== seen_exception)\n        {\n            individual_failures.push({\n                name: "Exception",\n                actual: seen_exception || "(none)",\n                expected: current_test.fixture.exception,\n            });\n        }\n\n        individual_failures = individual_failures.map(({ name, actual, expected }) => {\n            return {\n                name,\n                actual: format_value(actual),\n                expected: format_value(expected),\n            };\n        });\n\n        recorded_exceptions = [];\n\n        if (individual_failures.length > 0) {\n            process.send({\n                failures: individual_failures,\n                img_name: current_test.img_name\n            });\n        }\n        else {\n            process.send(DONE_MSG);\n        }\n    }\n\n    cluster.worker.on("message", function(message) {\n        if(message === TERMINATE_MSG)\n        {\n            emulator.stop();\n            emulator = null;\n        }\n        else\n        {\n            run_test(message);\n        }\n    });\n}\n', 'message': 'Log fixture test when test was killed during execution\n', 'author_name': 'Fabian', 'lang': 'js'}