aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNatasha Moongrave <natasha@256phi.eu>2026-03-30 13:34:04 +0200
committerNatasha Moongrave <natasha@256phi.eu>2026-03-30 13:34:15 +0200
commit89e964849b6781eaab0c2874c6e9208d4ec09b9a (patch)
treee71a4056e91d96d540f36bdf61668cfbd87d3170
parentb154e39f4d9603facee89d323aa7442bc602b17d (diff)
Add high-value integration tests for kernel functionality
- timer_interrupt: verifies timer IRQ fires and PIC EOI works - page_fault: verifies page fault handler triggers on unmapped access - page_table: tests CR3 reading and address translation - frame_allocator: tests physical frame allocation from memory map
-rw-r--r--StrixKernel/Cargo.toml8
-rw-r--r--StrixKernel/tests/frame_allocator.rs179
-rw-r--r--StrixKernel/tests/page_fault.rs128
-rw-r--r--StrixKernel/tests/page_table.rs142
-rw-r--r--StrixKernel/tests/timer_interrupt.rs123
5 files changed, 580 insertions, 0 deletions
diff --git a/StrixKernel/Cargo.toml b/StrixKernel/Cargo.toml
index 0df1444..8e43a3d 100644
--- a/StrixKernel/Cargo.toml
+++ b/StrixKernel/Cargo.toml
@@ -27,6 +27,14 @@ harness = false # Uses custom panic handler that signals success on panic
name = "stack_overflow"
harness = false # Uses custom IDT to catch double faults
+[[test]]
+name = "timer_interrupt"
+harness = false # Uses custom IDT to count timer interrupts
+
+[[test]]
+name = "page_fault"
+harness = false # Uses custom IDT to verify page fault handling
+
# =============================================================================
# Dependencies
# =============================================================================
diff --git a/StrixKernel/tests/frame_allocator.rs b/StrixKernel/tests/frame_allocator.rs
new file mode 100644
index 0000000..dee10f4
--- /dev/null
+++ b/StrixKernel/tests/frame_allocator.rs
@@ -0,0 +1,179 @@
+//! # Frame Allocator Integration Test
+//!
+//! This integration test verifies that the `BootInfoFrameAllocator` correctly
+//! allocates physical memory frames from the bootloader's memory map.
+//!
+//! ## Purpose
+//!
+//! The frame allocator is fundamental to memory management. This test verifies:
+//! - Frames can be allocated from the bootloader's memory map
+//! - Allocated frame addresses are 4 KiB aligned
+//! - Multiple allocations return unique (non-overlapping) frames
+//! - The allocator correctly identifies usable memory regions
+//!
+//! ## Test Cases
+//!
+//! - `test_allocate_single_frame`: One allocation succeeds with valid address
+//! - `test_allocate_multiple_frames`: Sequential allocations return unique frames
+//! - `test_frame_alignment`: All allocated frames are 4 KiB aligned
+//!
+//! ## Running This Test
+//!
+//! ```bash
+//! cargo test --test frame_allocator
+//! ```
+
+#![no_std]
+#![no_main]
+#![feature(custom_test_frameworks)]
+#![test_runner(strix_os::test_runner)]
+#![reexport_test_harness_main = "test_main"]
+
+use bootloader::{entry_point, BootInfo};
+use core::panic::PanicInfo;
+use strix_os::memory::BootInfoFrameAllocator;
+use strix_os::serial_println;
+use x86_64::structures::paging::FrameAllocator;
+
+entry_point!(test_kernel_main);
+
+/// Static storage for boot info, accessible from test cases.
+static mut BOOT_INFO: Option<&'static BootInfo> = None;
+
+/// Test kernel entry point.
+///
+/// Receives `BootInfo` from the bootloader, stores it for test access,
+/// then runs the test harness.
+fn test_kernel_main(boot_info: &'static BootInfo) -> ! {
+ strix_os::init();
+
+ // Store boot_info for tests to access
+ unsafe {
+ BOOT_INFO = Some(boot_info);
+ }
+
+ test_main();
+
+ strix_os::hlt_loop();
+}
+
+#[panic_handler]
+fn panic(info: &PanicInfo) -> ! {
+ strix_os::test_panic_handler(info)
+}
+
+/// Tests that a single frame can be allocated successfully.
+///
+/// Verifies:
+/// - `allocate_frame()` returns `Some(PhysFrame)`
+/// - The frame's physical address is 4 KiB aligned
+/// - The address is in a reasonable range (non-zero)
+#[test_case]
+fn test_allocate_single_frame() {
+ let boot_info = unsafe { BOOT_INFO.unwrap() };
+ let mut allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };
+
+ let frame = allocator.allocate_frame();
+ assert!(frame.is_some(), "Failed to allocate a single frame");
+
+ let frame = frame.unwrap();
+ let addr = frame.start_address().as_u64();
+
+ serial_println!(" Allocated frame at: {:#x}", addr);
+
+ // Verify 4 KiB alignment
+ assert_eq!(addr & 0xFFF, 0, "Frame address is not 4 KiB aligned");
+
+ // Verify non-zero (frame 0 is typically reserved)
+ assert!(addr > 0, "Allocated frame 0, which is typically reserved");
+}
+
+/// Tests that multiple frame allocations return unique addresses.
+///
+/// Allocates 10 frames and verifies each address is unique.
+/// This confirms the bump allocator correctly advances its index.
+#[test_case]
+fn test_allocate_multiple_frames() {
+ let boot_info = unsafe { BOOT_INFO.unwrap() };
+ let mut allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };
+
+ let mut addresses = [0u64; 10];
+
+ for i in 0..10 {
+ let frame = allocator.allocate_frame();
+ assert!(frame.is_some(), "Failed to allocate frame {}", i);
+ addresses[i] = frame.unwrap().start_address().as_u64();
+ }
+
+ serial_println!(" Allocated 10 frames:");
+ for (i, addr) in addresses.iter().enumerate() {
+ serial_println!(" Frame {}: {:#x}", i, addr);
+ }
+
+ // Verify all addresses are unique
+ for i in 0..10 {
+ for j in (i + 1)..10 {
+ assert_ne!(
+ addresses[i], addresses[j],
+ "Duplicate frame addresses: {} and {} both at {:#x}",
+ i, j, addresses[i]
+ );
+ }
+ }
+}
+
+/// Tests that all allocated frames are properly aligned.
+///
+/// Allocates 20 frames and verifies each is 4 KiB aligned.
+/// Physical frames must be page-aligned for use in page tables.
+#[test_case]
+fn test_frame_alignment() {
+ let boot_info = unsafe { BOOT_INFO.unwrap() };
+ let mut allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };
+
+ for i in 0..20 {
+ let frame = allocator.allocate_frame();
+ assert!(frame.is_some(), "Failed to allocate frame {}", i);
+
+ let addr = frame.unwrap().start_address().as_u64();
+ assert_eq!(
+ addr & 0xFFF,
+ 0,
+ "Frame {} at {:#x} is not 4 KiB aligned",
+ i,
+ addr
+ );
+ }
+
+ serial_println!(" All 20 frames properly aligned");
+}
+
+/// Tests that frames are allocated from usable memory regions only.
+///
+/// Verifies allocated frame addresses fall within the physical memory
+/// range reported by the bootloader (sanity check).
+#[test_case]
+fn test_frames_in_valid_range() {
+ let boot_info = unsafe { BOOT_INFO.unwrap() };
+ let mut allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };
+
+ // Allocate a few frames and check they're in reasonable range
+ for i in 0..5 {
+ let frame = allocator.allocate_frame();
+ assert!(frame.is_some(), "Failed to allocate frame {}", i);
+
+ let addr = frame.unwrap().start_address().as_u64();
+
+ // Physical addresses should be below 64 GiB for most systems
+ // (This is a sanity check, not a strict requirement)
+ let max_reasonable_addr = 64 * 1024 * 1024 * 1024; // 64 GiB
+ assert!(
+ addr < max_reasonable_addr,
+ "Frame {} address {:#x} seems unreasonably high",
+ i,
+ addr
+ );
+ }
+
+ serial_println!(" All frames in valid physical memory range");
+}
diff --git a/StrixKernel/tests/page_fault.rs b/StrixKernel/tests/page_fault.rs
new file mode 100644
index 0000000..ce7e8cc
--- /dev/null
+++ b/StrixKernel/tests/page_fault.rs
@@ -0,0 +1,128 @@
+//! # Page Fault Integration Test
+//!
+//! This integration test verifies that the page fault exception handler (vector 14)
+//! triggers correctly when accessing unmapped memory.
+//!
+//! ## Purpose
+//!
+//! Page faults occur when code attempts to access memory that violates page table
+//! permissions. This test verifies:
+//! - The IDT is correctly set up with a page fault handler
+//! - Accessing an unmapped address triggers a page fault
+//! - The CR2 register contains the faulting address
+//! - The error code is provided to the handler
+//!
+//! ## Test Approach
+//!
+//! 1. Set up a custom IDT with a page fault handler that verifies CR2
+//! 2. Access an unmapped address (0xdeadbeef)
+//! 3. The handler verifies CR2 matches and exits with success
+//!
+//! ## Why Custom IDT?
+//!
+//! The kernel's page fault handler calls `hlt_loop()`, which would hang the test.
+//! We need a custom handler that signals success and exits QEMU.
+//!
+//! ## Running This Test
+//!
+//! ```bash
+//! cargo test --test page_fault
+//! ```
+
+#![no_std]
+#![no_main]
+#![feature(abi_x86_interrupt)]
+
+use core::panic::PanicInfo;
+use lazy_static::lazy_static;
+use strix_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
+use x86_64::registers::control::Cr2;
+use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame, PageFaultErrorCode};
+
+/// The address we will deliberately access to trigger a page fault.
+///
+/// This address is chosen to be:
+/// - Not identity-mapped by the bootloader
+/// - Easy to recognize in debugging (0xDEADBEEF)
+/// - In user space range (below kernel space)
+const FAULT_ADDRESS: u64 = 0xdeadbeef;
+
+lazy_static! {
+ /// Test-specific Interrupt Descriptor Table.
+ ///
+ /// Only configures the page fault handler (vector 14).
+ /// The handler verifies CR2 and exits with success.
+ static ref TEST_IDT: InterruptDescriptorTable = {
+ let mut idt = InterruptDescriptorTable::new();
+ idt.page_fault.set_handler_fn(test_page_fault_handler);
+ idt
+ };
+}
+
+/// Page fault handler for testing.
+///
+/// Verifies that CR2 contains the expected faulting address and exits
+/// with success. If the address doesn't match, exits with failure.
+extern "x86-interrupt" fn test_page_fault_handler(
+ _stack_frame: InterruptStackFrame,
+ error_code: PageFaultErrorCode,
+) {
+ let faulting_addr = Cr2::read();
+
+ // Verify we faulted at the expected address
+ if faulting_addr.as_u64() == FAULT_ADDRESS {
+ serial_println!("[ok]");
+ serial_println!(" CR2: {:#x}", faulting_addr.as_u64());
+ serial_println!(" Error code: {:?}", error_code);
+ exit_qemu(QemuExitCode::Success);
+ } else {
+ serial_println!("[failed]");
+ serial_println!(
+ " Expected fault at {:#x}, got {:#x}",
+ FAULT_ADDRESS,
+ faulting_addr.as_u64()
+ );
+ exit_qemu(QemuExitCode::Failed);
+ }
+
+ loop {}
+}
+
+/// Loads the test IDT into the CPU.
+fn init_test_idt() {
+ TEST_IDT.load();
+}
+
+/// Test entry point.
+///
+/// Initializes the GDT and custom IDT, then deliberately accesses an
+/// unmapped address to trigger a page fault.
+#[unsafe(no_mangle)]
+pub extern "C" fn _start() -> ! {
+ serial_print!("page_fault::test_page_fault_triggers...\t");
+
+ // Initialize GDT (needed for proper exception handling)
+ strix_os::gdt::init();
+
+ // Load our custom IDT with the test page fault handler
+ init_test_idt();
+
+ // Trigger a page fault by reading from an unmapped address
+ // Use u8 to avoid alignment requirements
+ let ptr = FAULT_ADDRESS as *const u8;
+ unsafe {
+ // volatile read ensures the compiler doesn't optimize this away
+ let _ = core::ptr::read_volatile(ptr);
+ }
+
+ // Should never reach here - page fault handler should have exited
+ serial_println!("[failed] - no page fault occurred");
+ exit_qemu(QemuExitCode::Failed);
+
+ loop {}
+}
+
+#[panic_handler]
+fn panic(info: &PanicInfo) -> ! {
+ strix_os::test_panic_handler(info)
+}
diff --git a/StrixKernel/tests/page_table.rs b/StrixKernel/tests/page_table.rs
new file mode 100644
index 0000000..5601366
--- /dev/null
+++ b/StrixKernel/tests/page_table.rs
@@ -0,0 +1,142 @@
+//! # Page Table Integration Test
+//!
+//! This integration test verifies that page table access and virtual-to-physical
+//! address translation work correctly in a real boot context.
+//!
+//! ## Purpose
+//!
+//! The kernel uses an `OffsetPageTable` to translate between virtual and physical
+//! addresses. This test verifies:
+//! - CR3 register can be read and contains a valid page table address
+//! - The `memory::init()` function creates a working page table mapper
+//! - Virtual-to-physical translation works for known addresses (e.g., VGA buffer)
+//!
+//! ## Test Cases
+//!
+//! - `test_cr3_read`: Verify CR3 contains a valid, aligned page table address
+//! - `test_translate_vga_buffer`: Verify VGA buffer (0xb8000) translates correctly
+//!
+//! ## Running This Test
+//!
+//! ```bash
+//! cargo test --test page_table
+//! ```
+
+#![no_std]
+#![no_main]
+#![feature(custom_test_frameworks)]
+#![test_runner(strix_os::test_runner)]
+#![reexport_test_harness_main = "test_main"]
+
+use bootloader::{entry_point, BootInfo};
+use core::panic::PanicInfo;
+use strix_os::serial_println;
+use x86_64::structures::paging::Translate;
+use x86_64::VirtAddr;
+
+entry_point!(test_kernel_main);
+
+/// Static storage for boot info, accessible from test cases.
+static mut BOOT_INFO: Option<&'static BootInfo> = None;
+
+/// Test kernel entry point.
+///
+/// Receives `BootInfo` from the bootloader, stores it for test access,
+/// then runs the test harness.
+fn test_kernel_main(boot_info: &'static BootInfo) -> ! {
+ strix_os::init();
+
+ // Store boot_info for tests to access
+ unsafe {
+ BOOT_INFO = Some(boot_info);
+ }
+
+ test_main();
+
+ strix_os::hlt_loop();
+}
+
+#[panic_handler]
+fn panic(info: &PanicInfo) -> ! {
+ strix_os::test_panic_handler(info)
+}
+
+/// Tests that the CR3 register contains a valid page table address.
+///
+/// CR3 holds the physical address of the level 4 page table (PML4).
+/// This address must be:
+/// - Non-zero (a page table must exist)
+/// - 4 KiB aligned (page tables are page-aligned)
+#[test_case]
+fn test_cr3_read() {
+ use x86_64::registers::control::Cr3;
+
+ let (frame, _flags) = Cr3::read();
+ let phys_addr = frame.start_address().as_u64();
+
+ serial_println!(" CR3 physical address: {:#x}", phys_addr);
+
+ // Verify 4 KiB alignment (bits 0-11 must be zero)
+ assert_eq!(
+ phys_addr & 0xFFF,
+ 0,
+ "CR3 address is not 4 KiB aligned"
+ );
+
+ // Verify non-zero
+ assert!(phys_addr > 0, "CR3 address is zero");
+}
+
+/// Tests that the VGA buffer address translates correctly.
+///
+/// The bootloader identity-maps low memory, so the VGA buffer at virtual
+/// address 0xb8000 should translate to physical address 0xb8000.
+#[test_case]
+fn test_translate_vga_buffer() {
+ let boot_info = unsafe { BOOT_INFO.unwrap() };
+ let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
+
+ // Create the page table mapper
+ let mapper = unsafe { strix_os::memory::init(phys_mem_offset) };
+
+ // VGA buffer is at 0xb8000 (identity-mapped by bootloader)
+ let vga_virt = VirtAddr::new(0xb8000);
+ let phys = mapper.translate_addr(vga_virt);
+
+ serial_println!(" VGA virtual: {:#x}", vga_virt.as_u64());
+ serial_println!(" VGA physical: {:?}", phys);
+
+ assert!(phys.is_some(), "VGA buffer translation returned None");
+ assert_eq!(
+ phys.unwrap().as_u64(),
+ 0xb8000,
+ "VGA buffer not identity-mapped as expected"
+ );
+}
+
+/// Tests translation of an address in the physical memory mapping region.
+///
+/// The bootloader maps all physical memory starting at `physical_memory_offset`.
+/// An address like `physical_memory_offset + 0x1000` should translate to
+/// physical address 0x1000.
+#[test_case]
+fn test_translate_physical_memory_region() {
+ let boot_info = unsafe { BOOT_INFO.unwrap() };
+ let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
+
+ let mapper = unsafe { strix_os::memory::init(phys_mem_offset) };
+
+ // Address in the physical memory mapping: offset + 0x1000
+ let test_virt = VirtAddr::new(boot_info.physical_memory_offset + 0x1000);
+ let phys = mapper.translate_addr(test_virt);
+
+ serial_println!(" Test virtual: {:#x}", test_virt.as_u64());
+ serial_println!(" Test physical: {:?}", phys);
+
+ assert!(phys.is_some(), "Physical memory region translation returned None");
+ assert_eq!(
+ phys.unwrap().as_u64(),
+ 0x1000,
+ "Physical memory mapping translation incorrect"
+ );
+}
diff --git a/StrixKernel/tests/timer_interrupt.rs b/StrixKernel/tests/timer_interrupt.rs
new file mode 100644
index 0000000..402f7f2
--- /dev/null
+++ b/StrixKernel/tests/timer_interrupt.rs
@@ -0,0 +1,123 @@
+//! # Timer Interrupt Integration Test
+//!
+//! This integration test verifies that the timer interrupt (IRQ 0, vector 32)
+//! fires correctly after enabling interrupts and that PIC EOI acknowledgment works.
+//!
+//! ## Purpose
+//!
+//! The Programmable Interval Timer (PIT) fires approximately 18.2 times per second.
+//! This test verifies:
+//! - The PIC is correctly initialized and unmasked for IRQ 0
+//! - Timer interrupts fire after `sti` (enable interrupts)
+//! - The EOI (End of Interrupt) acknowledgment works correctly
+//! - Multiple interrupts can be received in sequence
+//!
+//! ## Test Approach
+//!
+//! 1. Set up a custom IDT with a timer handler that increments an atomic counter
+//! 2. Initialize the GDT and PIC
+//! 3. Enable interrupts
+//! 4. Spin for a period to allow timer interrupts to fire
+//! 5. Verify the counter is greater than zero
+//!
+//! ## Running This Test
+//!
+//! ```bash
+//! cargo test --test timer_interrupt
+//! ```
+
+#![no_std]
+#![no_main]
+#![feature(abi_x86_interrupt)]
+
+use core::panic::PanicInfo;
+use core::sync::atomic::{AtomicUsize, Ordering};
+use lazy_static::lazy_static;
+use strix_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
+use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
+
+/// Counter for timer interrupts received.
+///
+/// Uses atomic operations because it's modified from the interrupt handler
+/// and read from the main code.
+static TIMER_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+lazy_static! {
+ /// Test-specific Interrupt Descriptor Table.
+ ///
+ /// Only configures the timer interrupt handler (vector 32).
+ /// The handler increments `TIMER_COUNT` and sends EOI.
+ static ref TEST_IDT: InterruptDescriptorTable = {
+ let mut idt = InterruptDescriptorTable::new();
+ // Timer is at vector 32 (PIC_1_OFFSET + 0)
+ idt[32].set_handler_fn(test_timer_handler);
+ idt
+ };
+}
+
+/// Timer interrupt handler for testing.
+///
+/// Increments the global counter and sends EOI to the PIC.
+/// If EOI is not sent, the PIC will not deliver further timer interrupts.
+extern "x86-interrupt" fn test_timer_handler(_stack_frame: InterruptStackFrame) {
+ TIMER_COUNT.fetch_add(1, Ordering::SeqCst);
+
+ // Send End of Interrupt to the PIC
+ unsafe {
+ strix_os::interrupts::PICS
+ .lock()
+ .notify_end_of_interrupt(32);
+ }
+}
+
+/// Loads the test IDT into the CPU.
+fn init_test_idt() {
+ TEST_IDT.load();
+}
+
+/// Test entry point.
+///
+/// Initializes the system, enables interrupts, waits for timer ticks,
+/// then verifies at least one interrupt was received.
+#[unsafe(no_mangle)]
+pub extern "C" fn _start() -> ! {
+ serial_print!("timer_interrupt::test_timer_fires...\t");
+
+ // Initialize GDT (needed for proper interrupt handling)
+ strix_os::gdt::init();
+
+ // Load our custom IDT with the test timer handler
+ init_test_idt();
+
+ // Initialize the PICs (unmasks interrupts)
+ unsafe {
+ strix_os::interrupts::PICS.lock().initialize();
+ }
+
+ // Enable interrupts - timer should start firing
+ x86_64::instructions::interrupts::enable();
+
+ // Spin and wait for timer interrupts
+ // Timer fires ~18.2 Hz (~55ms), so 2M iterations should see multiple
+ for _ in 0..2_000_000 {
+ core::hint::spin_loop();
+ }
+
+ // Check how many interrupts we received
+ let count = TIMER_COUNT.load(Ordering::SeqCst);
+
+ if count > 0 {
+ serial_println!("[ok] ({} interrupts)", count);
+ exit_qemu(QemuExitCode::Success);
+ } else {
+ serial_println!("[failed] (no interrupts received)");
+ exit_qemu(QemuExitCode::Failed);
+ }
+
+ loop {}
+}
+
+#[panic_handler]
+fn panic(info: &PanicInfo) -> ! {
+ strix_os::test_panic_handler(info)
+}