From 2f8186303b1d7c36ea27128e4160f34bb95d811f Mon Sep 17 00:00:00 2001 From: Rob Blanckaert Date: Tue, 2 Jul 2024 23:53:14 -0700 Subject: [PATCH] Add tests and respond to PR feedback --- Makefile | 1 + src/browser/fetch_network.js | 32 ++- tests/devices/fetch_network.js | 358 +++++++++++++++++++++++++++++++++ tools/fetch_network_harness.js | 5 +- 4 files changed, 385 insertions(+), 11 deletions(-) create mode 100755 tests/devices/fetch_network.js diff --git a/Makefile b/Makefile index d3ee97c5e1..0652abd0b9 100644 --- a/Makefile +++ b/Makefile @@ -302,6 +302,7 @@ expect-tests: all-debug build/libwabt.js devices-test: all-debug ./tests/devices/virtio_9p.js ./tests/devices/virtio_console.js + ./tests/devices/fetch_network.js rust-test: $(RUST_FILES) env RUSTFLAGS="-D warnings" RUST_BACKTRACE=full RUST_TEST_THREADS=1 cargo test -- --nocapture diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 403fcdd7b1..1accbfc89d 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -15,19 +15,25 @@ const NTP_EPOCH = new Date('1900-01-01T00:00:00Z').getTime(); const NTP_EPOC_DIFF = UNIX_EPOCH - NTP_EPOCH; const TWO_TO_32 = Math.pow(2, 32); +const DHCP_MAGIC_COOKIE = 0x63825363; + /** * @constructor * * @param {BusConnector} bus */ -function FetchNetworkAdapter(bus) +function FetchNetworkAdapter(bus, config) { - let adapater = this; + config = config || {}; + let adapter = this; this.bus = bus; - this.router_mac = new Uint8Array([82, 84, 0, 1, 2, 3]); - this.router_ip = new Uint8Array([192, 168, 86, 1]); + this.router_mac = new Uint8Array((config.router_mac || "52:54:0:1:2:3").split(":").map(function(x) { return parseInt(x, 16); })); + this.router_ip = new Uint8Array((config.router_ip || "192.168.86.1").split(".").map(function(x) { return parseInt(x, 10); })); + this.tcp_conn = {}; - // this.cors_proxy = 'https://corsproxy.io/?' + + // Ex: 'https://corsproxy.io/?' + this.cors_proxy = config.cors_proxy ? this.cors_proxy : false; this.bus.register("net0-send", function(data) { @@ -35,12 +41,13 @@ function FetchNetworkAdapter(bus) }, this); this.fetch = function(url, options) { - if (adapater.cors_proxy) url = adapater.cors_proxy + encodeURIComponent(url); + if (adapter.cors_proxy) url = adapter.cors_proxy + encodeURIComponent(url); return fetch(url, options).then(function(resp) { return resp.arrayBuffer().then(function(ab) { return [resp, ab]; }); }).catch(err => { + console.warn('Fetch Failed: ' + url + '\n' + String(err)); let headers = new Headers(); headers.set('Content-Type', 'text/plain'); return [ @@ -284,7 +291,7 @@ FetchNetworkAdapter.prototype.send = function(data) } if (packet.udp && packet.udp.dport == 8) { - // UTP Echo Server + // UDP Echo Server let reply = {}; reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }; reply.ipv4 = { @@ -430,7 +437,7 @@ function parse_ipv4(data, o) { }; // Ethernet minmum packet size. - if (Math.max(len, 46) != data.length) dbg_log(`ipv4 Lenghth mismatch: ${len} != ${data.length}`, LOG_NET); + if (Math.max(len, 46) != data.length) dbg_log(`ipv4 Length mismatch: ${len} != ${data.length}`, LOG_NET); o.ipv4 = ipv4; let ipdata = data.subarray(ihl * 4, len); @@ -453,7 +460,7 @@ function write_ipv4(spec, data) { let ihl = 5; // 20 byte header length normally let version = 4; - let len = 4*ihl; // Total Lenghth + let len = 4 * ihl; // Total Length if (spec.icmp) { len += write_icmp(spec, data.subarray(ihl * 4)); @@ -747,7 +754,7 @@ function write_dhcp(spec, data) { view.setUint8(28+i, spec.dhcp.chaddr[i]); } - view.setUint32(236, 0x63825363); + view.setUint32(236, DHCP_MAGIC_COOKIE); let offset = 240; for (let o of spec.dhcp.options) { @@ -1101,3 +1108,8 @@ TCPConnection.prototype.pump = function(packet) { this.net.receive(make_packet(reply)); } }; + +if(typeof module !== "undefined" && typeof module.exports !== "undefined") +{ + module.exports["FetchNetworkAdapter"] = FetchNetworkAdapter; +} \ No newline at end of file diff --git a/tests/devices/fetch_network.js b/tests/devices/fetch_network.js new file mode 100755 index 0000000000..11c86eefe8 --- /dev/null +++ b/tests/devices/fetch_network.js @@ -0,0 +1,358 @@ +#!/usr/bin/env node +"use strict"; + +process.on("unhandledRejection", exn => { throw exn; }); + +const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD; + +var V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86; +const fs = require("fs"); + +const testfsjson = require("./testfs.json"); +const { assert } = require("console"); +const SHOW_LOGS = false; +const STOP_ON_FIRST_FAILURE = false; + +function log_pass(msg, ...args) +{ + console.log(`\x1b[92m[+] ${msg}\x1b[0m`, ...args); +} + +function log_warn(msg, ...args) +{ + console.error(`\x1b[93m[!] ${msg}\x1b[0m`, ...args); +} + +function log_fail(msg, ...args) +{ + console.error(`\x1b[91m[-] ${msg}\x1b[0m`, ...args); +} + +function assert_equal(actual, expected, message) +{ + if(actual !== expected) + { + log_warn("Failed assert equal (Test: %s). %s", tests[test_num].name, message || ""); + log_warn("Expected:\n" + expected); + log_warn("Actual:\n" + actual); + test_fail(); + } +} + +function assert_not_equal(actual, expected, message) +{ + if(actual === expected) + { + log_warn("Failed assert not equal (Test: %s). %s", tests[test_num].name, message || ""); + log_warn("Expected something different than:\n" + expected); + test_fail(); + } +} + +const tests = +[ + { + name: "DHCP", + timeout: 60, + start: () => + { + emulator.serial0_send("udhcpc\n"); + emulator.serial0_send("echo -e done\\\\tudhcpc\n"); + }, + end_trigger: "done\tudhcpc", + end: (capture, done) => + { + assert(/lease of 192.168.86.100 obtained/.test(capture), "lease of 192.168.86.100 obtained"); + done(); + }, + }, + { + name: "ifconfig", + timeout: 60, + start: () => + { + emulator.serial0_send("ifconfig\n"); + emulator.serial0_send("echo -e done\\\\tifconfig\n"); + }, + end_trigger: "done\tifconfig", + end: (capture, done) => + { + assert(/192.168.86.100/.test(capture), "192.168.86.100"); + done(); + }, + }, + { + name: "route", + timeout: 60, + start: () => + { + emulator.serial0_send("ip route\n"); + emulator.serial0_send("echo -e done\\\\troute\n"); + }, + end_trigger: "done\troute", + end: (capture, done) => + { + assert(/192.168.86.1/.test(capture), "192.168.86.100"); + done(); + }, + }, + { + name: "ping 1.2.3.4", + timeout: 60, + 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, done) => + { + assert(/2 packets transmitted, 2 packets received, 0% packet loss/.test(capture), "2 packets transmitted, 2 packets received, 0% packet loss"); + done(); + }, + }, + { + name: "arp -a", + timeout: 60, + start: () => + { + emulator.serial0_send("arp -a\n"); + emulator.serial0_send("echo -e done\\\\tarp\n"); + }, + end_trigger: "done\tarp", + end: (capture, done) => + { + assert(/.192.168.86.1. at 52:54:00:01:02:03 [ether] on eth0/.test(capture), "192.168.86.100"); + done(); + }, + }, + { + name: "Curl httpbin.org/ip", + timeout: 60, + start: () => + { + emulator.serial0_send("wget -q -O - example.org\n"); + emulator.serial0_send("echo -e done\\\\thttpbin\n"); + }, + end_trigger: "done\thttpbin", + end: (capture, done) => + { + assert(/This domain is for use in illustrative examples in documents/.test(capture), "got example.org text"); + done(); + }, + }, + +]; + +let test_num = 0; +let test_timeout = 0; +let test_has_failed = false; +const failed_tests = []; + +function test_fail() +{ + if(!test_has_failed) + { + test_has_failed = true; + failed_tests.push(test_num); + } +} + +const emulator = new V86({ + bios: { url: __dirname + "/../../bios/seabios.bin" }, + vga_bios: { url: __dirname + "/../../bios/vgabios.bin" }, + cdrom: { url: __dirname + "/../../images/linux4.iso" }, + autostart: true, + memory_size: 64 * 1024 * 1024, + filesystem: { + baseurl: __dirname + "/testfs/", + }, + disable_jit: +process.env.DISABLE_JIT, + network_relay_url: 'fetch', + log_level: SHOW_LOGS ? 0x400000 : 0, +}); + +let ran_command = false; +let line = ""; +let capturing = false; +let capture = ""; +let next_trigger; +let next_trigger_handler; + +function start_timeout() +{ + if(tests[test_num].timeout) + { + test_timeout = setTimeout(() => + { + log_fail("Test #%d (%s) took longer than %s sec. Timing out and terminating.", test_num, tests[test_num].name, tests[test_num].timeout); + process.exit(1); + }, tests[test_num].timeout * 1000); + } +} + +function begin() +{ + start_timeout(); + + console.log("\nPreparing test #%d: %s", test_num, tests[test_num].name); + start_test(); +} + +function start_test() +{ + console.log("Starting test #%d: %s", test_num, tests[test_num].name); + + capture = ""; + + tests[test_num].start(); + + if(tests[test_num].capture_trigger) + { + next_trigger = tests[test_num].capture_trigger; + next_trigger_handler = start_capture; + } + else + { + next_trigger = tests[test_num].end_trigger; + next_trigger_handler = end_test; + } + start_capture(); +} + +function start_capture() +{ + console.log("Capturing..."); + capture = ""; + capturing = true; + + next_trigger = tests[test_num].end_trigger; + next_trigger_handler = end_test; +} + +function end_test() +{ + capturing = false; + + if(tests[test_num].timeout) + { + clearTimeout(test_timeout); + } + + tests[test_num].end(capture, report_test); +} + +function report_test() +{ + if(!test_has_failed) + { + log_pass("Test #%d passed: %s", test_num, tests[test_num].name); + } + else + { + if(tests[test_num].allow_failure) + { + log_warn("Test #%d failed: %s (failure allowed)", test_num, tests[test_num].name); + } + else + { + log_fail("Test #%d failed: %s", test_num, tests[test_num].name); + + if(STOP_ON_FIRST_FAILURE) + { + finish_tests(); + } + } + test_has_failed = false; + } + + test_num++; + + if(test_num < tests.length) + { + begin(); + } + else + { + finish_tests(); + } +} + +function finish_tests() +{ + emulator.stop(); + + console.log("\nTests finished."); + if(failed_tests.length == 0) + { + console.log("All tests passed"); + } + else + { + let unallowed_failure = false; + + console.error("Failed %d out of %d tests:", failed_tests.length, tests.length); + for(const num of failed_tests) + { + if(tests[num].allow_failure) + { + log_warn("#%d %s (failure allowed)", num, tests[num].name); + } + else + { + unallowed_failure = true; + log_fail("#%d %s", num, tests[num].name); + } + } + if(unallowed_failure) + { + process.exit(1); + } + } +} + +emulator.bus.register("emulator-started", function() +{ + console.error("Booting now, please stand by"); +}); + +emulator.add_listener("serial0-output-byte", function(byte) +{ + var chr = String.fromCharCode(byte); + if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~") + { + return; + } + + let new_line = ""; + let is_new_line = false; + if(chr === "\n") + { + is_new_line = true; + new_line = line; + line = ""; + } + else + { + line += chr; + } + + if(!ran_command && line.endsWith("~% ")) + { + ran_command = true; + begin(); + } + else if(new_line === next_trigger) + { + next_trigger_handler(); + } + else if(is_new_line && capturing) + { + capture += new_line + "\n"; + console.log(" Captured: %s", new_line); + } + else if(is_new_line) + { + console.log(" Serial: %s", new_line); + } +}); diff --git a/tools/fetch_network_harness.js b/tools/fetch_network_harness.js index a0e7e991b8..d2fe870350 100644 --- a/tools/fetch_network_harness.js +++ b/tools/fetch_network_harness.js @@ -6,8 +6,11 @@ const path = require('node:path'); // qemu-system-i386 -m 2G -nographic -hda ~/disk.qcow2 -netdev dgram,id=net0,remote.type=inet,remote.host=127.0.0.1,remote.port=6677,local.host=127.0.0.1,local.port=7744,local.type=inet -device e1000,netdev=net0 globalThis.dbg_assert = require('node:assert'); +globalThis.dbg_log = (what, level) => console.log(what); +globalThis.dbg_trace = (what, level) => console.trace(what); +globalThis.LOG_NET = 0; -let FetchNetworkAdapter = eval(fs.readFileSync(path.join(__dirname, 'browser', 'fetch_network.js'), 'utf-8') + ";FetchNetworkAdapter"); +let FetchNetworkAdapter = require(path.join(__dirname, '..', 'src', 'browser', 'fetch_network.js')); let events = {};