#!/usr/bin/env node import { setTimeout as pause } from "timers/promises"; import url from "node:url"; import fs from "node:fs"; const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD; const { V86 } = await import(TEST_RELEASE_BUILD ? "../../build/libv86.mjs" : "../../src/main.js"); process.on("unhandledRejection", exn => { throw exn; }); function regexp_escape(text) { // TODO: The official RegExp.escape() would be prefarrable to this, // but currently (Aug 2025) the May 2025 Baseline is not yet available // at github CI. return text.replace(/[/\-\\^$*+?.()|[\]{}]/g, "\\$&"); } async function exec_test(test_name, v86_config, timeout_sec, test_function) { console.log("Starting: " + test_name); const tm_start = performance.now(); const emulator = new V86(v86_config); const timeout = setTimeout(async () => { console.warn(emulator.screen_adapter.get_text_screen()); await emulator.destroy(); throw new Error("Timeout error in test " + test_name); }, timeout_sec * 1000); await new Promise(resolve => emulator.bus.register("emulator-started", () => resolve())); try { await test_function(emulator); } catch(err) { console.warn(emulator.screen_adapter.get_text_screen()); throw new Error("Error in test " + test_name, { cause: err }); } clearTimeout(timeout); await emulator.destroy(); console.log("Done: " + test_name + " (" + ((performance.now() - tm_start) / 1000).toFixed(2) + " sec)"); } /** * Execute given CLI command and wait for its completion. * * Injects command into the guest's keyboard buffer, then waits for both the * command and the expected response lines to show at the bottom of the VGA * screen. * * If command is empty then no command is executed and only the expected * response lines are waited for. If command contains only whitespace and/or * newline characters then it is send to the guest, but it does not become * part of the expected response. * * Allowed character set for command and expected is the printable subset * of 7-bit ASCII, use newline character "\n" to encode ENTER key. * * Throws an Error if the given timeout elapsed before the expected response * could be detected. * * @param {V86} emulator * @param {string} command * @param {Array} expected * @param {number} timeout_msec */ async function expect(emulator, command, expected, timeout_msec) { if(command) { for(const ch of command) { emulator.keyboard_send_text(ch); await pause(10); } expected = [new RegExp(regexp_escape(command.trimRight()) + "$"), ...expected]; await pause(100); } if(!await emulator.wait_until_vga_screen_contains(expected, {timeout_msec: timeout_msec})) { throw new Error("Timeout of " + timeout_msec + " msec expired"); } } const CONFIG_MSDOS622_HD = { bios: { url: __dirname + "/../../bios/seabios.bin" }, vga_bios: { url: __dirname + "/../../bios/vgabios.bin" }, hda: { url: __dirname + "/../../images/msdos622.img" }, autostart: true, memory_size: 32 * 1024 * 1024, log_level: 0, disable_jit: +process.env.DISABLE_JIT }; const CONFIG_TINYCORE_CD = { bios: { url: __dirname + "/../../bios/seabios.bin" }, vga_bios: { url: __dirname + "/../../bios/vgabios.bin" }, cdrom: { url: __dirname + "/../../images/TinyCore-11.0.iso" }, autostart: true, memory_size: 128 * 1024 * 1024, log_level: 0, disable_jit: +process.env.DISABLE_JIT }; await exec_test("floppy-insert-eject", CONFIG_MSDOS622_HD, 60, async emulator => { console.log("Waiting for C:\\>"); await expect(emulator, "", ["C:\\>"], 10000); console.log("Reading A:"); await expect(emulator, "dir A:\n", ["", "", "General failure reading drive A", "Abort, Retry, Fail?"], 5000); await expect(emulator, "A", ["", "C:\\>"], 1000); console.log("Inserting disk freedos722.img into drive fda"); await emulator.set_fda({ url: __dirname + "/../../images/freedos722.img" }); console.log("Reading A:X86TEST.ASM"); await expect(emulator, "dir /B A:X86TEST.ASM\n", ["X86TEST.ASM", "", "C:\\>"], 3000); console.log("Ejecting disk from drive A:"); emulator.eject_fda(); console.log("Reading A:"); await expect(emulator, "dir A:\n", ["", " Volume in drive A is FREEDOS", "", "General failure reading drive A", "Abort, Retry, Fail?"], 5000); }); await exec_test("floppy-insert-fdb", CONFIG_MSDOS622_HD, 60, async emulator => { console.log("Waiting for C:\\>"); await expect(emulator, "", ["C:\\>"], 10000); console.log("Inserting disk freedos722.img into drive fdb"); await emulator.set_fdb({ url: __dirname + "/../../images/freedos722.img" }); console.log("Reading B:X86TEST.ASM"); await expect(emulator, "dir /B B:X86TEST.ASM\n", ["X86TEST.ASM", "", "C:\\>"], 3000); console.log("Formatting B:"); await expect(emulator, "format /V:V86 /U B:\n", ["Insert new diskette for drive B:", "and press ENTER when ready..."], 3000); await expect(emulator, "\n", [/Volume Serial Number is [0-9A-F-]+/, "", "Format another (Y/N)?"], 3000); await expect(emulator, "N\n", ["", "", "C:\\>"], 3000); }); await exec_test("floppy-tinycore-linux", CONFIG_TINYCORE_CD, 60, async emulator => { console.log("Waiting for boot menu"); await expect(emulator, "", [/BIOS default device boot in \d+ seconds\.\.\./], 10000); // press arrow down key 3 times for(let i = 0; i < 3; i++) { emulator.keyboard_send_scancodes([ 0xe0, 0x50, // press 0xe0, 0x50 | 0x80, // release ]); await pause(600); } console.log("Waiting for tc@box:~$"); await expect(emulator, "\n", ["tc@box:~$"], 30000); console.log("Inserting disk windows101.img into drive fda"); await emulator.set_fda({ url: __dirname + "/../../images/windows101.img" }); console.log("Mounting /dev/fd0 into /mnt/fd0"); await expect(emulator, "mkdir /mnt/fd0\n", ["tc@box:~$"], 3000); await expect(emulator, "sudo mount /dev/fd0 /mnt/fd0\n", ["tc@box:~$"], 3000); console.log("Reading /mnt/fd0/COMMAND.COM"); await expect(emulator, "ls /mnt/fd0/COMMAND.COM\n", ["/mnt/fd0/COMMAND.COM", "tc@box:~$"], 3000); console.log("Unmounting fda"); await expect(emulator, "sudo umount /dev/fd0\n", ["tc@box:~$"], 3000); console.log("Formatting /dev/fd0"); await expect(emulator, "sudo mke2fs -q /dev/fd0\n", ["/dev/fd0 contains a vfat file system labelled 'WIN101'", "Proceed anyway? (y,N)"], 3000); await expect(emulator, "y\n", ["tc@box:~$"], 5000); console.log("Reading /mnt/fd0"); await expect(emulator, "sudo mount /dev/fd0 /mnt/fd0\n", ["tc@box:~$"], 3000); await expect(emulator, "ls /mnt/fd0\n", ["lost+found/", "tc@box:~$"], 3000); }); await exec_test("floppy-state-snapshot", CONFIG_MSDOS622_HD, 60, async emulator => { console.log("Waiting for C:\\>"); await expect(emulator, "", ["C:\\>"], 10000); console.log("Inserting disk freedos722.img into drive fda"); await emulator.set_fda({ url: __dirname + "/../../images/freedos722.img" }); console.log("Saving initial state"); const initial_state = await emulator.save_state(); console.log("Creating file A:V86TEST.TXT"); await expect(emulator, "echo v86test > A:V86TEST.TXT\n", ["", "C:\\>"], 3000); console.log("Saving modified state"); const modified_state = await emulator.save_state(); console.log("Restoring initial state"); await emulator.restore_state(initial_state); console.log("Reading A:V86TEST.TXT"); await expect(emulator, "dir /B A:V86TEST.TXT\n", ["", "C:\\>"], 3000); console.log("Restoring modified state"); await emulator.restore_state(modified_state); console.log("Reading A:V86TEST.TXT"); await expect(emulator, "dir /B A:V86TEST.TXT\n", ["V86TEST.TXT", "", "C:\\>"], 3000); });