]> Projects (at) Tadryanom (dot) Me - AdrOS.git/commitdiff
feat: ICMP ping test, IOAPIC level-triggered PCI IRQ, multi-disk test battery
authorTulio A M Mendes <[email protected]>
Fri, 13 Feb 2026 11:06:06 +0000 (08:06 -0300)
committerTulio A M Mendes <[email protected]>
Fri, 13 Feb 2026 11:06:06 +0000 (08:06 -0300)
- 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

Makefile
include/arch/x86/ioapic.h
include/net.h
src/arch/x86/arch_platform.c
src/arch/x86/ioapic.c
src/kernel/init.c
src/net/e1000_netif.c
src/net/net_ping.c [new file with mode: 0644]
tests/1-test.sh [deleted file]
tests/smoke_test.exp
tests/test_battery.exp [new file with mode: 0644]

index ba43e6347fad008e4cd37f326c60c0e9a094d96c..4a58b2d490dbbab06898f434219839d9f66f0dde 100644 (file)
--- 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:
index e741d47254d4be9433240de4fd87fc1d58b79691..a2bddcf1ce32cd5aa665ac54d77b260528fb8829 100644 (file)
@@ -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);
 
index d129611c2f25727ce37313f5033879ded390995f..43f336b0fda89b00d974fdb6784b24c204e98316 100644 (file)
@@ -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
index 4798d89bfe4f054ad781171a6cdb1ef73385c7e4..5ef8248dd78734360e2c7f95829862743511a8e3 100644 (file)
@@ -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 */
 
index 6778c1d0bbe02120436873608ed5ccec8cf58b4a..5f8173bc004d95c9db7a0e964a89c48d3269a000 100644 (file)
@@ -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;
index 91bf14c5c05b8abd8dfcde8786b55ccb2bfd2ada..eb410aab59ad560aa072cb5c5885a2937e1a3694 100644 (file)
@@ -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);
 
index 5c853979df6103e10b3c487a2307ee20c85cbdfc..2bec758722f1ad1fddd7c37278dd78a972cfe7a2 100644 (file)
@@ -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 (file)
index 0000000..08284f1
--- /dev/null
@@ -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 <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");
+    }
+}
diff --git a/tests/1-test.sh b/tests/1-test.sh
deleted file mode 100755 (executable)
index 7944993..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/bash
-
-echo "##############################"
-echo "# Testing...                 #"
-echo "##############################"
index 1aef2c8b612ebd6c35334c4a9855be569f3725a1..e073f3cf6e735cb9dd7c3d27ad61215a48c1222c 100755 (executable)
@@ -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 (file)
index 0000000..b475d39
--- /dev/null
@@ -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