use std::{ collections::VecDeque, error::Error, fs::File, io::Read, path::{Path, PathBuf}, sync::Mutex, }; use anyhow::{anyhow, Context}; use clap::Parser; use image_compare::compare_image; use mgba::{LogLevel, Logger, MCore, MemoryBacked, VFile}; mod image_compare; static LOGGER: Logger = Logger::new(my_logger); static LOGGER_BUFFER: Mutex> = Mutex::new(VecDeque::new()); fn my_logger(category: &str, level: LogLevel, s: String) { LOGGER_BUFFER .lock() .unwrap() .push_back((category.to_string(), level, s)); } #[derive(Parser)] struct CliArguments { rom: PathBuf, } struct TestRunner { mgba: MCore, } enum Timer { Start(u64), Total(u64), } impl TestRunner { fn new(rom: V) -> Result> { let mut mgba = MCore::new().ok_or(anyhow!("cannot create core"))?; mgba::set_global_default_logger(&LOGGER); mgba.load_rom(rom); Ok(Self { mgba }) } fn run(mut self) -> Result<(), Box> { let mut timer: Timer = Timer::Total(0); let mut mark_tests_as_soft_failed = false; let mut mark_this_test_as_soft_failed = false; loop { self.mgba.step(); while let Some((category, level, message)) = LOGGER_BUFFER.lock().unwrap().pop_front() { match (category.as_ref(), level, message.as_ref()) { (_, LogLevel::Fatal, fatal_message) => { return Err(anyhow!("Failed with fatal message: {}", fatal_message).into()); } ("GBA I/O", _, "Stub I/O register write: FFF800") => match timer { Timer::Start(time) => { let total_cycles = self.mgba.current_cycle() - time; timer = Timer::Total(total_cycles); } Timer::Total(_) => { timer = Timer::Start(self.mgba.current_cycle()); } }, ("GBA Debug", _, debug_message) => { if let Some(image_path) = debug_message.strip_prefix("image:") { match compare_image(image_path, self.mgba.video_buffer()).with_context( || anyhow!("Could not open image {} for comparison", image_path), ) { Ok(compare) => { if !compare.success() { eprintln!("Image and video buffer do not match"); mark_tests_as_soft_failed = true; mark_this_test_as_soft_failed = true; } } Err(e) => eprintln!("{}", e), } } else if debug_message.ends_with("...") { eprint!("{}", debug_message); } else if debug_message == "[ok]" { let cycles = match timer { Timer::Start(_) => panic!("test completed with invalid timing"), Timer::Total(c) => c, }; if mark_this_test_as_soft_failed { mark_this_test_as_soft_failed = false; eprintln!( "[fail: {} c ≈ {} s]", cycles, ((cycles as f64 / (16.78 * 1_000_000.0)) * 100.0).round() / 100.0 ); } else { eprintln!( "[ok: {} c ≈ {} s]", cycles, ((cycles as f64 / (16.78 * 1_000_000.0)) * 100.0).round() / 100.0 ); } } else { eprintln!("{}", debug_message); } } _ => {} } if message == "Tests finished successfully" { if mark_tests_as_soft_failed { eprintln!("Tests failed"); return Err(anyhow!("Tests failed").into()); } else { eprintln!("{}", message); return Ok(()); } } } } } } fn main() -> Result<(), Box> { let args = CliArguments::parse(); let rom = load_rom(args.rom)?; let rom = MemoryBacked::new(rom); TestRunner::new(rom)?.run()?; Ok(()) } fn load_rom>(path: P) -> anyhow::Result> { let mut input_file = File::open(path)?; let mut input_file_buffer = Vec::new(); input_file.read_to_end(&mut input_file_buffer)?; let mut elf_buffer = Vec::new(); let inculde_debug_info = false; if agb_gbafix::write_gba_file( &input_file_buffer, Default::default(), agb_gbafix::PaddingBehaviour::DoNotPad, inculde_debug_info, &mut elf_buffer, ) .is_ok() { Ok(elf_buffer) } else { Ok(input_file_buffer) } }