From: Tulio A M Mendes Date: Fri, 13 Feb 2026 11:06:06 +0000 (-0300) Subject: feat: ICMP ping test, IOAPIC level-triggered PCI IRQ, multi-disk test battery X-Git-Url: https://projects.tadryanom.me/docs/static/gitweb.js?a=commitdiff_plain;h=b4392e6a185406ff7d65f56b7cbce63145dac0e9;p=AdrOS.git feat: ICMP ping test, IOAPIC level-triggered PCI IRQ, multi-disk test battery - net_ping.c: kernel ICMP ping test using lwIP raw API with inline e1000_recv polling (3 pings to 10.0.2.2 QEMU gateway) - ioapic: add ioapic_route_irq_level() for PCI interrupts (level-triggered, active-low per PCI spec) - arch_platform: route E1000 NIC IRQ 11 via ioapic_route_irq_level - e1000_netif: rx_thread uses process_sleep(1) polling fallback - smoke_test.exp: add PING network pattern (20/20 tests) - test_battery.exp: 16 tests covering multi-disk ATA detection (hda+hdb+hdd), VFS InitRD+diskfs mount, ping, and diskfs ops - Makefile: add test-battery target and -nic user,model=e1000 --- diff --git a/Makefile b/Makefile index ba43e63..4a58b2d 100644 --- a/Makefile +++ b/Makefile @@ -196,6 +196,7 @@ run: iso @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) @@ -251,6 +252,10 @@ test-1cpu: iso @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: diff --git a/include/arch/x86/ioapic.h b/include/arch/x86/ioapic.h index e741d47..a2bddcf 100644 --- a/include/arch/x86/ioapic.h +++ b/include/arch/x86/ioapic.h @@ -34,6 +34,12 @@ int ioapic_init(void); * 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); diff --git a/include/net.h b/include/net.h index d129611..43f336b 100644 --- a/include/net.h +++ b/include/net.h @@ -12,4 +12,7 @@ void net_poll(void); /* 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 diff --git a/src/arch/x86/arch_platform.c b/src/arch/x86/arch_platform.c index 4798d89..5ef8248 100644 --- a/src/arch/x86/arch_platform.c +++ b/src/arch/x86/arch_platform.c @@ -127,7 +127,7 @@ int arch_platform_setup(const struct boot_info* bi) { * 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 */ diff --git a/src/arch/x86/ioapic.c b/src/arch/x86/ioapic.c index 6778c1d..5f8173b 100644 --- a/src/arch/x86/ioapic.c +++ b/src/arch/x86/ioapic.c @@ -80,6 +80,23 @@ void ioapic_route_irq(uint8_t irq, uint8_t vector, uint8_t lapic_id) { 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; diff --git a/src/kernel/init.c b/src/kernel/init.c index 91bf14c..eb410aa 100644 --- a/src/kernel/init.c +++ b/src/kernel/init.c @@ -195,6 +195,7 @@ int init_start(const struct boot_info* bi) { pci_init(); e1000_init(); net_init(); + net_ping_test(); ksocket_init(); vbe_init(bi); diff --git a/src/net/e1000_netif.c b/src/net/e1000_netif.c index 5c85397..2bec758 100644 --- a/src/net/e1000_netif.c +++ b/src/net/e1000_netif.c @@ -98,10 +98,10 @@ static uint8_t rx_tmp[2048]; 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; diff --git a/src/net/net_ping.c b/src/net/net_ping.c new file mode 100644 index 0000000..08284f1 --- /dev/null +++ b/src/net/net_ping.c @@ -0,0 +1,194 @@ +/* + * 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 +#include + +#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"); + } +} diff --git a/tests/1-test.sh b/tests/1-test.sh deleted file mode 100755 index 7944993..0000000 --- a/tests/1-test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo "##############################" -echo "# Testing... #" -echo "##############################" diff --git a/tests/smoke_test.exp b/tests/smoke_test.exp index 1aef2c8..e073f3c 100755 --- a/tests/smoke_test.exp +++ b/tests/smoke_test.exp @@ -33,6 +33,7 @@ file delete -force $serial_log 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 &] @@ -61,6 +62,7 @@ set tests { {"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 ---- diff --git a/tests/test_battery.exp b/tests/test_battery.exp new file mode 100644 index 0000000..b475d39 --- /dev/null +++ b/tests/test_battery.exp @@ -0,0 +1,274 @@ +#!/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