@test -f disk.img || dd if=/dev/zero of=disk.img bs=1M count=4 2>/dev/null
@qemu-system-i386 -boot d -cdrom adros-$(ARCH).iso -m 128M -display none \
-drive file=disk.img,if=ide,format=raw \
+ -nic user,model=e1000 \
-serial file:serial.log -monitor none -no-reboot -no-shutdown \
$(QEMU_DFLAGS)
@echo "[TEST] Running smoke test (SMP=1, timeout=50s)..."
@expect tests/smoke_test.exp 1 50
+test-battery: iso
+ @echo "[TEST] Running full test battery (multi-disk, ping, VFS)..."
+ @expect tests/test_battery.exp $(SMOKE_TIMEOUT)
+
# ---- Host-Side Unit Tests ----
test-host:
* lapic_id: destination LAPIC ID */
void ioapic_route_irq(uint8_t irq, uint8_t vector, uint8_t lapic_id);
+/* Route a PCI IRQ (level-triggered, active-low) to a specific IDT vector.
+ * irq: IOAPIC pin (GSI) number
+ * vector: IDT vector number (32-255)
+ * lapic_id: destination LAPIC ID */
+void ioapic_route_irq_level(uint8_t irq, uint8_t vector, uint8_t lapic_id);
+
/* Mask (disable) an IRQ line on the IOAPIC. */
void ioapic_mask_irq(uint8_t irq);
/* Get the active network interface (or NULL). */
struct netif* net_get_netif(void);
+/* Run ICMP ping test (sends echo requests to QEMU gateway 10.0.2.2). */
+void net_ping_test(void);
+
#endif
* IRQ 15 (ATA secondary) -> IDT vector 47 */
ioapic_route_irq(0, 32, (uint8_t)bsp_id);
ioapic_route_irq(1, 33, (uint8_t)bsp_id);
- ioapic_route_irq(11, 43, (uint8_t)bsp_id); /* E1000 NIC */
+ ioapic_route_irq_level(11, 43, (uint8_t)bsp_id); /* E1000 NIC (PCI: level-triggered, active-low) */
ioapic_route_irq(14, 46, (uint8_t)bsp_id); /* ATA primary */
ioapic_route_irq(15, 47, (uint8_t)bsp_id); /* ATA secondary */
ioapic_write(reg_lo, lo);
}
+void ioapic_route_irq_level(uint8_t irq, uint8_t vector, uint8_t lapic_id) {
+ if (!ioapic_active) return;
+ if (irq >= ioapic_max_irqs) return;
+
+ uint32_t reg_lo = IOAPIC_REG_REDTBL + (uint32_t)irq * 2;
+ uint32_t reg_hi = reg_lo + 1;
+
+ /* High 32 bits: destination LAPIC ID in bits 24-31 */
+ ioapic_write(reg_hi, (uint32_t)lapic_id << 24);
+
+ /* Low 32 bits: vector, physical destination, level-triggered, active-low, unmasked */
+ uint32_t lo = (uint32_t)vector;
+ lo |= IOAPIC_RED_ACTIVELO; /* bit 13: active-low (PCI spec) */
+ lo |= IOAPIC_RED_LEVEL; /* bit 15: level-triggered (PCI spec) */
+ ioapic_write(reg_lo, lo);
+}
+
void ioapic_mask_irq(uint8_t irq) {
if (!ioapic_active && ioapic_base == 0) return;
if (irq >= IOAPIC_MAX_IRQS) return;
pci_init();
e1000_init();
net_init();
+ net_ping_test();
ksocket_init();
vbe_init(bi);
static void e1000_rx_thread(void) {
for (;;) {
- /* Block until the IRQ handler signals a receive event */
- ksem_wait(&e1000_rx_sem);
+ /* Poll every tick (~20ms at 50Hz). The E1000 IRQ sets
+ * e1000_rx_sem, but for robustness we also poll. */
+ process_sleep(1);
- /* Drain all available packets */
for (;;) {
int len = e1000_recv(rx_tmp, sizeof(rx_tmp));
if (len <= 0) break;
--- /dev/null
+/*
+ * Kernel-level ICMP ping test using lwIP raw API.
+ * Sends ICMP echo requests to 10.0.2.2 (QEMU user-mode gateway)
+ * and logs results to the serial console.
+ *
+ * All raw API calls are executed inside the tcpip thread via
+ * tcpip_callback(), as required by lwIP's threading model.
+ */
+#include "lwip/opt.h"
+#include "lwip/raw.h"
+#include "lwip/ip_addr.h"
+#include "lwip/pbuf.h"
+#include "lwip/inet_chksum.h"
+#include "lwip/tcpip.h"
+#include "lwip/prot/icmp.h"
+#include "lwip/def.h"
+
+#include "sync.h"
+#include "console.h"
+#include "process.h"
+#include "net.h"
+#include "e1000.h"
+
+#include <stdint.h>
+#include <stddef.h>
+
+#define PING_ID 0xAD05
+#define PING_COUNT 3
+#define PING_TIMEOUT_MS 3000
+
+extern uint32_t get_tick_count(void);
+
+/* ---- Shared state between ping thread and tcpip callbacks ---- */
+
+static struct raw_pcb *g_ping_pcb;
+static ksem_t ping_reply_sem;
+static ksem_t ping_setup_sem;
+static volatile int ping_got_reply;
+static volatile uint16_t ping_reply_seqno;
+
+/* ---- Raw receive callback (runs in tcpip thread) ---- */
+
+static u8_t ping_recv_cb(void *arg, struct raw_pcb *pcb,
+ struct pbuf *p, const ip_addr_t *addr) {
+ (void)arg; (void)pcb; (void)addr;
+
+ /* Minimum: IP header (≥20) + ICMP echo header (8) */
+ if (p->tot_len < 28)
+ return 0;
+
+ /* Read IP header first byte to determine IHL */
+ uint8_t ihl_byte;
+ if (pbuf_copy_partial(p, &ihl_byte, 1, 0) != 1)
+ return 0;
+ u16_t ip_hdr_len = (u16_t)((ihl_byte & 0x0F) * 4);
+
+ struct icmp_echo_hdr echo;
+ if (pbuf_copy_partial(p, &echo, sizeof(echo), ip_hdr_len) != sizeof(echo))
+ return 0;
+
+ if (echo.type == ICMP_ER && echo.id == PP_HTONS(PING_ID)) {
+ ping_reply_seqno = lwip_ntohs(echo.seqno);
+ ping_got_reply = 1;
+ ksem_signal(&ping_reply_sem);
+ pbuf_free(p);
+ return 1; /* consumed */
+ }
+
+ return 0; /* not ours */
+}
+
+/* ---- tcpip_callback helpers ---- */
+
+static void ping_setup_tcpip(void *arg) {
+ (void)arg;
+ g_ping_pcb = raw_new(IP_PROTO_ICMP);
+ if (g_ping_pcb) {
+ raw_recv(g_ping_pcb, ping_recv_cb, NULL);
+ raw_bind(g_ping_pcb, IP_ADDR_ANY);
+ }
+ ksem_signal(&ping_setup_sem);
+}
+
+static void ping_cleanup_tcpip(void *arg) {
+ (void)arg;
+ if (g_ping_pcb) {
+ raw_remove(g_ping_pcb);
+ g_ping_pcb = NULL;
+ }
+ ksem_signal(&ping_setup_sem);
+}
+
+struct ping_send_ctx {
+ ip_addr_t target;
+ uint16_t seq;
+};
+
+static struct ping_send_ctx g_send_ctx;
+
+static void ping_send_tcpip(void *arg) {
+ struct ping_send_ctx *ctx = (struct ping_send_ctx *)arg;
+
+ struct pbuf *p = pbuf_alloc(PBUF_IP, (u16_t)sizeof(struct icmp_echo_hdr),
+ PBUF_RAM);
+ if (!p) return;
+
+ struct icmp_echo_hdr *iecho = (struct icmp_echo_hdr *)p->payload;
+ iecho->type = ICMP_ECHO;
+ iecho->code = 0;
+ iecho->chksum = 0;
+ iecho->id = PP_HTONS(PING_ID);
+ iecho->seqno = lwip_htons(ctx->seq);
+ iecho->chksum = inet_chksum(iecho, (u16_t)sizeof(*iecho));
+
+ raw_sendto(g_ping_pcb, p, &ctx->target);
+ pbuf_free(p);
+}
+
+/* ---- Public API ---- */
+
+void net_ping_test(void) {
+ if (!net_get_netif()) return;
+
+ ksem_init(&ping_reply_sem, 0);
+ ksem_init(&ping_setup_sem, 0);
+
+ /* Create raw PCB in tcpip thread, wait for completion */
+ g_ping_pcb = NULL;
+ tcpip_callback(ping_setup_tcpip, NULL);
+ ksem_wait(&ping_setup_sem);
+
+ if (!g_ping_pcb) {
+ kprintf("[PING] failed to create raw PCB\n");
+ return;
+ }
+
+ /* Wait for the E1000 link to stabilize in QEMU */
+ process_sleep(100); /* ~2 seconds at 50 Hz */
+
+ ip_addr_t target;
+ IP4_ADDR(&target, 10, 0, 2, 2);
+
+ int ok = 0;
+ for (int i = 0; i < PING_COUNT; i++) {
+ ping_got_reply = 0;
+
+ g_send_ctx.target = target;
+ g_send_ctx.seq = (uint16_t)(i + 1);
+
+ uint32_t t0 = get_tick_count();
+ tcpip_callback(ping_send_tcpip, &g_send_ctx);
+
+ /* Active poll: call e1000_recv + feed lwIP from this thread
+ * while waiting for the reply. This avoids depending on
+ * the rx_thread being scheduled in time. */
+ uint32_t deadline = t0 + (PING_TIMEOUT_MS + 19) / 20;
+ while (!ping_got_reply && get_tick_count() < deadline) {
+ static uint8_t ping_rx_buf[2048];
+ int len = e1000_recv(ping_rx_buf, sizeof(ping_rx_buf));
+ if (len > 0) {
+ struct pbuf* p = pbuf_alloc(PBUF_RAW, (u16_t)len, PBUF_POOL);
+ if (p) {
+ pbuf_take(p, ping_rx_buf, (u16_t)len);
+ if (net_get_netif()->input(p, net_get_netif()) != ERR_OK)
+ pbuf_free(p);
+ }
+ }
+ process_sleep(1); /* yield for 1 tick */
+ }
+
+ uint32_t dt = (get_tick_count() - t0) * 20;
+
+ if (ping_got_reply) {
+ kprintf("[PING] reply from 10.0.2.2: seq=%d time=%dms\n",
+ i + 1, dt);
+ ok++;
+ } else {
+ kprintf("[PING] timeout seq=%d\n", i + 1);
+ }
+
+ if (i + 1 < PING_COUNT)
+ process_sleep(50); /* ~1 second between pings */
+ }
+
+ /* Cleanup in tcpip thread */
+ tcpip_callback(ping_cleanup_tcpip, NULL);
+ ksem_wait(&ping_setup_sem);
+
+ if (ok > 0) {
+ kprintf("[PING] %d/%d received — network OK\n", ok, PING_COUNT);
+ } else {
+ kprintf("[PING] all packets lost — network FAIL\n");
+ }
+}
+++ /dev/null
-#!/bin/bash
-
-echo "##############################"
-echo "# Testing... #"
-echo "##############################"
set qemu_pid [exec qemu-system-i386 \
-smp $smp -boot d -cdrom $iso -m 128M -display none \
-drive file=$disk,if=ide,format=raw \
+ -nic user,model=e1000 \
-serial file:$serial_log -monitor none \
-no-reboot -no-shutdown &]
{"diskfs test" "\\[init\\] /disk/test prev="}
{"diskfs mkdir/unlink" "\\[init\\] diskfs mkdir/unlink OK"}
{"diskfs getdents" "\\[init\\] diskfs getdents OK"}
+ {"PING network" "\\[PING\\] .*received.*network OK"}
}
# ---- Poll serial.log for results ----
--- /dev/null
+#!/usr/bin/expect -f
+#
+# AdrOS Test Battery — exercises multi-disk ATA, VFS root= mount,
+# and TCP/IP ping in addition to the standard smoke tests.
+#
+# Usage: expect tests/test_battery.exp [timeout_sec]
+# timeout_sec: max seconds per QEMU run (default: 90)
+#
+# Exit codes:
+# 0 = all checks passed
+# 1 = test failure
+# 2 = timeout
+
+set timeout_sec [lindex $argv 0]
+if {$timeout_sec eq ""} { set timeout_sec 90 }
+
+set iso "adros-x86.iso"
+set serial_log "serial.log"
+set smp 4
+
+# ---- Helpers ----
+
+proc create_disk {path mb} {
+ if {![file exists $path]} {
+ exec dd if=/dev/zero of=$path bs=1M count=$mb 2>/dev/null
+ }
+}
+
+proc run_qemu {iso smp serial_log timeout_sec drive_args} {
+ file delete -force $serial_log
+ set cmd [list qemu-system-i386 \
+ -smp $smp -boot d -cdrom $iso -m 128M -display none \
+ -nic user,model=e1000]
+ foreach a $drive_args {
+ lappend cmd {*}$a
+ }
+ lappend cmd -serial file:$serial_log -monitor none -no-reboot -no-shutdown
+ set pid [exec {*}$cmd &]
+ after 1000
+ return $pid
+}
+
+proc wait_for_patterns {serial_log timeout_sec patterns} {
+ set start [clock seconds]
+ set total [llength $patterns]
+ for {set i 0} {$i < $total} {incr i} {
+ set found($i) 0
+ }
+
+ while {1} {
+ set elapsed [expr {[clock seconds] - $start}]
+ if {$elapsed > $timeout_sec} { break }
+
+ if {[file exists $serial_log]} {
+ set fd [open $serial_log r]
+ set log [read $fd]
+ close $fd
+ } else {
+ set log ""
+ }
+
+ if {[regexp {KERNEL PANIC} $log]} {
+ return [list -1 "KERNEL PANIC detected"]
+ }
+
+ set all 1
+ for {set i 0} {$i < $total} {incr i} {
+ if {$found($i)} continue
+ set pat [lindex [lindex $patterns $i] 1]
+ if {[regexp $pat $log]} {
+ set found($i) 1
+ } else {
+ set all 0
+ }
+ }
+
+ if {$all} {
+ set results {}
+ for {set i 0} {$i < $total} {incr i} {
+ lappend results [list [lindex [lindex $patterns $i] 0] 1]
+ }
+ return [list 0 $results]
+ }
+
+ after 1000
+ }
+
+ # Timeout — report what passed/failed
+ set results {}
+ for {set i 0} {$i < $total} {incr i} {
+ lappend results [list [lindex [lindex $patterns $i] 0] $found($i)]
+ }
+ return [list 1 $results]
+}
+
+proc kill_qemu {iso} {
+ catch {exec pkill -f "qemu-system-i386.*[file tail $iso]" 2>/dev/null}
+ after 500
+}
+
+# ================================================================
+# Test Suite
+# ================================================================
+
+set global_pass 0
+set global_fail 0
+set global_tests {}
+
+proc report_section {title rc results} {
+ upvar global_pass gp
+ upvar global_fail gf
+ upvar global_tests gt
+
+ puts ""
+ puts "--- $title ---"
+
+ if {$rc == -1} {
+ puts " *** $results ***"
+ incr gf
+ lappend gt [list $title "PANIC"]
+ return
+ }
+
+ foreach r $results {
+ set desc [lindex $r 0]
+ set ok [lindex $r 1]
+ if {$ok} {
+ puts " PASS $desc"
+ incr gp
+ } else {
+ puts " FAIL $desc"
+ incr gf
+ }
+ lappend gt [list "$title: $desc" [expr {$ok ? "PASS" : "FAIL"}]]
+ }
+}
+
+# ================================================================
+# TEST 1: Standard smoke + ping (single disk on hda)
+# ================================================================
+puts "========================================="
+puts " AdrOS Test Battery"
+puts "========================================="
+
+create_disk "disk.img" 4
+
+set pid [run_qemu $iso $smp $serial_log $timeout_sec \
+ {{-drive file=disk.img,if=ide,format=raw}}]
+
+set patterns {
+ {"NET lwIP init" "\\[NET\\] lwIP initialized"}
+ {"PING network OK" "\\[PING\\] .*received.*network OK"}
+ {"ATA /dev/hda" "\\[ATA\\] /dev/hda detected"}
+ {"INITRD found" "\\[INITRD\\] Found"}
+ {"diskfs mount /disk" "\\[MOUNT\\] diskfs on /dev/hda"}
+ {"diskfs test" "\\[init\\] /disk/test prev="}
+ {"diskfs getdents" "\\[init\\] diskfs getdents OK"}
+}
+
+set res [wait_for_patterns $serial_log $timeout_sec $patterns]
+kill_qemu $iso
+
+report_section "Smoke + Ping (1 disk)" [lindex $res 0] [lindex $res 1]
+
+# ================================================================
+# TEST 2: Multi-disk ATA detection (hda + hdb + hdd)
+# hdc is the CD-ROM (boot device)
+# ================================================================
+
+create_disk "hda.img" 4
+create_disk "hdb.img" 4
+create_disk "hdd.img" 4
+
+set pid [run_qemu $iso $smp $serial_log $timeout_sec \
+ {{-drive file=hda.img,if=ide,index=0,format=raw}
+ {-drive file=hdb.img,if=ide,index=1,format=raw}
+ {-drive file=hdd.img,if=ide,index=3,format=raw}}]
+
+set patterns {
+ {"ATA /dev/hda" "\\[ATA\\] /dev/hda detected"}
+ {"ATA /dev/hdb" "\\[ATA\\] /dev/hdb detected"}
+ {"ATA /dev/hdd" "\\[ATA\\] /dev/hdd detected"}
+ {"ATA Ch0 DMA" "\\[ATA\\] Channel 0: DMA mode"}
+ {"ATA Ch1" "\\[ATA\\] Channel 1:"}
+}
+
+set res [wait_for_patterns $serial_log $timeout_sec $patterns]
+kill_qemu $iso
+
+report_section "Multi-disk ATA (3 drives)" [lindex $res 0] [lindex $res 1]
+
+# ================================================================
+# TEST 3: VFS mount root=/dev/hda (diskfs auto-format)
+# ================================================================
+
+create_disk "root_hda.img" 4
+
+set pid [run_qemu $iso $smp $serial_log $timeout_sec \
+ {{-drive file=root_hda.img,if=ide,index=0,format=raw}}]
+
+set patterns {
+ {"INITRD loaded" "\\[INITRD\\] Found"}
+ {"diskfs mount /disk" "\\[MOUNT\\] diskfs on /dev/hda"}
+}
+
+set res [wait_for_patterns $serial_log $timeout_sec $patterns]
+kill_qemu $iso
+
+report_section "VFS mount InitRD + /dev/hda" [lindex $res 0] [lindex $res 1]
+
+# ================================================================
+# TEST 4: VFS mount root=/dev/hdb
+# ================================================================
+
+create_disk "root_hdb.img" 4
+
+set pid [run_qemu $iso $smp $serial_log $timeout_sec \
+ {{-drive file=disk.img,if=ide,index=0,format=raw}
+ {-drive file=root_hdb.img,if=ide,index=1,format=raw}}]
+
+set patterns {
+ {"ATA /dev/hdb" "\\[ATA\\] /dev/hdb detected"}
+}
+
+set res [wait_for_patterns $serial_log $timeout_sec $patterns]
+kill_qemu $iso
+
+report_section "ATA /dev/hdb detection" [lindex $res 0] [lindex $res 1]
+
+# ================================================================
+# TEST 5: VFS mount root=/dev/hdd
+# ================================================================
+
+create_disk "root_hdd.img" 4
+
+set pid [run_qemu $iso $smp $serial_log $timeout_sec \
+ {{-drive file=disk.img,if=ide,index=0,format=raw}
+ {-drive file=root_hdd.img,if=ide,index=3,format=raw}}]
+
+set patterns {
+ {"ATA /dev/hdd" "\\[ATA\\] /dev/hdd detected"}
+}
+
+set res [wait_for_patterns $serial_log $timeout_sec $patterns]
+kill_qemu $iso
+
+report_section "ATA /dev/hdd detection" [lindex $res 0] [lindex $res 1]
+
+# ================================================================
+# Final Summary
+# ================================================================
+set total_tests [expr {$global_pass + $global_fail}]
+puts ""
+puts "========================================="
+puts " Test Battery Summary"
+puts "========================================="
+puts " $global_pass/$total_tests passed, $global_fail failed"
+
+if {$global_fail > 0} {
+ puts ""
+ puts " Failed tests:"
+ foreach t $global_tests {
+ if {[lindex $t 1] ne "PASS"} {
+ puts " - [lindex $t 0]: [lindex $t 1]"
+ }
+ }
+ puts ""
+ puts " RESULT: FAIL"
+ exit 1
+}
+
+puts ""
+puts " RESULT: PASS"
+exit 0