Testing Framework
Aegis OS integration test architecture: Vortex-based QEMU harness, assertion library, presets, and visual regression testing
Testing Framework
Aegis uses a Rust-based integration test suite that boots the actual kernel in QEMU, captures serial output, and asserts on boot behavior, subsystem initialization, GUI rendering, and end-to-end installer flows. There are no unit tests for the C kernel code – all testing is full-system integration testing against QEMU.
v1 note: The test framework is v1 and growing. Contributions are welcome – file issues or propose changes at exec/aegis.
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ cargo test │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ boot_oracle │ │ login_flow │ │ installer │ │
│ │ .rs │ │ _test.rs │ │ _test.rs │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ aegis_tests library │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ │
│ │ │ harness │ │ assert │ │ presets │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ boot() │ │ subsys │ │ aegis_pc() │ │ │
│ │ │ boot_ │ │ boot_seq │ │ aegis_q35() │ │ │
│ │ │ stream() │ │ line_ │ │ graphical() │ │ │
│ │ │ boot_ │ │ contains │ │ installer() │ │ │
│ │ │ disk_ │ │ wait_for │ │ installed() │ │ │
│ │ │ only() │ │ _line() │ │ │ │ │
│ │ └────┬─────┘ └──────────┘ └────────────────┘ │ │
│ │ │ │ │
│ │ ┌────┴─────┐ │ │
│ │ │ image │ PPM loader + fuzzy compare │ │
│ │ └──────────┘ │ │
│ └────────────────────────┬────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Vortex │ │
│ │ QEMU mgmt │ │
│ └──────┬───────┘ │
│ │ │
└───────────────────────────┼─────────────────────────────┘
▼
┌──────────────┐
│ QEMU │
│ │
│ ┌──────────┐ │
│ │ Aegis OS │ │
│ └──────────┘ │
└──────────────┘
Dependencies
[package]
name = "aegis-tests"
version = "0.1.0"
edition = "2021"
[dependencies]
vortex = { path = "../../vortex", features = ["qemu"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"
Vortex is the QEMU management library that handles VM lifecycle, serial capture, monitor socket communication, screendumps, and keyboard/mouse injection. The qemu feature enables the QEMU backend. All tests are async and run on the Tokio runtime.
Running Tests
Quick Start
# Build ISO first (required by all tests)
make iso
# Run all tests
make test
# Run with q35 preset (includes NVMe disk)
make test-q35
# Run a specific test
cargo test --manifest-path tests/Cargo.toml boot_oracle -- --nocapture
# Run installer tests (requires test-iso + OVMF)
make test-iso
AEGIS_INSTALLER_ISO=build/aegis-test.iso cargo test \
--manifest-path tests/Cargo.toml --test installer_test -- --nocapture
Environment Variables
| Variable | Default | Description |
|---|---|---|
AEGIS_ISO |
build/aegis.iso |
Path to the graphical live ISO |
AEGIS_INSTALLER_ISO |
build/aegis-test.iso |
Path to the text-mode test ISO |
AEGIS_DISK |
build/disk.img |
Path to the GPT disk image |
AEGIS_BOOT_TIMEOUT |
30 |
Seconds to wait for boot completion |
AEGIS_PRESET |
(none) | Set to q35 for the Q35 machine preset |
AEGIS_UPDATE_SCREENSHOTS |
(unset) | Set to update reference screenshots |
Test Harness (harness.rs)
The AegisHarness struct provides three boot modes:
AegisHarness::boot(opts, iso)
Boots QEMU, collects all serial output until QEMU exits or the boot timeout elapses, then returns the complete ConsoleOutput. QEMU is killed on timeout.
pub async fn boot(opts: QemuOpts, iso: &Path) -> Result<ConsoleOutput, HarnessError>
Used by tests that only need to verify boot output (e.g., boot_oracle).
AegisHarness::boot_stream(opts, iso)
Boots QEMU and returns a live ConsoleStream + QemuProcess handle. The caller drives the test interactively – waiting for specific output, sending keystrokes, capturing screenshots – and is responsible for killing the process.
pub async fn boot_stream(
opts: QemuOpts,
iso: &Path,
) -> Result<(ConsoleStream, QemuProcess), HarnessError>
Used by interactive tests: login flows, installer tests, GUI tests.
AegisHarness::boot_disk_only(opts)
Boots QEMU from a persistent disk image with no ISO. Used for the second boot in installer test sequences – verifying that the installed system boots standalone from NVMe via UEFI.
pub async fn boot_disk_only(
opts: QemuOpts,
) -> Result<(ConsoleStream, QemuProcess), HarnessError>
Exit Code Mappings
The harness configures QEMU’s isa-debug-exit device at I/O port 0xf4. The kernel can write to this port to signal test outcomes:
| Raw Exit Code | Meaning | Description |
|---|---|---|
| 33 | Pass |
Kernel explicitly signalled success |
| 35 | Fail |
Kernel explicitly signalled failure (HarnessError::KernelFail) |
Boot Timeout
Default: 30 seconds (configurable via AEGIS_BOOT_TIMEOUT). On timeout, the harness kills the QEMU process and returns whatever output was collected.
Assertion Library (assert.rs)
Collected-Output Assertions
These operate on the complete ConsoleOutput returned by AegisHarness::boot():
assert_subsystem_ok(out, subsystem)
Asserts that the kernel output contains [<subsystem>] OK. Panics with full serial capture on failure.
assert_subsystem_ok(&out, "PMM"); // passes if "[PMM] OK: ..." is in output
assert_subsystem_ok(&out, "SCHED"); // passes if "[SCHED] OK: ..." is in output
assert_subsystem_fail(out, subsystem)
Asserts that [<subsystem>] FAIL appears in kernel output. Used to verify expected error conditions.
assert_boot_subsequence(out, expected)
Asserts that all strings in expected appear in the kernel output in order, with arbitrary gaps allowed between matches. This is the primary boot-sequence oracle assertion.
// Passes: gaps between PMM and SCHED are allowed
assert_boot_subsequence(&out, &["[PMM] OK", "[SCHED] OK"]);
// Fails: wrong order
assert_boot_subsequence(&out, &["[SCHED] OK", "[PMM] OK"]);
assert_line_contains(out, substr) / assert_no_line_contains(out, substr)
Positive and negative substring assertions across all output lines (not just kernel lines).
Streaming Assertion
wait_for_line(stream, pattern, timeout)
Consumes lines from a live ConsoleStream until one containing pattern is found, or the timeout expires. Returns the matching line on success, WaitTimeout on failure.
wait_for_line(&mut stream, "[BASTION] greeter ready", Duration::from_secs(30))
.await
.expect("[BASTION] greeter ready never fired within 30s");
This is the primary synchronization primitive for interactive tests – it gates on specific kernel/application log lines before proceeding to the next test step.
QEMU Presets (presets.rs)
Presets configure the virtual machine hardware for different test scenarios:
aegis_pc() – Minimal PC
Machine: pc (i440FX)
Display: none
VGA: std
CPU: Broadwell
Devices: isa-debug-exit only
Serial: captured
Monitor: disabled
Minimal configuration for boot-sequence oracle tests. No NVMe, no networking, no USB.
aegis_q35() – Full Q35
Machine: q35 (ICH9)
Display: none
VGA: std
CPU: Broadwell
Devices: xHCI + USB keyboard + USB mouse + NVMe + virtio-net
Drives: NVMe backed by build/disk.img
Network: User-mode with port forwarding (SSH:2222, HTTP:8080)
Serial: captured
Monitor: disabled
Full hardware simulation matching production configuration. Used with AEGIS_PRESET=q35.
aegis_q35_graphical_mouse() – GUI Testing
Machine: q35 (ICH9)
Display: VNC (127.0.0.1:17, walk to :99)
VGA: virtio-vga (required for framebuffer mapping)
CPU: Broadwell
Devices: virtio-vga only (no USB keyboard/mouse)
Serial: captured
Monitor: enabled (HMP socket)
Key design: No USB HID devices. HMP sendkey and mouse_move route to the first keyboard/mouse device on the machine. On q35, the ICH9 LPC provides PS/2 keyboard and AUX mouse by default. Adding usb-kbd would intercept HMP events, preventing them from reaching the PS/2 drivers that Aegis actually reads from (/dev/kbd, /dev/mouse).
The VNC display is required instead of -display none because QEMU’s screendump HMP command produces all-black output when no display backend renders the VGA surface.
aegis_q35_installer(disk_path) – Text Installer
Machine: q35
Display: none
VGA: std
Devices: NVMe (caller-supplied disk path)
Serial: captured
Monitor: enabled (for sendkey)
aegis_q35_gui_installer(disk_path) – GUI Installer
Machine: q35
Display: VNC (127.0.0.1:19)
VGA: virtio-vga (required for framebuffer)
Devices: virtio-vga + NVMe
Serial: captured
Monitor: enabled
aegis_q35_installed_ovmf(disk_path, ovmf_path) – Post-Install UEFI Boot
Machine: q35
Display: none
VGA: std
Devices: NVMe
Drives: OVMF pflash firmware (read-only) + NVMe disk
Serial: captured
Monitor: enabled
Used for Boot 2 of installer tests. Verifies the installed system boots from the NVMe drive via UEFI (OVMF firmware).
Visual Regression Testing (image.rs)
The image module provides fuzzy PPM comparison for screendump-based GUI tests.
PPM Format
QEMU’s screendump HMP command outputs P6 PPM files (binary RGB, 8-bit per channel, maxval 255). The parser handles the format directly without external image libraries.
Fuzzy Comparison
Exact pixel comparison is too brittle for animated UI (cursor blink, clock updates, focus rings). The comparison uses two thresholds:
| Metric | Threshold | Description |
|---|---|---|
| Mean absolute diff | < 2.0 | Average per-channel pixel difference across the entire image |
| Bad pixel ratio | < 0.5% | Pixels where any channel differs by more than 24 (~10% of 255) |
pub fn assert_ppm_matches(actual: &Path, reference: &Path)
Panics with detailed diagnostics (mean diff, bad pixel count/percentage) when either threshold is exceeded.
Reference Screenshot Workflow
- First run: No reference exists; the captured screenshot becomes the reference
- Subsequent runs: Captures are compared against the stored reference
- Update mode: Set
AEGIS_UPDATE_SCREENSHOTS=1to regenerate all references
Reference screenshots are stored in tests/screenshots/.
Test Suite
boot_oracle – Boot Sequence Verification
File: tests/tests/boot_oracle.rs
Boots on the minimal pc machine and asserts the kernel’s subsystem initialization sequence matches the expected boot oracle. The oracle is a subsequence of 32 kernel log lines covering all major subsystems from serial init through scheduler start:
[SERIAL] OK → [VGA] OK → [PMM] OK → [VMM] OK → [KVA] OK →
[CAP] OK → [IDT] OK → [PIC] OK → [PIT] OK → [KBD] OK →
[MOUSE] OK → [GDT] OK → [TSS] OK → [SYSCALL] OK → [SMAP] OK →
[SMEP] OK → [RNG] OK → [RAMDISK] OK → [VFS] OK → [INITRD] OK →
[ACPI] OK → [LAPIC] OK → [IOAPIC] OK → [PCIE] OK → [EXT2] OK →
[CAP_POLICY] OK → [POLL] OK → [SMP] OK → [CAP] OK (baseline) →
[VMM] OK (identity map removed) → [SCHED] OK
Also includes a negative test: on pc (no virtio-net), no [NET] lines should appear.
poll_test – Kernel Poll Self-Test
File: tests/tests/poll_test.rs
Boots on pc and asserts [POLL] OK appears in kernel output, verifying the kernel’s built-in poll() self-test passed.
screendump_test – Framebuffer Capture
File: tests/tests/screendump_test.rs
Boots graphically (q35 + virtio-vga), waits for [BASTION] greeter ready, captures a screendump, and verifies the PPM file is at least 900 KB (not truncated). Validates the QEMU screendump pipeline works end-to-end.
login_flow_test – Login + Desktop Verification
File: tests/tests/login_flow_test.rs
Full graphical login flow:
- Boot graphically, wait for
[BASTION] greeter ready - Capture Bastion greeter screenshot, compare against reference
- Send credentials:
root<Tab>forevervigilant<Enter>via HMPsendkey - Wait for
[LUMEN] ready - Capture Lumen desktop screenshot, compare against reference
Uses fuzzy PPM comparison for visual regression detection.
dock_click_test – GUI Interaction Testing
File: tests/tests/dock_click_test.rs
The most complex GUI test. After login:
- Parse
[DOCK] item=<key> cx=<x> cy=<y>lines from serial output to discover dock icon coordinates - For each testable dock item (
terminal,widgets):- Home the mouse to (0,0) with multiple small negative-delta hops
- Move to the target icon, compensating for Lumen’s 1.5x mouse speed multiplier (send 2/3 of the desired delta)
- Click (press + 80ms + release)
- Wait for
[LUMEN] window_opened=<key>confirmation - Capture post-click screenshot
The mouse homing algorithm accounts for QEMU PS/2 emulation limits (~635 delta units per poll cycle) by using 4 hops of (-500, -500) with 200ms settle between each.
installer_test – Text Installer End-to-End
File: tests/tests/installer_test.rs
Two-boot test sequence:
Boot 1 (text-mode ISO + empty NVMe):
- Boot from
aegis-test.iso(text mode) - Log in as root
- Launch
installer - Drive prompts via HMP
sendkey: confirm disk, set root password, create user account - Wait for
=== Installation complete! ===
Boot 2 (OVMF + installed NVMe, no ISO):
- Boot via UEFI firmware from the NVMe disk
- Assert
[EXT2] OK: mounted nvme0p1appears (proves UEFI boot + GRUB + ext2 mount all work)
Requires make test-iso and OVMF (apt install ovmf).
gui_installer_test – GUI Installer End-to-End
File: tests/tests/gui_installer_test.rs
Same two-boot pattern as installer_test, but drives the graphical installer wizard:
- Navigate through wizard screens via Tab/Enter (5 screens total)
- Fill user account form: root password, confirm, username, user password, confirm
- Wait for
[INSTALLER] donesentinel - Boot 2 verifies UEFI boot from installed NVMe
Uses aegis_q35_gui_installer preset (virtio-vga required for framebuffer mapping).
mouse_api_smoke_test – HMP Mouse Round-Trip
File: tests/tests/mouse_api_smoke_test.rs
Smoke test verifying Vortex’s mouse_move and mouse_button HMP methods execute without error against a live QEMU instance. Does not verify guest-side delivery – that is covered by dock_click_test.
Writing New Tests
Basic Boot Test
use aegis_tests::{aegis_pc, iso, AegisHarness, assert_subsystem_ok};
#[tokio::test]
async fn my_subsystem_boots() {
let iso = iso();
if !iso.exists() {
eprintln!("SKIP: {} not found", iso.display());
return;
}
let out = AegisHarness::boot(aegis_pc(), &iso)
.await
.expect("QEMU failed to start");
assert_subsystem_ok(&out, "MY_SUBSYSTEM");
}
Interactive Test
use aegis_tests::{aegis_q35_graphical_mouse, iso, wait_for_line, AegisHarness};
use std::time::Duration;
#[tokio::test]
async fn my_interactive_test() {
let iso = iso();
if !iso.exists() { return; }
let (mut stream, mut proc) =
AegisHarness::boot_stream(aegis_q35_graphical_mouse(), &iso)
.await
.expect("QEMU failed to start");
// Wait for a kernel/app log line
wait_for_line(&mut stream, "[MY_APP] ready", Duration::from_secs(30))
.await
.expect("app never ready");
// Send keystrokes via HMP
proc.send_keys("hello\n").await.expect("sendkey failed");
// Capture screenshot
let out = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("screenshots/my_test.ppm");
proc.screendump(&out).await.expect("screendump failed");
proc.kill().await.unwrap();
}
Graceful Skip Pattern
All tests follow a consistent skip pattern for missing prerequisites:
if !iso.exists() {
eprintln!("SKIP: {} not found — run `make iso` first", iso.display());
return;
}
This allows cargo test to run without failing when build artifacts are absent – tests that cannot run simply print a skip message and return success.