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..8e5b562b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,17 +22,55 @@ 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 update && sudo apt-get install qemu + - name: run tests + uses: actions-rs/cargo@v1.0.1 + with: + command: test-x64 + + host-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + - name: run host tests uses: actions-rs/cargo@v1.0.1 with: command: test + args: --all --all-features clippy: runs-on: ubuntu-latest needs: build steps: - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + components: clippy - name: rust-clippy-check uses: actions-rs/clippy-check@v1.0.5 with: token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features diff --git a/Cargo.toml b/Cargo.toml index ede37fc8..8cf5b3ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,17 @@ edition = "2018" [lib] name = "mycelium_kernel" +harness = false + +[[bin]] +name = "mycelium_kernel" +path = "src/main.rs" +test = false [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 +42,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/alloc/src/lib.rs b/alloc/src/lib.rs index 580f7c73..6c9f3502 100644 --- a/alloc/src/lib.rs +++ b/alloc/src/lib.rs @@ -2,7 +2,7 @@ //! Allocates into a "large" static array. #![no_std] -use core::alloc::{Layout, GlobalAlloc}; +use core::alloc::{GlobalAlloc, Layout}; use core::cell::UnsafeCell; use core::mem::MaybeUninit; use core::ptr; @@ -14,7 +14,7 @@ macro_rules! try_null { Some(x) => x, None => return ptr::null_mut(), } - } + }; } // 640k is enough for anyone @@ -85,7 +85,7 @@ mod test { assert_eq!(p0.align_offset(mem::align_of::()), 0); assert_eq!(p1.align_offset(mem::align_of::()), 0); - assert_eq!((p1 as usize) - (p0 as usize), 4); + assert_eq!((p0 as usize) - (p1 as usize), 4); } #[test] @@ -99,8 +99,8 @@ mod test { let p2 = unsafe { alloc(Layout::new::()) }; assert!(!p2.is_null()); - assert_eq!((p1 as usize) - (p0 as usize), 1); - assert!((p2 as usize) - (p1 as usize) > 0); + assert_eq!((p0 as usize) - (p1 as usize), 1); + assert!((p1 as usize) - (p2 as usize) > 0); assert_eq!(p2.align_offset(mem::align_of::()), 0); } diff --git a/hal-core/src/addr.rs b/hal-core/src/addr.rs index b52a2355..4089515c 100644 --- a/hal-core/src/addr.rs +++ b/hal-core/src/addr.rs @@ -40,7 +40,7 @@ pub trait Address: /// This is equivalent to /// ```rust /// # use hal_core::Address; - /// # fn doc(addr: A) -> A { + /// # fn doc(addr: T) -> T { /// addr.align_up(core::mem::align_of::()) /// # } /// ```` @@ -68,7 +68,7 @@ pub trait Address: /// This is equivalent to /// ```rust /// # use hal_core::Address; - /// # fn doc(addr: A) -> A { + /// # fn doc(addr: T) -> T { /// addr.align_down(core::mem::align_of::()) /// # } /// ```` diff --git a/src/arch/x86_64.rs b/src/arch/x86_64.rs index 76312846..201b6d3d 100644 --- a/src/arch/x86_64.rs +++ b/src/arch/x86_64.rs @@ -153,9 +153,38 @@ pub fn oops(cause: &dyn core::fmt::Display) -> ! { let _ = writeln!(vga, "\n uwu we did a widdle fucky-wucky!\n{}", cause); let _ = vga.write_str("\n it will never be safe to turn off your computer."); + #[cfg(test)] + qemu_exit(QemuExitCode::Failed); + + #[cfg(not(test))] loop { unsafe { asm!("hlt" :::: "volatile"); } } } + +#[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"); + + // If the previous line didn't immediately trigger shutdown, hang. + asm!("cli" :::: "volatile"); + loop { + asm!("hlt" :::: "volatile"); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index e46e4d42..60a2e01c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ +#![cfg_attr(all(target_os = "none", test), no_main)] #![cfg_attr(target_os = "none", no_std)] #![cfg_attr(target_os = "none", feature(alloc_error_handler))] +#![cfg_attr(target_os = "none", feature(panic_info_message, track_caller))] #![feature(asm)] + extern crate alloc; pub mod arch; @@ -8,12 +11,8 @@ pub mod arch; use core::fmt::Write; use hal_core::{boot::BootInfo, mem}; -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) -> ! { let mut writer = bootinfo.writer(); writeln!( @@ -62,10 +61,44 @@ pub fn kernel_main(bootinfo: &impl BootInfo) -> ! { arch::interrupt::init::(); + #[cfg(test)] { - let span = tracing::info_span!("alloc test"); + let span = tracing::info_span!("run tests"); let _enter = span.enter(); + + let mut passed = 0; + let mut failed = 0; + for test in mycelium_util::testing::all_tests() { + let span = tracing::info_span!("test", test.name, test.module); + let _enter = span.enter(); + + if (test.run)() { + passed += 1; + } else { + failed += 1; + } + } + + tracing::warn!("{} passed | {} failed", passed, failed); + if failed == 0 { + arch::qemu_exit(arch::QemuExitCode::Success); + } else { + arch::qemu_exit(arch::QemuExitCode::Failed); + } + } + + // 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 {} +} + +mycelium_util::decl_test! { + fn basic_alloc() { // Let's allocate something, for funsies + use alloc::vec::Vec; let mut v = Vec::new(); tracing::info!(vec = ?v, vec.addr = ?v.as_ptr()); v.push(5u64); @@ -75,23 +108,13 @@ pub fn kernel_main(bootinfo: &impl BootInfo) -> ! { 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"), - } +mycelium_util::decl_test! { + fn wasm_hello_world() -> Result<(), wasmi::Error> { + const HELLOWORLD_WASM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/helloworld.wasm")); + wasm::run_wasm(HELLOWORLD_WASM) } - - // 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] @@ -103,3 +126,45 @@ pub static GLOBAL: mycelium_alloc::Alloc = mycelium_alloc::Alloc; fn alloc_error(layout: core::alloc::Layout) -> ! { panic!("alloc error: {:?}", layout); } + +#[cfg(target_os = "none")] +#[panic_handler] +#[cold] +fn panic(panic: &core::panic::PanicInfo) -> ! { + use core::fmt; + + struct PrettyPanic<'a>(&'a core::panic::PanicInfo<'a>); + impl<'a> fmt::Display for PrettyPanic<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = self.0.message(); + let location = self.0.location(); + let caller = core::panic::Location::caller(); + if let Some(message) = message { + writeln!(f, " mycelium panicked: {}", message)?; + if let Some(loc) = location { + writeln!(f, " at: {}:{}:{}", loc.file(), loc.line(), loc.column(),)?; + } + } else { + writeln!(f, " mycelium panicked: {}", self.0)?; + } + writeln!( + f, + " in: {}:{}:{}", + caller.file(), + caller.line(), + caller.column() + )?; + Ok(()) + } + } + + let caller = core::panic::Location::caller(); + tracing::error!(%panic, ?caller); + let pp = PrettyPanic(panic); + arch::oops(&pp) +} + +#[cfg(all(test, not(target_os = "none")))] +pub fn main() { + /* no host-platform tests in this crate */ +} diff --git a/src/main.rs b/src/main.rs index 75b86274..85b51e83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,47 +3,6 @@ #![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))] -use mycelium_kernel::arch; -#[cfg(target_os = "none")] -#[panic_handler] -#[cold] -fn panic(panic: &core::panic::PanicInfo) -> ! { - use core::fmt; - - struct PrettyPanic<'a>(&'a core::panic::PanicInfo<'a>); - impl<'a> fmt::Display for PrettyPanic<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let message = self.0.message(); - let location = self.0.location(); - let caller = core::panic::Location::caller(); - if let Some(message) = message { - writeln!(f, " mycelium panicked: {}", message)?; - if let Some(loc) = location { - writeln!(f, " at: {}:{}:{}", loc.file(), loc.line(), loc.column(),)?; - } - } else { - writeln!(f, " mycelium panicked: {}", self.0)?; - } - writeln!( - f, - " in: {}:{}:{}", - caller.file(), - caller.line(), - caller.column() - )?; - Ok(()) - } - } - - let caller = core::panic::Location::caller(); - tracing::error!(%panic, ?caller); - let pp = PrettyPanic(panic); - arch::oops(&pp) -} - -fn main() { - unsafe { - core::hint::unreachable_unchecked(); - } -} +// Force linking to the `mycelium_kernel` lib. +use mycelium_kernel; diff --git a/util/Cargo.toml b/util/Cargo.toml index 4a90b301..408099c7 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" alloc = [] [dependencies] +tracing = { version = "0.1", default_features = false } loom = { version = "0.2.14", optional = true } [dev-dependencies] diff --git a/util/src/io/cursor.rs b/util/src/io/cursor.rs index b1b04970..9a7796fb 100644 --- a/util/src/io/cursor.rs +++ b/util/src/io/cursor.rs @@ -242,26 +242,18 @@ mod tests { use alloc::{boxed::Box, vec, vec::Vec}; #[test] + #[cfg(feature = "alloc")] fn test_vec_writer() { let mut writer = Vec::new(); assert_eq!(writer.write(&[0]).unwrap(), 1); assert_eq!(writer.write(&[1, 2, 3]).unwrap(), 3); assert_eq!(writer.write(&[4, 5, 6, 7]).unwrap(), 4); - assert_eq!( - writer - .write_vectored(&[ - IoSlice::new(&[]), - IoSlice::new(&[8, 9]), - IoSlice::new(&[10]) - ],) - .unwrap(), - 3 - ); - let b: &[_] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let b: &[_] = &[0, 1, 2, 3, 4, 5, 6, 7]; assert_eq!(writer, b); } #[test] + #[cfg(feature = "alloc")] fn test_mem_writer() { let mut writer = Cursor::new(Vec::new()); assert_eq!(writer.write(&[0]).unwrap(), 1); @@ -272,6 +264,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn test_mem_mut_writer() { let mut vec = Vec::new(); let mut writer = Cursor::new(&mut vec); @@ -283,6 +276,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn test_box_slice_writer() { let mut writer = Cursor::new(vec![0u8; 9].into_boxed_slice()); assert_eq!(writer.position(), 0); @@ -359,6 +353,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn test_mem_reader() { let mut reader = Cursor::new(vec![0, 1, 2, 3, 4, 5, 6, 7]); let mut buf = []; @@ -494,9 +489,12 @@ mod tests { assert_eq!(r.seek(SeekFrom::Start(10)).unwrap(), 10); assert_eq!(r.write(&[3]).unwrap(), 0); - let mut r = Cursor::new(vec![10].into_boxed_slice()); - assert_eq!(r.seek(SeekFrom::Start(10)).unwrap(), 10); - assert_eq!(r.write(&[3]).unwrap(), 0); + #[cfg(feature = "alloc")] + { + let mut r = Cursor::new(vec![10].into_boxed_slice()); + assert_eq!(r.seek(SeekFrom::Start(10)).unwrap(), 10); + assert_eq!(r.write(&[3]).unwrap(), 0); + } } #[test] diff --git a/util/src/lib.rs b/util/src/lib.rs index 231e66a0..95e3aa6d 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -9,3 +9,4 @@ 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..568838f6 --- /dev/null +++ b/util/src/testing.rs @@ -0,0 +1,98 @@ +use core::fmt; +use core::mem; +use core::slice; + +/// Test descriptor created by `decl_test!`. Describes and allows running an +/// individual test. +pub struct Test { + pub module: &'static str, + pub name: &'static str, + pub run: fn() -> bool, +} + +/// Type which may be used as a test return type. +pub trait TestResult { + /// Report any errors to `tracing`, then returns either `true` for a + /// success, or `false` for a failure. + fn report(self) -> bool; +} + +impl TestResult for () { + fn report(self) -> bool { + true + } +} + +impl TestResult for Result<(), T> { + fn report(self) -> bool { + match self { + Ok(_) => true, + Err(err) => { + tracing::error!("FAIL {:?}", err); + false + } + } + } +} + +/// Declare a new test, sort-of like the `#[test]` attribute. +// FIXME: Declare a `#[test]` custom attribute instead? +#[macro_export] +macro_rules! decl_test { + (fn $name:ident $($t:tt)*) => { + fn $name $($t)* + + // Introduce an anonymous const to avoid name conflicts. The `#[used]` + // will prevent the symbol from being dropped, and `link_section` will + // make it visible. + const _: () = { + #[used] + #[link_section = "MyceliumTests"] + static TEST: $crate::testing::Test = $crate::testing::Test { + module: module_path!(), + name: stringify!($name), + run: || $crate::testing::TestResult::report($name()), + }; + }; + } +} + +// These symbols are auto-generated by lld (and similar linkers) for data +// `link_section` sections, and are located at the beginning and end of the +// section. +// +// The memory region between the two symbols will contain an array of `Test` +// instances. +extern "C" { + static __start_MyceliumTests: (); + static __stop_MyceliumTests: (); +} + +/// Get a list of `Test` objects. +pub fn all_tests() -> &'static [Test] { + unsafe { + // FIXME: These should probably be `&raw const __start_*`. + let start: *const () = &__start_MyceliumTests; + let stop: *const () = &__stop_MyceliumTests; + + let len_bytes = (stop as usize) - (start as usize); + let len = len_bytes / mem::size_of::(); + assert!( + len_bytes % mem::size_of::() == 0, + "Section should contain a whole number of `Test`s" + ); + + if len > 0 { + slice::from_raw_parts(start as *const Test, len) + } else { + &[] + } + } +} + +decl_test! { + fn it_works() -> Result<(), ()> { + tracing::info!("I'm running in a test!"); + Ok(()) + } +}