Cross-architecture test suite

This commit is contained in:
2026-07-03 12:39:26 +01:00
parent 9cf135302d
commit c8e89e8115
10 changed files with 500 additions and 8 deletions
+122
View File
@@ -0,0 +1,122 @@
//! In-kernel test cases, run at the end of bring-up when the kernel is built with
//! `-Dtest-case=<name>`. Each case writes structured markers to the serial port
//! that the QEMU harness (test/qemu_test.py) asserts on:
//!
//! [PASS]/[FAIL] <check> per assertion
//! DANOS-TEST-RESULT: PASS|FAIL overall, for non-faulting cases
//!
//! Faulting cases (fault-ud, fault-pf, fault-df) deliberately don't return a
//! result line — they trigger a CPU exception, and the harness asserts on the
//! exception report the handler prints (which also reaches serial).
const std = @import("std");
const danos = @import("danos");
const arch = @import("arch");
const pmm = @import("pmm.zig");
/// Formatted write straight to serial, independent of the framebuffer console.
fn log(comptime fmt: []const u8, args: anytype) void {
var buf: [128]u8 = undefined;
arch.serialWrite(std.fmt.bufPrint(&buf, fmt, args) catch return);
}
var passed: u32 = 0;
var failed: u32 = 0;
fn check(name: []const u8, ok: bool) void {
if (ok) {
passed += 1;
log("[PASS] {s}\n", .{name});
} else {
failed += 1;
log("[FAIL] {s}\n", .{name});
}
}
pub fn run(case: []const u8, boot_info: *const BootInfo) void {
if (eql(case, "smoke")) {
smoke(boot_info);
} else if (eql(case, "fault-ud")) {
faultInvalidOpcode();
} else if (eql(case, "fault-pf")) {
faultPageFault();
} else if (eql(case, "fault-df")) {
faultDoubleFault();
} else {
log("DANOS-TEST-RESULT: FAIL (unknown case '{s}')\n", .{case});
}
}
const BootInfo = danos.BootInfo;
fn eql(a: []const u8, b: []const u8) bool {
return std.mem.eql(u8, a, b);
}
/// Non-destructive checks of the memory map and frame allocator.
fn smoke(boot_info: *const BootInfo) void {
log("DANOS-TEST-BEGIN: smoke\n", .{});
// The memory map has some usable RAM.
const mm = boot_info.memory_map;
const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len];
var usable: u64 = 0;
for (regions) |r| {
if (r.kind == .usable) usable += r.pages;
}
check("memory map reports usable RAM", usable > 0);
// The frame allocator hands out distinct, page-aligned frames.
const a = pmm.alloc();
const b = pmm.alloc();
check("alloc returns a frame", a != null);
check("alloc returns distinct frames", a != null and b != null and a.? != b.?);
check("frames are page-aligned", (a orelse 1) % danos.page_size == 0);
// Freeing restores the count.
const before = pmm.stats().free_frames;
if (a) |p| pmm.free(p);
if (b) |p| pmm.free(p);
check("free returns frames to the pool", pmm.stats().free_frames == before + 2);
// Paging is active on our own tables (CR3 is non-zero and page-aligned).
const cr3 = arch.readCr3();
check("paging active (CR3 set)", cr3 != 0 and cr3 % danos.page_size == 0);
log("DANOS-TEST-RESULT: {s} ({d} passed, {d} failed)\n", .{
if (failed == 0) "PASS" else "FAIL",
passed,
failed,
});
log("DANOS-TEST-DONE\n", .{});
}
fn faultInvalidOpcode() void {
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
asm volatile ("ud2");
}
fn faultPageFault() void {
log("DANOS-TEST-BEGIN: fault-pf\n", .{});
// Runtime address so the backend emits a register store (not a `mov moffs`,
// which the self-hosted x86_64 backend can't encode).
var addr: u64 = 0xdeadbeef000; // above our identity-mapped 4 GiB
const p: *volatile u64 = @ptrFromInt(addr);
p.* = 1;
addr += 0;
}
fn faultDoubleFault() void {
log("DANOS-TEST-BEGIN: fault-df\n", .{});
// Point RSP at unmapped memory, then fault: the CPU can't push the fault
// frame, which escalates to #DF — survivable only because #DF runs on IST1.
var bad_sp: u64 = 0x5000000000;
asm volatile (
\\mov %[sp], %%rsp
\\ud2
:
: [sp] "r" (bad_sp),
: .{ .memory = true }
);
bad_sp += 0;
}