use anyhow::{bail, Result};
use meru_interface::EmulatorCore;
use std::sync::{Arc, Mutex};
use tgbr::{
config::{BootRom, Config, Model},
gameboy::GameBoy,
interface::LinkCable,
};
type Ref<T> = Arc<Mutex<T>>;
trait CheckFn: Fn(&[u8]) -> Option<Result<()>> {}
impl<T> CheckFn for T where T: Fn(&[u8]) -> Option<Result<()>> {}
struct TestLinkCable<F: CheckFn> {
check_fn: F,
buf: Ref<Vec<u8>>,
completed: Ref<Option<Result<()>>>,
}
impl<F: CheckFn> TestLinkCable<F> {
fn new(check_fn: F, buf: &Ref<Vec<u8>>, completed: &Ref<Option<Result<()>>>) -> Self {
Self {
check_fn,
buf: Ref::clone(buf),
completed: Ref::clone(completed),
}
}
}
impl<F: CheckFn> LinkCable for TestLinkCable<F> {
fn send(&mut self, data: u8) {
self.buf.lock().unwrap().push(data);
*self.completed.lock().unwrap() = (self.check_fn)(self.buf.lock().unwrap().as_slice());
}
fn try_recv(&mut self) -> Option<u8> {
None
}
}
fn test_serial_output_test_rom(
rom_bytes: &[u8],
check_fn: impl CheckFn + Send + Sync + 'static,
) -> Result<()> {
let config = Config {
model: Model::Dmg,
boot_rom: BootRom::Internal,
..Default::default()
};
let mut gb = GameBoy::try_from_file(rom_bytes, None, &config)?;
let buf = Ref::default();
let completed = Ref::default();
gb.set_link_cable(Some(TestLinkCable::new(check_fn, &buf, &completed)));
let mut frames = 0;
while completed.lock().unwrap().is_none() && frames < 1200 {
gb.exec_frame(false);
frames += 1;
}
let completed = completed.lock().unwrap();
match completed.as_ref() {
None => bail!(
"Test timed out: output = {}",
String::from_utf8_lossy(buf.lock().unwrap().as_slice())
),
Some(Ok(())) => Ok(()),
Some(Err(e)) => bail!("Test failed: {}", e),
}
}
macro_rules! gen_tester {
(@process $exp:path, $cur_dir:expr => $test_name:ident, $($rest:tt)*) => {
gen_tester!(@process $exp, $cur_dir => $test_name => stringify!($test_name), $($rest)*);
};
(@process $exp:path, $cur_dir:expr => $test_name:ident => $rom_name:expr, $($rest:tt)*) => {
#[test]
#[allow(non_snake_case)]
fn $test_name() -> anyhow::Result<()> {
const ROM_BYTES: &[u8] = include_bytes!(concat!($cur_dir, "/", $rom_name, ".gb"));
test_serial_output_test_rom(ROM_BYTES, $exp)
}
gen_tester!(@process $exp, $cur_dir => $($rest)*);
};
(@process $exp:path, $cur_dir:expr => $dir:ident:: {$($con:tt)*}, $($rest:tt)*) => {
mod $dir {
use super::*;
gen_tester!(@process $exp, concat!($cur_dir, "/", stringify!($dir)) => $($con)*);
}
gen_tester!(@process $exp, $cur_dir => $($rest)*);
};
(@process $exp:path, $cur_dir:expr => $(,)?) => {};
($tag:ident, $path:literal, $exp:path: {$($con:tt)*}, $($rest:tt)*) => {
mod $tag {
#[allow(unused)]
use super::*;
gen_tester!(@process $exp, $path => $($con)*);
}
gen_tester!($($rest)*);
};
() => {};
}
fn blargg_check_fn(output: &[u8]) -> Option<Result<()>> {
const EXPECTED: &[u8] = b"Passed\n";
const FAILED: &[u8] = b"FAILED\n";
if output.len() >= EXPECTED.len() && &output[output.len() - EXPECTED.len()..] == EXPECTED {
return Some(Ok(()));
}
if output.len() >= FAILED.len() && &output[output.len() - FAILED.len()..] == FAILED {
return Some(Err(anyhow::anyhow!(
"Test failed: {}",
String::from_utf8_lossy(output)
)));
}
None
}
fn mooneye_check_fn(output: &[u8]) -> Option<Result<()>> {
const EXPECTED: &[u8] = &[3, 5, 8, 13, 21, 34];
if output.len() == EXPECTED.len() {
Some(if output == EXPECTED {
Ok(())
} else {
Err(anyhow::anyhow!(
"Output did not match expected: {:?}",
output
))
})
} else {
None
}
}
gen_tester! {
blargg, "blargg", blargg_check_fn: {
cpu_instrs::{
individual::{
_01_special => "01-special",
_02_interrupts => "02-interrupts",
_03_op_sp_hl => "03-op sp,hl",
_04_op_r_imm => "04-op r,imm",
_05_op_rp => "05-op rp",
_06_ld_r_r => "06-ld r,r",
_07_jr_jp_call_ret_rst => "07-jr,jp,call,ret,rst",
_08_misc_instrs => "08-misc instrs",
_09_op_r_r => "09-op r,r",
_10_bit_ops => "10-bit ops",
_11_op_a_hl => "11-op a,(hl)",
},
},
mem_timing::{
individual::{
_01_read_timing => "01-read_timing",
_02_write_timing => "02-write_timing",
_03_modify_timing => "03-modify_timing",
},
},
},
mooneye, "mooneye-test-suite/acceptance", mooneye_check_fn: {
add_sp_e_timing,
boot_div_dmgABCmgb => "boot_div-dmgABCmgb",
boot_hwio_dmgABCmgb => "boot_hwio-dmgABCmgb",
boot_regs_dmgABC => "boot_regs-dmgABC",
call_cc_timing,
call_cc_timing2,
call_timing,
call_timing2,
di_timing_GS => "di_timing-GS",
div_timing,
ei_sequence,
ei_timing,
halt_ime0_ei,
halt_ime0_nointr_timing,
halt_ime1_timing,
halt_ime1_timing2_GS => "halt_ime1_timing2-GS",
if_ie_registers,
intr_timing,
jp_cc_timing,
jp_timing,
ld_hl_sp_e_timing,
oam_dma_restart,
oam_dma_start,
oam_dma_timing,
pop_timing,
push_timing,
rapid_di_ei,
ret_cc_timing,
ret_timing,
reti_intr_timing,
reti_timing,
rst_timing,
bits::{
mem_oam,
reg_f,
unused_hwio_GS => "unused_hwio-GS",
},
instr::{
daa,
},
interrupts::{
ie_push,
},
oam_dma::{
basic,
reg_read,
sources_GS => "sources-GS",
},
ppu::{
hblank_ly_scx_timing_GS => "hblank_ly_scx_timing-GS",
intr_1_2_timing_GS => "intr_1_2_timing-GS",
intr_2_0_timing,
intr_2_mode0_timing,
intr_2_mode0_timing_sprites,
intr_2_mode3_timing,
intr_2_oam_ok_timing,
lcdon_timing_GS => "lcdon_timing-GS",
lcdon_write_timing_GS => "lcdon_write_timing-GS",
stat_irq_blocking,
stat_lyc_onoff,
vblank_stat_intr_GS => "vblank_stat_intr-GS",
},
serial::{
boot_sclk_align_dmgABCmgb => "boot_sclk_align-dmgABCmgb",
},
timer::{
div_write,
rapid_toggle,
tim00,
tim00_div_trigger,
tim01,
tim01_div_trigger,
tim10,
tim10_div_trigger,
tim11,
tim11_div_trigger,
tima_reload,
tima_write_reloading,
tma_write_reloading,
},
},
}