diff --git a/.cargo/config b/.cargo/config index 88c60470..c265a889 100644 --- a/.cargo/config +++ b/.cargo/config @@ -5,3 +5,4 @@ runner = "bootimage runner" dev-env = "install cargo-xbuild bootimage" run-x64 = "xrun --target=x86_64-mycelium.json" debug-x64 = "xrun --target=x86_64-mycelium.json -- -gdb tcp::1234 -S" +test-x64 = "xtest --target=x86_64-mycelium.json" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3babc59b..4ed250b4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,10 +22,27 @@ jobs: with: command: bootimage args: --target=x86_64-mycelium.json - - name: Test + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + components: rust-src, llvm-tools-preview + - name: install dev env + uses: actions-rs/cargo@v1.0.1 + with: + command: dev-env + - name: install qemu + run: sudo apt-get install qemu + - name: run tests uses: actions-rs/cargo@v1.0.1 with: - command: test + command: test-x64 clippy: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index ede37fc8..eebb1566 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "alloc", "util", + "util-macros", "hal-core", "hal-x86_64", ] @@ -12,12 +13,10 @@ version = "0.0.1" authors = ["Eliza Weisman "] edition = "2018" -[lib] -name = "mycelium_kernel" - [dependencies] hal-core = { path = "hal-core" } mycelium-alloc = { path = "alloc" } +mycelium-util = { path = "util" } tracing = { version = "0.1", default_features = false } [target.'cfg(target_arch = "x86_64")'.dependencies] @@ -35,6 +34,11 @@ wat = "1.0" [package.metadata.bootimage] default-target = "x86_64-mycelium.json" +test-success-exit-code = 33 # (0x10 << 1) | 1 +test-args = [ + "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", + "-serial", "stdio", "-display", "none" +] [package.metadata.target.'cfg(target_arch = "x86_64")'.cargo-xbuild] memcpy = true diff --git a/src/arch/x86_64.rs b/src/arch/x86_64.rs index 61444c24..37a245d4 100644 --- a/src/arch/x86_64.rs +++ b/src/arch/x86_64.rs @@ -66,10 +66,9 @@ impl BootInfo for RustbootBootInfo { } #[no_mangle] -#[cfg(target_os = "none")] pub extern "C" fn _start(info: &'static bootinfo::BootInfo) -> ! { let bootinfo = RustbootBootInfo { inner: info }; - mycelium_kernel::kernel_main(&bootinfo); + crate::kernel_main(&bootinfo); } #[cold] @@ -122,3 +121,22 @@ pub(crate) fn oops(cause: &dyn core::fmt::Display) -> ! { } } } + +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub(crate) enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +/// Exit using `isa-debug-exit`, for use in tests +/// +/// NOTE: This is a temporary mechanism until we get proper shutdown implemented +#[cfg(test)] +pub(crate) fn qemu_exit(exit_code: QemuExitCode) { + let code = exit_code as u32; + unsafe { + asm!("out 0xf4, eax" :: "{eax}"(code) :: "intel","volatile"); + } +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index fc3724fd..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -#![cfg_attr(target_os = "none", no_std)] -#![cfg_attr(target_os = "none", feature(alloc_error_handler))] - -extern crate alloc; - -use core::fmt::Write; -use hal_core::{boot::BootInfo, mem, Architecture}; - -use alloc::vec::Vec; - -mod wasm; - -const HELLOWORLD_WASM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/helloworld.wasm")); - -pub fn kernel_main(bootinfo: &impl BootInfo) -> ! -where - A: Architecture, -{ - let mut writer = bootinfo.writer(); - writeln!( - &mut writer, - "hello from mycelium {} (on {})", - env!("CARGO_PKG_VERSION"), - A::NAME - ) - .unwrap(); - writeln!(&mut writer, "booting via {}", bootinfo.bootloader_name()).unwrap(); - - if let Some(subscriber) = bootinfo.subscriber() { - tracing::dispatcher::set_global_default(subscriber).unwrap(); - } - - let mut regions = 0; - let mut free_regions = 0; - let mut free_bytes = 0; - - { - let span = tracing::info_span!("memory map"); - let _enter = span.enter(); - for region in bootinfo.memory_map() { - let kind = region.kind(); - let size = region.size(); - tracing::info!( - " {:>10?} {:>15?} {:>15?} B", - region.base_addr(), - kind, - size, - ); - regions += 1; - if region.kind() == mem::RegionKind::FREE { - free_regions += 1; - free_bytes += size; - } - } - - tracing::info!( - "found {} memory regions, {} free regions ({} bytes)", - regions, - free_regions, - free_bytes, - ); - } - - A::init_interrupts(bootinfo); - - { - let span = tracing::info_span!("alloc test"); - let _enter = span.enter(); - // Let's allocate something, for funsies - let mut v = Vec::new(); - tracing::info!(vec = ?v, vec.addr = ?v.as_ptr()); - v.push(5u64); - tracing::info!(vec = ?v, vec.addr = ?v.as_ptr()); - v.push(10u64); - tracing::info!(vec=?v, vec.addr=?v.as_ptr()); - assert_eq!(v.pop(), Some(10)); - assert_eq!(v.pop(), Some(5)); - } - - { - let span = tracing::info_span!("wasm test"); - let _enter = span.enter(); - - match wasm::run_wasm(HELLOWORLD_WASM) { - Ok(()) => tracing::info!("wasm test Ok!"), - Err(err) => tracing::error!(?err, "wasm test Err"), - } - } - - // if this function returns we would boot loop. Hang, instead, so the debug - // output can be read. - // - // eventually we'll call into a kernel main loop here... - #[allow(clippy::empty_loop)] - loop {} -} - -#[global_allocator] -#[cfg(target_os = "none")] -pub static GLOBAL: mycelium_alloc::Alloc = mycelium_alloc::Alloc; - -#[alloc_error_handler] -#[cfg(target_os = "none")] -fn alloc_error(layout: core::alloc::Layout) -> ! { - panic!("alloc error: {:?}", layout); -} diff --git a/src/main.rs b/src/main.rs index 4583e9af..e795ea05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,121 @@ #![cfg_attr(target_os = "none", feature(alloc_error_handler))] #![cfg_attr(target_os = "none", feature(asm))] #![cfg_attr(target_os = "none", feature(panic_info_message, track_caller))] +#![cfg_attr(target_os = "none", feature(custom_test_frameworks))] +#![cfg_attr(target_os = "none", test_runner(test_runner))] +#![cfg_attr(target_os = "none", reexport_test_harness_main = "test_main")] -pub mod arch; +extern crate alloc; + +use core::fmt::Write; +use hal_core::{boot::BootInfo, mem, Architecture}; +use alloc::vec::Vec; + +mod wasm; +mod arch; + +const HELLOWORLD_WASM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/helloworld.wasm")); + +pub fn kernel_main(bootinfo: &impl BootInfo) -> ! +where + A: Architecture, +{ + let mut writer = bootinfo.writer(); + writeln!( + &mut writer, + "hello from mycelium {} (on {})", + env!("CARGO_PKG_VERSION"), + A::NAME + ) + .unwrap(); + writeln!(&mut writer, "booting via {}", bootinfo.bootloader_name()).unwrap(); + + if let Some(subscriber) = bootinfo.subscriber() { + tracing::dispatcher::set_global_default(subscriber).unwrap(); + } + + let mut regions = 0; + let mut free_regions = 0; + let mut free_bytes = 0; + + { + let span = tracing::info_span!("memory map"); + let _enter = span.enter(); + for region in bootinfo.memory_map() { + let kind = region.kind(); + let size = region.size(); + tracing::info!( + " {:>10?} {:>15?} {:>15?} B", + region.base_addr(), + kind, + size, + ); + regions += 1; + if region.kind() == mem::RegionKind::FREE { + free_regions += 1; + free_bytes += size; + } + } + + tracing::info!( + "found {} memory regions, {} free regions ({} bytes)", + regions, + free_regions, + free_bytes, + ); + } + + A::init_interrupts(bootinfo); + + #[cfg(test)] + test_main(); + + // if this function returns we would boot loop. Hang, instead, so the debug + // output can be read. + // + // eventually we'll call into a kernel main loop here... + #[allow(clippy::empty_loop)] + loop {} +} + +#[cfg(test)] +fn test_runner(tests: &[&mycelium_util::testing::TestCase]) { + let span = tracing::info_span!("==== running tests ====", count = tests.len()); + let _enter = span.enter(); + + let mut fails = 0; + for test in tests { + let span = tracing::info_span!("running test", test.name); + let _enter = span.enter(); + match (test.func)() { + Ok(_) => tracing::info!(test.name, "TEST OK"), + Err(_) => { + fails += 1; + tracing::error!(test.name, "TEST FAIL"); + } + } + } + + let exit_code; + if fails == 0 { + tracing::info!("Tests OK"); + exit_code = arch::QemuExitCode::Success; + } else { + tracing::error!(fails, "Some Tests FAILED"); + exit_code = arch::QemuExitCode::Failed; + } + + arch::qemu_exit(exit_code); + panic!("failed to exit qemu"); +} + +#[global_allocator] +pub static GLOBAL: mycelium_alloc::Alloc = mycelium_alloc::Alloc; + +#[alloc_error_handler] +fn alloc_error(layout: core::alloc::Layout) -> ! { + panic!("alloc error: {:?}", layout); +} #[cfg(target_os = "none")] #[panic_handler] @@ -43,6 +156,28 @@ fn panic(panic: &core::panic::PanicInfo) -> ! { arch::oops(&pp) } +#[mycelium_util::test] +fn test_alloc() { + // Let's allocate something, for funsies + let mut v = Vec::new(); + tracing::info!(vec = ?v, vec.addr = ?v.as_ptr()); + v.push(5u64); + tracing::info!(vec = ?v, vec.addr = ?v.as_ptr()); + v.push(10u64); + tracing::info!(vec = ?v, vec.addr = ?v.as_ptr()); + assert_eq!(v.pop(), Some(10)); + assert_eq!(v.pop(), Some(5)); +} + + +#[mycelium_util::test] +fn test_wasm() { + match wasm::run_wasm(HELLOWORLD_WASM) { + Ok(()) => tracing::info!("wasm test Ok!"), + Err(err) => tracing::error!(?err, "wasm test Err"), + } +} + fn main() { unsafe { core::hint::unreachable_unchecked(); diff --git a/util-macros/Cargo.toml b/util-macros/Cargo.toml new file mode 100644 index 00000000..0496350b --- /dev/null +++ b/util-macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mycelium-util-macros" +version = "0.1.0" +authors = ["Nika Layzell "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0", features = ["full"] } +proc-macro2 = "1.0" +quote = "1.0" diff --git a/util-macros/src/lib.rs b/util-macros/src/lib.rs new file mode 100644 index 00000000..87b8e163 --- /dev/null +++ b/util-macros/src/lib.rs @@ -0,0 +1,29 @@ +extern crate proc_macro; + +use std::iter::FromIterator; + +#[proc_macro_attribute] +pub fn test( + _args: proc_macro::TokenStream, + mut ts: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let ts_clone = ts.clone(); + let func = syn::parse_macro_input!(ts_clone as syn::ItemFn); + + let ident = &func.sig.ident; + let test_name = ident.to_string(); + let test_case_ident = quote::format_ident!("TEST_CASE_{}", ident); + let result = quote::quote! { + #[test_case] + const #test_case_ident: ::mycelium_util::testing::TestCase = ::mycelium_util::testing::TestCase { + name: #test_name, + func: || ::mycelium_util::testing::TestResult::into_result(#ident()), + }; + }; + + // Go to some lengths to preserve the `TokenStream` instance, to avoid rustc + // messing up the spans. + ts.extend(proc_macro::TokenStream::from(result)); + ts +} + diff --git a/util/Cargo.toml b/util/Cargo.toml index 4a90b301..4582fd50 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -11,6 +11,8 @@ alloc = [] [dependencies] loom = { version = "0.2.14", optional = true } +mycelium-util-macros = { path = "../util-macros" } +tracing = { version = "0.1", default_features = false } [dev-dependencies] loom = "0.2.14" diff --git a/util/src/lib.rs b/util/src/lib.rs index 231e66a0..e2af29ed 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -5,7 +5,10 @@ #[cfg(feature = "alloc")] extern crate alloc; +pub use mycelium_util_macros::*; + pub mod cell; pub mod error; pub mod io; pub mod sync; +pub mod testing; diff --git a/util/src/testing.rs b/util/src/testing.rs new file mode 100644 index 00000000..2d8ad72b --- /dev/null +++ b/util/src/testing.rs @@ -0,0 +1,28 @@ +use core::fmt; + +pub struct TestCase { + pub name: &'static str, + pub func: fn() -> Result<(), ()>, +} + +pub trait TestResult { + fn into_result(self) -> Result<(), ()>; +} + +impl TestResult for () { + fn into_result(self) -> Result<(), ()> { + Ok(()) + } +} + +impl TestResult for Result<(), R> { + fn into_result(self) -> Result<(), ()> { + match self { + Ok(_) => Ok(()), + Err(err) => { + tracing::error!(?err, "test failed"); + Err(()) + } + } + } +}