From cc2eabd500e6769136ee3601acb036a02c722570 Mon Sep 17 00:00:00 2001 From: lif <> Date: Tue, 29 Apr 2025 02:12:56 -0700 Subject: [PATCH] unit test support --- src/main.rs | 20 +++++++-- src/test_harness.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 src/test_harness.rs diff --git a/src/main.rs b/src/main.rs index 7837450..9cdeb5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,17 @@ -// from https://github.com/rust-console/gba/blob/main/examples/hello.rs #![no_std] #![no_main] +#![cfg_attr(test, feature(custom_test_frameworks))] +#![cfg_attr(test, test_runner(test_harness::test_runner))] +#![cfg_attr(test, reexport_test_harness_main = "test_main")] + +#[cfg(test)] +mod test_harness; use core::panic::PanicInfo; use gba::prelude::*; #[allow(unused_must_use)] -#[unsafe(no_mangle)] +#[cfg_attr(not(test), unsafe(no_mangle))] #[instruction_set(arm::t32)] extern "C" fn main() -> ! { BG_PALETTE.index(0).write(Color(0x7F2B)); // blue @@ -33,7 +38,8 @@ extern "C" fn main() -> ! { unreachable!(); } -#[panic_handler] +#[cfg_attr(not(test), panic_handler)] +#[cfg_attr(test, allow(unused))] fn panic_handler(panic_info: &PanicInfo) -> ! { #[cfg(debug_assertions)] if let Ok(mut log) = MgbaBufferedLogger::try_new(MgbaMessageLevel::Fatal) { @@ -48,3 +54,11 @@ fn panic_handler(panic_info: &PanicInfo) -> ! { VBlankIntrWait(); } } + +#[cfg(test)] +mod tests { + #[test_case] + fn it_works() { + assert_eq!(1 + 1, 2); + } +} diff --git a/src/test_harness.rs b/src/test_harness.rs new file mode 100644 index 0000000..7e22928 --- /dev/null +++ b/src/test_harness.rs @@ -0,0 +1,99 @@ +use core::fmt::Write; +use gba::{bios, mem, mgba, prelude::*}; + +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + DISPSTAT.write(DisplayStatus::new().with_irq_vblank(true)); + BG_PALETTE.index(0).write(Color::from_rgb(25, 10, 5)); + IE.write(IrqBits::VBLANK); + IME.write(true); + VBlankIntrWait(); + VBlankIntrWait(); + VBlankIntrWait(); + + // the Fatal one kills emulation after one line / 256 bytes + // so emit all the information as Error first + if let Ok(mut log) = mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Error) { + writeln!(log, "[failed]").ok(); + write!(log, "{}", info).ok(); + } + + if let Ok(mut log) = mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Fatal) { + if let Some(loc) = info.location() { + write!(log, "panic at {loc}! see mgba error log for details.").ok(); + } else { + write!(log, "panic! see mgba error log for details.").ok(); + } + } + + IE.write(IrqBits::new()); + bios::IntrWait(true, IrqBits::new()); + loop {} +} + +pub(crate) trait UnitTest { + fn run(&self); +} + +impl UnitTest for T { + fn run(&self) { + if let Ok(mut log) = mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) { + write!(log, "{}...", core::any::type_name::()).ok(); + } + + self(); + + if let Ok(mut log) = mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) { + writeln!(log, "[ok]").ok(); + } + } +} + +pub(crate) fn test_runner(tests: &[&dyn UnitTest]) { + if let Ok(mut log) = mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) { + write!(log, "Running {} tests", tests.len()).ok(); + } + + for test in tests { + test.run(); + } + if let Ok(mut log) = mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) { + write!(log, "Tests finished successfully").ok(); + } +} + +#[unsafe(no_mangle)] +extern "C" fn main() { + DISPCNT.write(DisplayControl::new().with_video_mode(VideoMode::_0)); + BG_PALETTE.index(0).write(Color::new()); + + crate::test_main(); + + BG_PALETTE.index(0).write(Color::from_rgb(5, 15, 25)); + BG_PALETTE.index(1).write(Color::new()); + BG0CNT.write( + BackgroundControl::new() + .with_charblock(0) + .with_screenblock(31), + ); + DISPCNT.write( + DisplayControl::new() + .with_video_mode(VideoMode::_0) + .with_show_bg0(true), + ); + + // some niceties for people without mgba-test-runner + let tsb = TEXT_SCREENBLOCKS.get_frame(31).unwrap(); + unsafe { + mem::set_u32x80_unchecked(tsb.into_block::<1024>().as_mut_ptr().cast(), 0, 12); + } + Cga8x8Thick.bitunpack_4bpp(CHARBLOCK0_4BPP.as_region(), 0); + + let row = tsb.get_row(9).unwrap().iter().skip(6); + for (addr, ch) in row.zip(b"all tests passed!") { + addr.write(TextEntry::new().with_tile(*ch as u16)); + } + + DISPSTAT.write(DisplayStatus::new()); + bios::IntrWait(true, IrqBits::new()); +}