diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index 23142a6..d383a87 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -28,5 +28,8 @@ jobs: - name: Verify xorriso installation run: xorriso -version - - name: Run build - run: make + - name: Install qemu + run: sudo apt-get install qemu-system + + - name: Run build & test + run: make test diff --git a/.gitignore b/.gitignore index 5a8ec5d..44d005f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.iso *.img *.hdd +/hexium_os-tests diff --git a/GNUmakefile b/GNUmakefile index 9a25ece..44237f8 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -28,6 +28,31 @@ run-x86_64: ovmf/ovmf-code-$(KARCH).fd ovmf/ovmf-vars-$(KARCH).fd $(IMAGE_NAME). -cdrom $(IMAGE_NAME).iso \ $(QEMUFLAGS) +.PHONY: test +test: test-iso + @set -e; \ + FAILED=0; \ + echo "\n\n\n--------RUNNING KERNEL INTEGRATION TESTS-------\n\n"; \ + for iso in hexium_os-tests/*.iso; do \ + echo "==============================="; \ + echo "Running integration test: $$iso"; \ + echo "-------------------------------"; \ + if qemu-system-x86_64 \ + -cdrom "$$iso" \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ + -serial stdio -display none; \ + then \ + echo "✅ Integration Test passed: $$iso"; \ + elif [ $$? -eq 33 ]; then \ + echo "✅ Integration Test passed (exit 33): $$iso"; \ + else \ + echo "❌ Integration Test failed: $$iso"; \ + FAILED=1; \ + fi; \ + echo ""; \ + done; \ + exit $$FAILED + ovmf/ovmf-code-$(KARCH).fd: mkdir -p ovmf curl -Lo $@ https://github.com/osdev0/edk2-ovmf-nightly/releases/latest/download/ovmf-code-$(KARCH).fd @@ -45,6 +70,10 @@ limine/limine: kernel: $(MAKE) -C kernel +.PHONY: kernel-test +kernel-test: + $(MAKE) -C kernel test + .PHONY: ramfs ramfs: mkdir -p initrd/ @@ -69,10 +98,33 @@ $(IMAGE_NAME).iso: limine/limine kernel ramfs ./limine/limine bios-install $(IMAGE_NAME).iso rm -rf iso_root +.PHONY: test-iso +test-iso: limine/limine ramfs kernel-test + mkdir -p hexium_os-tests + for testbin in kernel/kernel-test/*; do \ + testname=$$(basename $$testbin); \ + isodir=iso_root_$$testname; \ + mkdir -p $$isodir/boot/limine $$isodir/EFI/BOOT; \ + cp -v $$testbin $$isodir/boot/kernel; \ + cp -v ramfs.img $$isodir/boot/; \ + cp -v limine.conf $$isodir/boot/limine/; \ + cp -v limine/limine-bios.sys limine/limine-bios-cd.bin limine/limine-uefi-cd.bin $$isodir/boot/limine/; \ + cp -v limine/BOOTX64.EFI $$isodir/EFI/BOOT/; \ + cp -v limine/BOOTIA32.EFI $$isodir/EFI/BOOT/; \ + xorriso -as mkisofs -b boot/limine/limine-bios-cd.bin \ + -no-emul-boot -boot-load-size 4 -boot-info-table \ + --efi-boot boot/limine/limine-uefi-cd.bin \ + -efi-boot-part --efi-boot-image --protective-msdos-label \ + $$isodir -o hexium_os-tests/hexium_os-$$testname.iso; \ + ./limine/limine bios-install hexium_os-tests/hexium_os-$$testname.iso; \ + rm -rf $$isodir; \ + done + .PHONY: clean clean: $(MAKE) -C kernel clean - rm -rf iso_root $(IMAGE_NAME).iso + rm -rf iso_root *.iso + rm -rf hexium_os-tests .PHONY: distclean distclean: clean diff --git a/kernel/.gitignore b/kernel/.gitignore index 5d7f773..31130f9 100644 --- a/kernel/.gitignore +++ b/kernel/.gitignore @@ -1,2 +1,4 @@ /kernel /target +/kernel-test +.test-log.txt \ No newline at end of file diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 71292d4..7843d08 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -18,3 +18,7 @@ pc-keyboard = "0.7.0" x86 = "0.52.0" x86_64 = "0.14.2" uart_16550 = "0.3.2" + +[[test]] +name="should_panic" +harness=false \ No newline at end of file diff --git a/kernel/GNUmakefile b/kernel/GNUmakefile index e2961fe..12af846 100644 --- a/kernel/GNUmakefile +++ b/kernel/GNUmakefile @@ -28,10 +28,26 @@ all: RUSTFLAGS="-C relocation-model=static" cargo build --target $(RUST_TARGET) --profile $(RUST_PROFILE) cp target/$(RUST_TARGET)/$(RUST_PROFILE_SUBDIR)/hexium_os kernel +test: + mkdir -p kernel-test + RUSTFLAGS="-C relocation-model=static" cargo test --no-run --target $(RUST_TARGET) --profile $(RUST_PROFILE) --color always \ + 2>&1 | tee .test-log.txt + @grep -o 'target/[^ )]*' .test-log.txt | while read -r path; do \ + if [ -x "$$path" ]; then \ + echo "Copying $$path to kernel-test/"; \ + cp "$$path" kernel-test/; \ + else \ + echo "Skipping non-executable: $$path"; \ + fi \ + done + + @rm .test-log.txt + .PHONY: clean clean: cargo clean rm -rf kernel + rm -rf kernel-test .PHONY: distclean distclean: clean diff --git a/kernel/src/devices/keyboard/mod.rs b/kernel/src/devices/keyboard/mod.rs index 0f56b6b..88157d8 100644 --- a/kernel/src/devices/keyboard/mod.rs +++ b/kernel/src/devices/keyboard/mod.rs @@ -96,10 +96,10 @@ pub async fn trace_keypresses() { if let Some(key) = keyboard.process_keyevent(key_event) { match key { DecodedKey::Unicode(character) => { - trace!("Received keyboard interrupt with key: {}\n", character) + trace!("Received keyboard interrupt with key: {}", character) } DecodedKey::RawKey(key) => { - trace!("Received keyboard interrupt with key: {:?}\n", key) + trace!("Received keyboard interrupt with key: {:?}", key) } } } diff --git a/kernel/src/fs/ramfs.rs b/kernel/src/fs/ramfs.rs index 697afd1..ef49071 100644 --- a/kernel/src/fs/ramfs.rs +++ b/kernel/src/fs/ramfs.rs @@ -20,12 +20,12 @@ impl RamFs { impl FileSystem for RamFs { fn mount(&mut self, _path: &str) -> Result<(), ()> { - info!("RamFs mounted\n"); + info!("RamFs mounted"); Ok(()) } fn unmount(&mut self) -> Result<(), String> { - info!("RamFs unmounted\n"); + info!("RamFs unmounted"); Ok(()) } @@ -60,7 +60,7 @@ pub fn init(vfs: &mut VFS) { if let Some(module_response) = boot::MODULE_REQUEST.get_response() { let modules = module_response.modules(); if !modules.is_empty() { - trace!("Ramdisk information:\n"); + trace!("Ramdisk information:"); print!(" Ramdisk address: {:?}\n", modules[0].addr()); print!(" Ramdisk size (bytes): {:?}\n", modules[0].size()); print!(" Ramdisk module path: {:?}\n", modules[0].path()); diff --git a/kernel/src/interrupts/idt.rs b/kernel/src/interrupts/idt.rs index b54b417..4d8925e 100644 --- a/kernel/src/interrupts/idt.rs +++ b/kernel/src/interrupts/idt.rs @@ -29,7 +29,7 @@ pub fn init() { } extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) { - debug!("EXCEPTION: BREAKPOINT\n{:#?}\n", stack_frame); + debug!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame); } extern "x86-interrupt" fn double_fault_handler( diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index 7e9923d..f1497dc 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -1,10 +1,14 @@ #![no_std] +#![no_main] #![feature(abi_x86_interrupt)] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main="test_main"] extern crate alloc; use alloc::string::String; -use core::arch::asm; +use core::{arch::asm, panic::PanicInfo}; pub mod boot; pub mod devices; @@ -26,11 +30,13 @@ pub fn init() { let mut vfs = fs::vfs::VFS::new(None); fs::ramfs::init(&mut vfs); + print_startup_message(&mut vfs); - let mut executor = crate::task::executor::Executor::new(); - let _ = executor.spawn(crate::task::Task::new(devices::keyboard::trace_keypresses())); - executor.run(); + // Issue#30: Commented out for now as the code doesn't run past this section. Will return it back. + // let mut executor = crate::task::executor::Executor::new(); + // let _ = executor.spawn(crate::task::Task::new(devices::keyboard::trace_keypresses())); + // executor.run(); //vfs.unmount_fs(); } @@ -42,16 +48,16 @@ fn print_startup_message(vfs: &mut fs::vfs::VFS) -> [u8; 128] { Ok(vnode) => match vfs.read_file(&vnode, &mut buffer, 0) { Ok(_bytes_read) => {} Err(err) => { - error!("Error reading file: {}\n", err); + error!("Error reading file: {}", err); } }, Err(err) => { - error!("File not found: {}\n", err); + error!("File not found: {}", err); } } info!( - "Hexium OS kernel v{} succesfully initialized at {}\n", + "Hexium OS kernel v{} succesfully initialized at {}", env!("CARGO_PKG_VERSION"), unsafe { rtc::read_rtc() } ); @@ -72,3 +78,64 @@ pub fn hlt_loop() -> ! { } } } + +pub fn test_panic_handler(info: &PanicInfo) -> ! { + serial_println!("[failed]"); + serial_println!("Error: {}", info); + exit_qemu(QemuExitCode::Failed); + loop{} +} + +pub fn test_runner(tests: &[&dyn Testable]) { + serial_println!("Running {} tests", tests.len()); + + for test in tests { + test.run(); + } + + exit_qemu(QemuExitCode::Success); +} + + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) { + use x86_64::instructions::port::Port; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } +} + +pub trait Testable { + fn run(&self); +} + +impl Testable for T +where T : Fn(), +{ + fn run(&self) { + serial_print!("{}...\t", core::any::type_name::()); + self(); + serial_println!("[ok]"); + } +} + +#[cfg(test)] +#[unsafe(no_mangle)] +unsafe extern "C" fn kmain() -> ! { + test_main(); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + test_panic_handler(info) +} \ No newline at end of file diff --git a/kernel/src/log.rs b/kernel/src/log.rs index ce44d89..d2e8e46 100644 --- a/kernel/src/log.rs +++ b/kernel/src/log.rs @@ -26,7 +26,7 @@ macro_rules! log { $crate::log::LogLevel::Panic => ("PANIC", "\x1b[97;41m"), // White text on Red background }; - $crate::print!("{}[{}]\x1b[0m {}", color_code, label, format_args!($($arg)*)); + $crate::println!("{}[{}]\x1b[0m {}", color_code, label, format_args!($($arg)*)); }}; } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 7cd0d5a..b42e72d 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -1,21 +1,56 @@ #![no_std] #![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(hexium_os::test_runner)] +#![reexport_test_harness_main = "test_main"] +use core::panic::PanicInfo; use hexium_os::{boot, hlt_loop, init, panic_log}; +#[test_case] +fn test_example() { + assert_eq!(1+1, 2); +} + +#[cfg(test)] +#[unsafe(no_mangle)] +unsafe extern "C" fn kmain() -> ! { + assert!(boot::BASE_REVISION.is_supported()); + init(); + test_main(); + loop {} +} + +#[cfg(not(test))] #[unsafe(no_mangle)] unsafe extern "C" fn kmain() -> ! { assert!(boot::BASE_REVISION.is_supported()); + /* + Issue#30: The lines at the end of this comment below do not seem to have an effect after the init method above + however calling them above the init method causes a boot-loop. + NOTE: Calling them after the init method after the executor code has been commented back in, + will cause them not to be run as the executor code seems to block the 'thread'. + print!("Test"); + println!("Test2"); + */ + init(); hlt_loop(); } +#[cfg(not(test))] #[panic_handler] -fn rust_panic(info: &core::panic::PanicInfo) -> ! { - use hexium_os::utils::registers::*; +fn panic(info: &PanicInfo) -> ! { + use hexium_os::utils::registers::{print_register_dump, get_registers}; panic_log!("{}\n", info); print_register_dump(&get_registers()); - hlt_loop(); + loop {} } + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + hexium_os::test_panic_handler(info) +} \ No newline at end of file diff --git a/kernel/src/memory/mod.rs b/kernel/src/memory/mod.rs index 70994fc..66e53b2 100644 --- a/kernel/src/memory/mod.rs +++ b/kernel/src/memory/mod.rs @@ -10,11 +10,11 @@ pub mod paging; static PHYS_MEM_OFFSET: Once = Once::new(); static mut MEM_MAPPER: Option> = None; -pub fn init() -> () { +pub fn init() { if let Some(hhdm_response) = boot::HHDM_REQUEST.get_response() { PHYS_MEM_OFFSET.call_once(|| VirtAddr::new(hhdm_response.offset())); } - trace!("Hhdm offset: {:#x}\n", phys_mem_offset()); + trace!("Hhdm offset: {:#x}", phys_mem_offset()); // Create frame allocator let mut frame_allocator = diff --git a/kernel/src/memory/paging.rs b/kernel/src/memory/paging.rs index af42cfc..a4a6f3a 100644 --- a/kernel/src/memory/paging.rs +++ b/kernel/src/memory/paging.rs @@ -28,9 +28,9 @@ pub extern "x86-interrupt" fn page_fault_handler( ) { use x86_64::registers::control::Cr2; - error!("EXCEPTION: PAGE FAULT\n"); - error!("Accessed Address: {:?}\n", Cr2::read()); - error!("Error Code: {:?}\n", error_code); + error!("EXCEPTION: PAGE FAULT"); + error!("Accessed Address: {:?}", Cr2::read()); + error!("Error Code: {:?}", error_code); print!("\n{:#?}\n", stack_frame); hlt_loop(); } diff --git a/kernel/src/writer.rs b/kernel/src/writer.rs index 46884bc..8fbd1a6 100644 --- a/kernel/src/writer.rs +++ b/kernel/src/writer.rs @@ -1,4 +1,3 @@ -use crate::serial_print; use crate::{utils::types::option_to_c_void, boot}; use core::fmt; use core::ptr; @@ -92,8 +91,8 @@ macro_rules! print { #[macro_export] macro_rules! println { - () => (print!("\n")); - ($($arg:tt)*) => (crate::print!("{}\n", format_args!($($arg)*))); + () => ($crate::print!("\n")); + ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*))); } #[doc(hidden)] @@ -103,6 +102,5 @@ pub fn _print(args: fmt::Arguments) { interrupts::without_interrupts(|| { WRITER.lock().write_fmt(args).unwrap(); - serial_print!("{}", args); }); -} +} \ No newline at end of file diff --git a/kernel/tests/should_panic.rs b/kernel/tests/should_panic.rs new file mode 100644 index 0000000..ddc18c6 --- /dev/null +++ b/kernel/tests/should_panic.rs @@ -0,0 +1,27 @@ +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +use hexium_os::{exit_qemu, init, serial_print, serial_println}; + +#[unsafe(no_mangle)] +unsafe extern "C" fn kmain() -> ! { + init(); + should_fail(); + serial_print!("[test did not panic]"); + exit_qemu(hexium_os::QemuExitCode::Failed); + loop{} +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + serial_println!("[ok]"); + exit_qemu(hexium_os::QemuExitCode::Success); + loop {} +} + +fn should_fail() { + serial_println!("should_panic::should_fail...\t"); + assert_eq!(0,1); +} \ No newline at end of file diff --git a/kernel/tests/writer.rs b/kernel/tests/writer.rs new file mode 100644 index 0000000..222e152 --- /dev/null +++ b/kernel/tests/writer.rs @@ -0,0 +1,52 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(hexium_os::test_runner)] +#![reexport_test_harness_main="test_main"] + +use core::panic::PanicInfo; + +use hexium_os::{println, init}; + +#[unsafe(no_mangle)] +unsafe extern "C" fn kmain() -> ! { + init(); // Issue#30: Not sure why it's absence causes an loop running of test_println_long test. + test_main(); + loop{} +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + hexium_os::test_panic_handler(info); + loop{} +} + +#[test_case] +fn test_println_simple() { + println!("Simple print new line statement"); +} + +#[test_case] +fn test_println_long() { + for _ in 0..200 { + println!("Simple print new line many times"); + } +} + +#[test_case] +fn test_println_long_more() { + for _ in 0..200 { + println!("Simple print new line many times"); + } +} + +// TODO: Issue#31: Needs buffer access +// #[test_case] +// fn test_println_output() { +// let s = "Some test fitting single line"; +// println!("{}", s); +// for (i, c) in s.chars().enumerate() { +// let screen_char = WRITER.lock().write_char(c).buffer.chars[BUFFER_HEIGHT - 2][i].read(); +// assert_eq!(char::from(screen_char.ascii_character), c); +// } +// } \ No newline at end of file