diff --git a/Cargo.lock b/Cargo.lock index 712653a..1547d91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,24 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + [[package]] name = "bootloader" version = "0.9.34" @@ -24,9 +42,17 @@ dependencies = [ "bootloader", "lazy_static", "spin 0.5.2", - "volatile", + "uart_16550", + "volatile 0.2.7", + "x86_64", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "spin" version = "0.5.2" @@ -39,8 +65,37 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "uart_16550" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614ff2a87880d4bd4374722268598a970bbad05ced8bf630439417347254ab2e" +dependencies = [ + "bitflags 1.3.2", + "rustversion", + "x86_64", +] + [[package]] name = "volatile" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b06ad3ed06fef1713569d547cdbdb439eafed76341820fb0e0344f29a41945" + +[[package]] +name = "volatile" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442887c63f2c839b346c192d047a7c87e73d0689c9157b00b53dcc27dd5ea793" + +[[package]] +name = "x86_64" +version = "0.14.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c101112411baafbb4bf8d33e4c4a80ab5b02d74d2612331c61e8192fc9710491" +dependencies = [ + "bit_field", + "bitflags 2.11.0", + "rustversion", + "volatile 0.4.6", +] diff --git a/Cargo.toml b/Cargo.toml index f1b2035..75c670c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,19 +5,32 @@ edition = "2024" authors = ["Ahmed Nagi"] [profile.dev] -panic = "abort" - +# panic = "abort" [profile.release] panic = "abort" [[bin]] name = "rust-kernel" -test = false +test = true bench = false +[package.metadata.bootimage] +test-args = [ + "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio", + "-display", "none" +] +test-success-exit-code = 33 +test-timeout = 300 # (in seconds) + +[[test]] +name = "should_panic" +harness = false + [dependencies] bootloader = "0.9" volatile = "0.2.6" lazy_static = { version = "1.5", features = ["spin_no_std"] } spin = "0.5.2" +x86_64 = "0.14.2" +uart_16550 = "0.2.0" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..18a3e78 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,71 @@ +#![no_std] +#![cfg_attr(test, no_main)] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +pub mod serial; +pub mod vga_buffer; + +use core::panic::PanicInfo; + +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]"); + } +} + +pub fn test_runner(tests: &[&dyn Testable]) { + serial_println!("Running {} tests", tests.len()); + + for test in tests { + test.run(); + } + exit_qemu(QemuExitCode::Success); +} + +pub fn test_panic_handler(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + exit_qemu(QemuExitCode::Failed); + + loop {} +} + +#[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); + } +} + +// Entry point for `cargo test` +#[cfg(test)] +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + test_main(); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + test_panic_handler(info) +} diff --git a/src/main.rs b/src/main.rs index c7bf5db..6008167 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,32 @@ #![no_std] #![no_main] - -mod vga_buffer; +#![feature(custom_test_frameworks)] +#![test_runner(rust_kernel::test_runner)] +#![reexport_test_harness_main = "test_main"] use core::panic::PanicInfo; +use rust_kernel::println; #[unsafe(no_mangle)] pub extern "C" fn _start() -> ! { println!("Hello World{}", "!"); - panic!("Some panic message"); + + #[cfg(test)] + test_main(); + loop {} } +// called on panic. +#[cfg(not(test))] #[panic_handler] fn panic(info: &PanicInfo) -> ! { - println!("{info}"); + println!("{}", info); loop {} } + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + rust_kernel::test_panic_handler(info) +} diff --git a/src/serial.rs b/src/serial.rs new file mode 100644 index 0000000..e8807df --- /dev/null +++ b/src/serial.rs @@ -0,0 +1,37 @@ +use lazy_static::lazy_static; +use spin::Mutex; +use uart_16550::SerialPort; + +lazy_static! { + pub static ref SERIAL1: Mutex = { + let mut serial_port = unsafe { SerialPort::new(0x3F8) }; + serial_port.init(); + Mutex::new(serial_port) + }; +} + +#[doc(hidden)] +pub fn _print(args: ::core::fmt::Arguments) { + use core::fmt::Write; + SERIAL1 + .lock() + .write_fmt(args) + .expect("Printing to serial failed"); +} + +/// Prints to the host through the serial interface. +#[macro_export] +macro_rules! serial_print { + ($($arg:tt)*) => { + $crate::serial::_print(format_args!($($arg)*)); + }; +} + +/// Prints to the host through the serial interface, appending a newline. +#[macro_export] +macro_rules! serial_println { + () => ($crate::serial_print!("\n")); + ($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n"))); + ($fmt:expr, $($arg:tt)*) => ($crate::serial_print!( + concat!($fmt, "\n"), $($arg)*)); +} diff --git a/src/vga_buffer.rs b/src/vga_buffer.rs index a95c08d..e34aa23 100644 --- a/src/vga_buffer.rs +++ b/src/vga_buffer.rs @@ -39,16 +39,16 @@ impl ColorCode { #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(C)] struct ScreenChar { - ascii_character: u8, - color_code: ColorCode, + pub ascii_character: u8, + pub color_code: ColorCode, } -const BUFFER_HEIGHT: usize = 25; -const BUFFER_WIDTH: usize = 80; +pub const BUFFER_HEIGHT: usize = 25; +pub const BUFFER_WIDTH: usize = 80; #[repr(transparent)] struct Buffer { - chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT], + pub chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT], } pub struct Writer { @@ -142,3 +142,31 @@ pub fn _print(args: fmt::Arguments) { use core::fmt::Write; WRITER.lock().write_fmt(args).unwrap(); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test_case] + fn test_println_simple() { + println!("test_println_simple output"); + } + + #[test_case] + fn test_println_many() { + for _ in 0..200 { + println!("test_println_many output") + } + } + + #[test_case] + fn test_println_output() { + let s = "Some test string that fits on a single line"; + println!("{s}"); + + for (i, c) in s.chars().enumerate() { + let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read(); + assert_eq!(char::from(screen_char.ascii_character), c) + } + } +} diff --git a/tests/basic_boot.rs b/tests/basic_boot.rs new file mode 100644 index 0000000..4df2c6c --- /dev/null +++ b/tests/basic_boot.rs @@ -0,0 +1,25 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(rust_kernel::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; + +use rust_kernel::println; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + test_main(); + loop {} +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + rust_kernel::test_panic_handler(info) +} + +#[test_case] +fn test_println() { + println!("test_println output"); +} diff --git a/tests/should_panic.rs b/tests/should_panic.rs new file mode 100644 index 0000000..3b1b79e --- /dev/null +++ b/tests/should_panic.rs @@ -0,0 +1,25 @@ +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use rust_kernel::{QemuExitCode, exit_qemu, serial_print, serial_println}; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + should_fail(); + serial_println!("[test did not panic]"); + exit_qemu(QemuExitCode::Failed); + loop {} +} + +fn should_fail() { + serial_print!("should_panic::should_fail...\t"); + assert_eq!(0, 1); +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +}