Spaces:
Runtime error
Runtime error
| import assert from "assert/strict"; | |
| import url from "node:url"; | |
| import { Worker, isMainThread, parentPort } from "node:worker_threads"; | |
| import { createServer } from "node:http"; | |
| const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); | |
| const __filename = url.fileURLToPath(import.meta.url); | |
| process.on("unhandledRejection", exn => { throw exn; }); | |
| const USE_VIRTIO = !!process.env.USE_VIRTIO; | |
| const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD; | |
| const { V86 } = await import(TEST_RELEASE_BUILD ? "../../build/libv86.mjs" : "../../src/main.js"); | |
| const SHOW_LOGS = false; | |
| function wait(time) { | |
| return new Promise((res) => setTimeout(res, time)); | |
| } | |
| if(isMainThread) | |
| { | |
| let SERVER_PORT = 0; | |
| const tests = | |
| [ | |
| { | |
| name: "DHCP", | |
| start: () => | |
| { | |
| emulator.serial0_send("udhcpc\n"); | |
| emulator.serial0_send("echo -e done\\\\tudhcpc\n"); | |
| }, | |
| end_trigger: "done\tudhcpc", | |
| end: (capture) => | |
| { | |
| assert(/lease of 192.168.86.100 obtained/.test(capture), "lease of 192.168.86.100 obtained"); | |
| }, | |
| }, | |
| { | |
| name: "lspci", | |
| timeout: 60, | |
| start: () => | |
| { | |
| emulator.serial0_send("lspci -k\n"); | |
| emulator.serial0_send("echo -e done\\\\tlspci\n"); | |
| }, | |
| end_trigger: "done\tlspci", | |
| end: (capture) => | |
| { | |
| if(!USE_VIRTIO) { | |
| assert(/ne2k/.test(capture), "ne2k missing from lspci"); | |
| } else { | |
| assert(!/ne2k/.test(capture), "ne2k in lspci"); | |
| } | |
| }, | |
| }, | |
| { | |
| name: "ifconfig", | |
| start: () => | |
| { | |
| emulator.serial0_send("ifconfig\n"); | |
| emulator.serial0_send("echo -e done\\\\tifconfig\n"); | |
| }, | |
| end_trigger: "done\tifconfig", | |
| end: (capture) => | |
| { | |
| assert(/192.168.86.100/.test(capture), "192.168.86.100"); | |
| }, | |
| }, | |
| { | |
| name: "route", | |
| start: () => | |
| { | |
| emulator.serial0_send("ip route\n"); | |
| emulator.serial0_send("echo -e done\\\\troute\n"); | |
| }, | |
| end_trigger: "done\troute", | |
| end: (capture) => | |
| { | |
| assert(/192.168.86.1/.test(capture), "192.168.86.100"); | |
| }, | |
| }, | |
| { | |
| name: "ping 1.2.3.4", | |
| start: () => | |
| { | |
| emulator.serial0_send("ping -c 2 1.2.3.4\n"); | |
| emulator.serial0_send("echo -e done\\\\tping\n"); | |
| }, | |
| end_trigger: "done\tping", | |
| end: (capture) => | |
| { | |
| assert(/2 packets transmitted, 2 (packets )?received, 0% packet loss/.test(capture), "2 packets transmitted, 2 packets received, 0% packet loss"); | |
| assert(/from 1\.2\.3\.4:/.test(capture), "got correct source ip"); | |
| }, | |
| }, | |
| { | |
| name: "arp -a", | |
| start: () => | |
| { | |
| emulator.serial0_send("arp -a\n"); | |
| emulator.serial0_send("echo -e done\\\\tarp\n"); | |
| }, | |
| end_trigger: "done\tarp", | |
| end: (capture) => | |
| { | |
| assert(/.192.168.86.1. at 52:54:00:01:02:03 \[ether\] {2}on eth0/.test(capture), "(192.168.86.1) at 52:54:00:01:02:03 [ether] on eth0"); | |
| }, | |
| }, | |
| { | |
| name: "Accept incoming connection", | |
| timeout: 60, | |
| allow_failure: true, | |
| start: async () => | |
| { | |
| let open = await emulator.network_adapter.tcp_probe(80); | |
| assert(!open, "Probe shows port not open"); | |
| emulator.serial0_send("echo -n hello | socat TCP4-LISTEN:80 - && echo -e done\\\\tlisten\n"); | |
| await wait(1000); | |
| open = await emulator.network_adapter.tcp_probe(80); | |
| assert(open, "Probe shows port open, but does not show as a connection"); | |
| await wait(1000); | |
| let h = emulator.network_adapter.connect(80); | |
| h.on("connect", () => { | |
| h.write(new TextEncoder().encode("From VM: ")); | |
| h.on("data", (d) => { | |
| d.reverse(); | |
| h.write(d); | |
| h.write(new TextEncoder().encode("\n")); | |
| h.close(); | |
| }); | |
| }); | |
| }, | |
| end_trigger: "done\tlisten", | |
| end: (capture) => | |
| { | |
| assert(/From VM: olleh/.test(capture), "got From VM"); | |
| }, | |
| }, | |
| { | |
| name: "GET mocked.example.org", | |
| allow_failure: true, | |
| start: () => | |
| { | |
| emulator.serial0_send("wget -T 10 -O - mocked.example.org\n"); | |
| emulator.serial0_send("echo -e done\\\\tmocked.example.org\n"); | |
| }, | |
| end_trigger: "done\tmocked.example.org", | |
| end: (capture) => | |
| { | |
| assert(/This text is from the mock/.test(capture), "got mocked.example.org text"); | |
| }, | |
| }, | |
| { | |
| name: "GET example.org", | |
| allow_failure: true, | |
| start: () => | |
| { | |
| emulator.serial0_send("wget -T 10 -O - example.org\n"); | |
| emulator.serial0_send("echo -e done\\\\texample.org\n"); | |
| }, | |
| end_trigger: "done\texample.org", | |
| end: (capture) => | |
| { | |
| assert(/This domain is for use in illustrative examples in documents/.test(capture), "got example.org text"); | |
| }, | |
| }, | |
| { | |
| name: "GET local server", | |
| allow_failure: true, | |
| start: () => | |
| { | |
| emulator.serial0_send(`wget -T 10 -O - ${SERVER_PORT}.external\n`); | |
| emulator.serial0_send("echo -e done\\\\tlocal server\n"); | |
| }, | |
| end_trigger: "done\tlocal server", | |
| end: (capture) => | |
| { | |
| assert(/This text is from the local server/.test(capture), "got local server text"); | |
| }, | |
| }, | |
| { | |
| name: "GET local server with custom header", | |
| allow_failure: true, | |
| start: () => | |
| { | |
| emulator.serial0_send(`wget -S -T 10 --header='x-client-test: hello' -O - ${SERVER_PORT}.external/header\n`); | |
| emulator.serial0_send("echo -e done\\\\tlocal server custom header\n"); | |
| }, | |
| end_trigger: "done\tlocal server custom header", | |
| end: (capture) => | |
| { | |
| assert(/x-server-test: {1,}h_e_l_l_o/.test(capture), "got local server custom header"); | |
| }, | |
| }, | |
| { | |
| name: "GET local server with redirect", | |
| allow_failure: true, | |
| start: () => | |
| { | |
| emulator.serial0_send(`curl -m 10 -L -v ${SERVER_PORT}.external/redirect\n`); | |
| emulator.serial0_send("echo -e done\\\\tlocal server redirect\n"); | |
| }, | |
| end_trigger: "done\tlocal server redirect", | |
| end: (capture) => | |
| { | |
| assert(/x-was-fetch-redirected: {1,}true/.test(capture), "got local server redirect header"); | |
| assert(/This text is from the local server/.test(capture), "got actual redirect"); | |
| }, | |
| }, | |
| { | |
| name: "Forbidden character in header name", | |
| start: () => | |
| { | |
| emulator.serial0_send("wget --header='test.v86: 123' -T 10 -O - test.domain\n"); | |
| emulator.serial0_send("echo -e done\\\\tincorrect header name\n"); | |
| }, | |
| end_trigger: "done\tincorrect header name", | |
| end: (capture) => | |
| { | |
| assert(/400 Bad Request/.test(capture), "got error 400"); | |
| }, | |
| }, | |
| { | |
| name: "Empty header value", | |
| start: () => | |
| { | |
| emulator.serial0_send("wget --header='test:' -T 10 -O - test.domain\n"); | |
| emulator.serial0_send("echo -e done\\\\tempty header value\n"); | |
| }, | |
| end_trigger: "done\tempty header value", | |
| end: (capture) => | |
| { | |
| assert(/400 Bad Request/.test(capture), "got error 400"); | |
| }, | |
| }, | |
| { | |
| name: "Header without separator", | |
| start: () => | |
| { | |
| emulator.serial0_send("wget --header='testheader' -T 10 -O - test.domain\n"); | |
| emulator.serial0_send("echo -e done\\\\theader without colon\n"); | |
| }, | |
| end_trigger: "done\theader without colon", | |
| end: (capture) => | |
| { | |
| assert(/400 Bad Request/.test(capture), "got error 400"); | |
| }, | |
| }, | |
| ]; | |
| const emulator = new V86({ | |
| bios: { url: __dirname + "/../../bios/seabios.bin" }, | |
| vga_bios: { url: __dirname + "/../../bios/vgabios.bin" }, | |
| bzimage: { url: __dirname + "/../../images/buildroot-bzimage68.bin" }, | |
| autostart: true, | |
| memory_size: 64 * 1024 * 1024, | |
| disable_jit: +process.env.DISABLE_JIT, | |
| net_device: { | |
| relay_url: "fetch", | |
| type: USE_VIRTIO ? "virtio" : "ne2k", | |
| }, | |
| log_level: SHOW_LOGS ? 0x400000 : 0, | |
| }); | |
| const server = new Worker(__filename); | |
| server.on("error", (e) => { throw new Error("server: " + e); }); | |
| server.on("message", function(msg) { | |
| SERVER_PORT = msg; | |
| console.log("Server started on port " + SERVER_PORT); | |
| }); | |
| emulator.add_listener("emulator-ready", function () { | |
| let network_adapter = emulator.network_adapter; | |
| let original_fetch = network_adapter.fetch; | |
| network_adapter.fetch = (url, opts) => { | |
| if(/^http:\/\/mocked.example.org\/?/.test(url)) { | |
| let contents = new TextEncoder().encode("This text is from the mock"); | |
| let headers = new Headers(); | |
| headers.append("Content-Type", "text/plain"); | |
| headers.append("Content-Length", contents.length); | |
| return new Promise(res => setTimeout(() => res(new Response(contents, { | |
| headers | |
| })), 50)); | |
| } | |
| return original_fetch(url, opts); | |
| }; | |
| }); | |
| let test_num = 0; | |
| let booted = false; | |
| let line = ""; | |
| let capture = ""; | |
| let end_trigger; | |
| emulator.bus.register("emulator-started", function() | |
| { | |
| console.log("Booting now, please stand by"); | |
| }); | |
| emulator.add_listener("serial0-output-byte", function(byte) | |
| { | |
| const chr = String.fromCharCode(byte); | |
| if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~") | |
| { | |
| return; | |
| } | |
| let new_line = ""; | |
| if(chr === "\n") | |
| { | |
| console.log(" Captured: %s", line); | |
| new_line = line; | |
| capture += line + "\n"; | |
| line = ""; | |
| } | |
| else | |
| { | |
| line += chr; | |
| } | |
| if(new_line === end_trigger) | |
| { | |
| let test_has_failed = false; | |
| try { | |
| tests[test_num].end(capture); | |
| } catch(e) { | |
| console.log(e); | |
| test_has_failed = true; | |
| } | |
| if(!test_has_failed) | |
| { | |
| console.log("[+] Test #%d passed: %s", test_num, tests[test_num].name); | |
| } | |
| else | |
| { | |
| if(tests[test_num].allow_failure) | |
| { | |
| console.warn("[!] Test #%d failed: %s (failure allowed)", test_num, tests[test_num].name); | |
| } | |
| else | |
| { | |
| console.error("[-] Test #%d failed: %s", test_num, tests[test_num].name); | |
| server.terminate(); | |
| process.exit(1); | |
| } | |
| } | |
| test_num++; | |
| } | |
| if(!booted && line.endsWith("~% ") || new_line === end_trigger) | |
| { | |
| booted = true; | |
| if(test_num >= tests.length) | |
| { | |
| emulator.destroy(); | |
| server.terminate(); | |
| console.log("Tests finished."); | |
| } | |
| else | |
| { | |
| console.log("Starting test #%d: %s", test_num, tests[test_num].name); | |
| capture = ""; | |
| end_trigger = tests[test_num].end_trigger; | |
| tests[test_num].start(); | |
| } | |
| } | |
| }); | |
| } | |
| else | |
| { | |
| const server = createServer(function(request, response) { | |
| switch(request.method) { | |
| case "GET": | |
| if(request.url === "/") { | |
| response.end("This text is from the local server"); | |
| } else if(request.url === "/header") { | |
| response.writeHead(200, { "x-server-test": request.headers["x-client-test"].split("").join("_") || "none" }); | |
| response.end(); | |
| } else if(request.url === "/redirect") { | |
| response.writeHead(307, { "location": "/" }); | |
| response.end(); | |
| } else { | |
| response.writeHead(404); | |
| response.end("Unknown endpoint"); | |
| } | |
| break; | |
| default: | |
| response.writeHead(405); | |
| response.end("Unknown method"); | |
| break; | |
| } | |
| }); | |
| server.listen(0, () => parentPort.postMessage(server.address().port)); | |
| } | |