use crate::{
Outcome, ShouldPanic, TestCase, Tests, allocator,
allocator::Allocator,
contains::contains,
log,
mmio::{
DisplayStatus, Interrupt,
bios::{HaltControl, RegisterRamReset},
},
test_case::Ignore,
ui,
};
use core::{arch::asm, fmt::Display, mem::MaybeUninit, panic::PanicInfo, ptr::addr_of};
const DISPSTAT: *mut DisplayStatus = 0x0400_0004 as *mut DisplayStatus;
const IME: *mut bool = 0x0400_0208 as *mut bool;
const IE: *mut Interrupt = 0x0400_0200 as *mut Interrupt;
#[unsafe(link_section = ".noinit")]
static mut INITIALIZED: bool = false;
#[unsafe(link_section = ".noinit")]
static mut TESTS: MaybeUninit<Tests> = MaybeUninit::uninit();
#[global_allocator]
static ALLOCATOR: Allocator = Allocator;
fn store_outcome<Data>(outcome: Outcome<Data>)
where
Data: Display,
{
if unsafe { INITIALIZED } {
unsafe { TESTS.assume_init_mut().complete_test(outcome) };
} else {
panic!("attempted to write outcome, but `TESTS` is not initialized");
}
}
#[inline]
#[instruction_set(arm::t32)]
fn reset() -> ! {
#[cfg(gba_test_mb)]
unsafe {
(0x0300_7ffa as *mut bool).write_volatile(true)
}
unsafe {
asm! {
"swi #0x01",
"swi #0x00",
in("r0") RegisterRamReset::new()
.with_palette()
.with_vram()
.with_oam()
.with_sio_registers()
.with_sound_registers()
.with_other_registers()
.to_u8(),
options(noreturn),
}
};
}
#[inline]
#[instruction_set(arm::t32)]
pub(crate) fn report_result(result: usize) {
unsafe {
asm! {
"swi #0x27",
in("r0") result,
in("r2") HaltControl::Halt as u8,
}
}
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
if unsafe { INITIALIZED }
&& let Some(test) = unsafe { TESTS.assume_init_ref().current_test() }
{
match test.should_panic() {
ShouldPanic::No => {
log::info!("test failed");
store_outcome(Outcome::Failed(info));
}
ShouldPanic::Yes => {
log::info!("test passed");
store_outcome(Outcome::<&str>::Passed);
}
ShouldPanic::YesWithMessage(message) => {
let panic_message = info.message();
if contains(info, message) {
log::info!("test passed");
store_outcome(Outcome::<&str>::Passed);
} else {
log::info!("test failed");
store_outcome(Outcome::Failed(format_args!(
"panic did not contain expected string\n panic message: `{panic_message}`\nexpected substring: `{message}`"
)))
}
}
}
reset()
}
log::error!("{info}");
ui::panic::display(info);
}
pub fn runner(tests: &'static [&'static dyn TestCase]) -> ! {
if unsafe { !INITIALIZED } {
unsafe extern "C" {
static __ewram_data_end: u8;
}
unsafe {
TESTS = MaybeUninit::new(Tests::new(
tests,
(addr_of!(__ewram_data_end) as usize) as *mut u8,
));
INITIALIZED = true;
}
}
unsafe {
allocator::init(TESTS.assume_init_ref().data_cursor().cast::<u8>());
}
if let Some(test) = unsafe { TESTS.assume_init_mut().start_test() } {
log::info!("running test: {}", test.name());
match test.ignore() {
Ignore::Yes | Ignore::YesWithMessage(_) => {
log::info!("test ignored");
store_outcome(Outcome::<&str>::Ignored);
}
Ignore::No => {
test.run();
match test.should_panic() {
ShouldPanic::No => {
log::info!("test passed");
store_outcome(Outcome::<&str>::Passed);
}
ShouldPanic::Yes | ShouldPanic::YesWithMessage(_) => {
log::info!("test failed");
store_outcome(Outcome::Failed("note: test did not panic as expected"))
}
}
}
}
reset();
}
log::info!("tests finished");
let outcomes = unsafe { TESTS.assume_init_ref() }.outcomes();
unsafe {
DISPSTAT.write_volatile(DisplayStatus::ENABLE_VBLANK_INTERRUPTS);
IE.write_volatile(Interrupt::VBLANK);
IME.write(true);
}
report_result(
outcomes
.iter()
.any(|(_, outcome)| matches!(outcome, Outcome::Failed(_))) as usize,
);
ui::run(outcomes)
}