diff options
| author | Natasha Moongrave <natasha@256phi.eu> | 2026-03-30 13:34:04 +0200 |
|---|---|---|
| committer | Natasha Moongrave <natasha@256phi.eu> | 2026-03-30 13:34:15 +0200 |
| commit | 89e964849b6781eaab0c2874c6e9208d4ec09b9a (patch) | |
| tree | e71a4056e91d96d540f36bdf61668cfbd87d3170 | |
| parent | b154e39f4d9603facee89d323aa7442bc602b17d (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.toml | 8 | ||||
| -rw-r--r-- | StrixKernel/tests/frame_allocator.rs | 179 | ||||
| -rw-r--r-- | StrixKernel/tests/page_fault.rs | 128 | ||||
| -rw-r--r-- | StrixKernel/tests/page_table.rs | 142 | ||||
| -rw-r--r-- | StrixKernel/tests/timer_interrupt.rs | 123 |
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) +} |
