use super::gba_suite_runner::{
ARMWRESTLER_TEST_PAGE_COUNT, MGBA_SUITE_COUNT, MGBA_SUITE_KEYS, MgbaVideoResult, Suite,
VIDEO_TEST_NAMES, boot_mgba_suite, run_armwrestler, run_mgba_bios_math_diagnostics,
run_mgba_dma_diagnostics, run_mgba_io_read_diagnostics,
run_mgba_io_read_diagnostics_after_bios_intro, run_mgba_memory_diagnostics,
run_mgba_memory_diagnostics_with_bios_path, run_mgba_misc_edge_diagnostics,
run_mgba_sio_read_diagnostics, run_mgba_sio_timing_diagnostics, run_mgba_suite,
run_mgba_timer_irq_diagnostics, run_mgba_timers_diagnostics, run_mgba_timing_diagnostics,
run_mgba_video_tests, run_suite,
};
use crate::gba::bios::EMBEDDED_BIOS;
use crate::gba::integration_tests::gba_suite_runner::GBA_CYCLES_PER_FRAME;
use crate::platform::emulator::Emulator;
use std::collections::HashMap;
use std::io::Write;
use std::sync::OnceLock;
const APPROVALS_FILE: &str = "src/gba/integration_tests/gba_suite_crc_approvals.txt";
const APPROVALS_RAW: &str = include_str!("gba_suite_crc_approvals.txt");
fn parse_hex_crc(value: &str) -> Option<u32> {
let trimmed = value.trim();
let digits = trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))?;
if digits.len() != 8 || !digits.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
u32::from_str_radix(digits, 16).ok()
}
fn load_approved_crcs() -> HashMap<String, u32> {
let mut map = HashMap::new();
for (line_no, raw_line) in APPROVALS_RAW.lines().enumerate() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (suite, crc_text) = line.split_once('=').unwrap_or_else(|| {
panic!(
"invalid entry in {APPROVALS_FILE}:{}: expected <suite>=0x<CRC32>",
line_no + 1
)
});
let suite = suite.trim();
let crc_text = crc_text.trim();
let crc = parse_hex_crc(crc_text).unwrap_or_else(|| {
panic!(
"invalid CRC in {APPROVALS_FILE}:{}: expected 8 hex digits with 0x prefix, got '{}': {}",
line_no + 1,
crc_text,
raw_line
)
});
let previous = map.insert(suite.to_string(), crc);
assert!(
previous.is_none(),
"duplicate suite entry '{}' in {}:{}",
suite,
APPROVALS_FILE,
line_no + 1
);
}
map
}
fn approved_crc_for_suite(suite: Suite) -> u32 {
approved_crc_for_suite_key(suite.label())
}
fn approved_crc_for_suite_key(key: &str) -> u32 {
let approvals = load_approved_crcs();
*approvals.get(key).unwrap_or_else(|| {
panic!(
"missing approved CRC for suite '{}' in {}. Generate captures with NESER_CAPTURE_SCREEN=1 and add {}=0x........ after visual approval.",
key, APPROVALS_FILE, key
)
})
}
fn cached_mgba_timing_diagnostics() -> &'static super::gba_suite_runner::MgbaMemoryDiagnosticResult
{
static RESULT: OnceLock<super::gba_suite_runner::MgbaMemoryDiagnosticResult> = OnceLock::new();
RESULT.get_or_init(run_mgba_timing_diagnostics)
}
fn assert_suite_passes_with_crc(suite: Suite) {
let suite_label = suite.label();
let expected_crc32 = approved_crc_for_suite(suite);
let result = run_suite(suite);
let ewram_info = result
.ewram_dump
.as_deref()
.map(|d| format!("\n{d}"))
.unwrap_or_default();
assert!(
result.passed,
"{suite_label} suite failed: index={} reg={} pc=0x{:08X} cpsr=0x{:08X} thumb={} opcode=0x{:08X} fb_crc=0x{:08X} cycles={} exit={:?}{ewram_info}",
result.failing_index,
result.reg_name.unwrap_or("n/a"),
result.pc,
result.cpsr,
result.thumb,
result.opcode_at_pc,
result.framebuffer_crc32,
result.cycles,
result.exit_reason
);
assert_eq!(
result.framebuffer_crc32, expected_crc32,
"{suite_label} suite framebuffer CRC mismatch: expected=0x{expected_crc32:08X} actual=0x{:08X}{ewram_info}",
result.framebuffer_crc32
);
}
#[test]
fn gba_suite_arm_rom_passes() {
assert_suite_passes_with_crc(Suite::Arm);
}
#[test]
fn gba_suite_thumb_rom_passes() {
assert_suite_passes_with_crc(Suite::Thumb);
}
#[test]
fn gba_suite_nes_rom_passes() {
assert_suite_passes_with_crc(Suite::Nes);
}
#[test]
fn gba_suite_memory_rom_passes() {
assert_suite_passes_with_crc(Suite::Memory);
}
#[test]
fn gba_suite_save_none_rom_passes() {
assert_suite_passes_with_crc(Suite::SaveNone);
}
#[test]
fn gba_suite_save_sram_rom_passes() {
assert_suite_passes_with_crc(Suite::SaveSram);
}
#[test]
fn gba_suite_save_flash64_rom_passes() {
assert_suite_passes_with_crc(Suite::SaveFlash64);
}
#[test]
fn gba_suite_save_flash128_rom_passes() {
assert_suite_passes_with_crc(Suite::SaveFlash128);
}
#[test]
fn gba_suite_ppu_hello_rom_passes() {
assert_suite_passes_with_crc(Suite::PpuHello);
}
#[test]
fn gba_suite_ppu_shades_rom_passes() {
assert_suite_passes_with_crc(Suite::PpuShades);
}
#[test]
fn gba_suite_ppu_stripes_rom_passes() {
assert_suite_passes_with_crc(Suite::PpuStripes);
}
#[test]
fn gba_fuzzarm_data_processing_passes() {
assert_suite_passes_with_crc(Suite::FuzzArmDataProcessing);
}
#[test]
fn gba_fuzzarm_any_passes() {
assert_suite_passes_with_crc(Suite::FuzzArmAny);
}
#[test]
fn gba_fuzzthumb_data_processing_passes() {
assert_suite_passes_with_crc(Suite::FuzzThumbDataProcessing);
}
#[test]
fn gba_fuzzthumb_any_passes() {
assert_suite_passes_with_crc(Suite::FuzzThumbAny);
}
#[test]
fn gba_fuzzarm_mixed_passes() {
assert_suite_passes_with_crc(Suite::FuzzArmMixed);
}
#[test]
fn gba_suite_armwrestler_passes() {
let approvals = load_approved_crcs();
let result = run_armwrestler();
assert_eq!(
result.page_crcs.len(),
ARMWRESTLER_TEST_PAGE_COUNT,
"expected {} test page CRCs but got {}",
ARMWRESTLER_TEST_PAGE_COUNT,
result.page_crcs.len()
);
for (i, &crc) in result.page_crcs.iter().enumerate() {
let key = format!("armwrestler_page{i}");
let expected = approvals.get(&key).unwrap_or_else(|| {
panic!(
"missing approved CRC for '{}' in {}. Run with NESER_CAPTURE_SCREEN=1 and add {}=0x{:08X}",
key, APPROVALS_FILE, key, crc
)
});
assert_eq!(
crc, *expected,
"armwrestler page {i} CRC mismatch: expected=0x{expected:08X} actual=0x{crc:08X}"
);
}
}
#[test]
fn gba_mgba_suite_passes() {
let approvals = load_approved_crcs();
let result = run_mgba_suite();
assert_eq!(
result.suite_crcs.len(),
MGBA_SUITE_COUNT,
"expected {} sub-suite CRCs but got {}",
MGBA_SUITE_COUNT,
result.suite_crcs.len()
);
let mut mismatches = Vec::new();
for (i, &crc) in result.suite_crcs.iter().enumerate() {
let key = MGBA_SUITE_KEYS[i];
match approvals.get(key) {
Some(&expected) if crc == expected => {}
Some(&expected) => mismatches.push(format!(
"{key}: expected=0x{expected:08X} actual=0x{crc:08X}"
)),
None => mismatches.push(format!("{key}: missing approval actual=0x{crc:08X}")),
}
}
assert!(
mismatches.is_empty(),
"mGBA suite approval mismatches:\n{}",
mismatches.join("\n")
);
}
#[test]
fn gba_mgba_memory_diagnostics_reports_mgba_log() {
let result = run_mgba_memory_diagnostics();
assert_eq!(
result.total_count,
Some(1552),
"raw mGBA Memory log: {:?}",
result.raw_log
);
assert!(
result.passed_count.is_some(),
"mGBA Memory diagnostics should include a parsed pass count"
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA Memory diagnostics should pass every test with the embedded BIOS.\nraw log:\n{}",
result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA Memory diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
assert!(
result.raw_log.contains("Memory"),
"mGBA Memory diagnostics should include the mGBA log, got: {:?}",
result.raw_log
);
assert_eq!(
result.framebuffer_crc32,
approved_crc_for_suite_key("mgba_memory")
);
}
#[test]
fn gba_mgba_io_read_diagnostics_passes_every_register_read() {
let result = run_mgba_io_read_diagnostics();
assert_eq!(
result.total_count,
Some(130),
"raw mGBA I/O read log: {:?}",
result.raw_log
);
assert!(
result.passed_count.is_some(),
"mGBA I/O read diagnostics should include a parsed pass count"
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA I/O read diagnostics should pass every register read with the embedded BIOS.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA I/O read diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_io_read_diagnostics_passes_after_bios_intro() {
let result = run_mgba_io_read_diagnostics_after_bios_intro();
assert_eq!(
result.total_count,
Some(130),
"raw mGBA I/O read log after BIOS intro: {:?}",
result.raw_log
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA I/O read diagnostics should pass after the normal BIOS intro.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA I/O read diagnostics reported failures after the normal BIOS intro: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_timing_diagnostics_passes_every_timing_case() {
let result = cached_mgba_timing_diagnostics();
assert_eq!(
result.total_count,
Some(2020),
"raw mGBA Timing log:\n{}",
result.raw_log
);
assert!(
result.passed_count.is_some(),
"mGBA Timing diagnostics should include a parsed pass count"
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA Timing diagnostics should pass every timing case with the embedded BIOS.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA Timing diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_timing_diagnostics_passes_c_loop_prefetch_cases() {
let result = cached_mgba_timing_diagnostics();
let c_loop_start = result
.raw_log
.find("Timing test: C loop")
.expect("mGBA Timing log should include the C-loop test section");
let c_loop_suffix = &result.raw_log[c_loop_start..];
let c_loop_end = c_loop_suffix
.find("Timing test: BIOS Division")
.unwrap_or(c_loop_suffix.len());
let c_loop_log = &c_loop_suffix[..c_loop_end];
assert!(
!c_loop_log.contains("FAIL:"),
"mGBA Timing C-loop prefetch cases should pass.\nC-loop log:\n{}",
c_loop_log
);
}
#[test]
fn gba_mgba_timing_diagnostics_passes_trivial_internal_dma_start_cases() {
let result = cached_mgba_timing_diagnostics();
let failing_cases = [
"FAIL: Trivial DMA (16) ARM/IWRAM",
"FAIL: Trivial DMA (16) Thumb/IWRAM",
"FAIL: Trivial DMA (32) ARM/IWRAM",
"FAIL: Trivial DMA (32) Thumb/IWRAM",
];
for failing_case in failing_cases {
assert!(
!result.raw_log.contains(failing_case),
"mGBA Timing internal DMA start case should pass: {failing_case}\nraw log:\n{}",
result.raw_log
);
}
}
#[test]
fn gba_mgba_timing_diagnostics_passes_rom_dma_prefetch_cases() {
let result = cached_mgba_timing_diagnostics();
let failing_prefixes = [
"FAIL: Trivial DMA (16/ROM)",
"FAIL: Trivial DMA (16/to ROM)",
"FAIL: Trivial DMA (16/ROM to ROM)",
"FAIL: Trivial DMA (32/from ROM)",
"FAIL: Trivial DMA (32/to ROM)",
"FAIL: Trivial DMA (32/ROM to ROM)",
"FAIL: Short DMA (16/from ROM)",
"FAIL: Short DMA (16/to ROM)",
"FAIL: Short DMA (16/ROM to ROM)",
"FAIL: Short DMA (32/from ROM)",
"FAIL: Short DMA (32/to ROM)",
"FAIL: Short DMA (32/ROM to ROM)",
];
for failing_prefix in failing_prefixes {
assert!(
!result.raw_log.contains(failing_prefix),
"mGBA Timing ROM-DMA prefetch case should pass: {failing_prefix}\nraw log:\n{}",
result.raw_log
);
}
}
#[test]
fn gba_mgba_timing_diagnostics_passes_bios_division_cases() {
let result = cached_mgba_timing_diagnostics();
let failing_prefixes = ["FAIL: BIOS Division ", "FAIL: BIOS Division 2 "];
for failing_prefix in failing_prefixes {
assert!(
!result.raw_log.contains(failing_prefix),
"mGBA Timing BIOS division case should pass: {failing_prefix}\nraw log:\n{}",
result.raw_log
);
}
}
#[test]
fn gba_mgba_timing_diagnostics_passes_bios_sqrt_cases() {
let result = cached_mgba_timing_diagnostics();
let failing_prefixes = [
"FAIL: BIOS Sqrt ",
"FAIL: BIOS Sqrt 2 ",
"FAIL: BIOS Sqrt 3 ",
];
for failing_prefix in failing_prefixes {
assert!(
!result.raw_log.contains(failing_prefix),
"mGBA Timing BIOS sqrt case should pass: {failing_prefix}\nraw log:\n{}",
result.raw_log
);
}
}
#[test]
fn gba_mgba_timing_diagnostics_passes_bios_arctan_cases() {
let result = cached_mgba_timing_diagnostics();
assert!(
!result.raw_log.contains("FAIL: BIOS ArcTan "),
"mGBA Timing BIOS ArcTan case should pass.\nraw log:\n{}",
result.raw_log
);
}
#[test]
fn gba_mgba_timing_diagnostics_passes_bios_cpuset_cases() {
let result = cached_mgba_timing_diagnostics();
assert!(
!result.raw_log.contains("FAIL: CpuSet "),
"mGBA Timing BIOS CpuSet/CpuFastSet case should pass.\nraw log:\n{}",
result.raw_log
);
}
#[test]
fn gba_mgba_timers_diagnostics_passes_every_count_up_case() {
let result = run_mgba_timers_diagnostics();
assert_eq!(
result.total_count,
Some(936),
"raw mGBA Timers log:\n{}",
result.raw_log
);
assert!(
result.passed_count.is_some(),
"mGBA Timers diagnostics should include a parsed pass count"
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA Timer count-up diagnostics should pass every case with the embedded BIOS.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA Timer count-up diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_timer_irq_diagnostics_passes_every_irq_case() {
let result = run_mgba_timer_irq_diagnostics();
assert_eq!(
result.total_count,
Some(90),
"raw mGBA Timer IRQ log:\n{}",
result.raw_log
);
assert!(
result.passed_count.is_some(),
"mGBA Timer IRQ diagnostics should include a parsed pass count"
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA Timer IRQ diagnostics should pass every case with the embedded BIOS.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA Timer IRQ diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_dma_diagnostics_passes_every_case() {
let result = run_mgba_dma_diagnostics();
assert_eq!(
result.total_count,
Some(1256),
"raw mGBA DMA log:\n{}",
result.raw_log
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA DMA diagnostics should pass every case with the embedded BIOS.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA DMA diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_sio_timing_diagnostics_passes_every_counted_case() {
let result = run_mgba_sio_timing_diagnostics();
assert_eq!(
result.total_count,
Some(4),
"raw mGBA SIO timing log:\n{}",
result.raw_log
);
assert!(
result.passed_count.is_some(),
"mGBA SIO timing diagnostics should include a parsed pass count"
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA SIO timing diagnostics should pass every counted case with the embedded BIOS.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA SIO timing diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_sio_read_diagnostics_still_passes_every_case() {
let result = run_mgba_sio_read_diagnostics();
assert_eq!(
result.total_count,
Some(90),
"raw mGBA SIO read log:\n{}",
result.raw_log
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA SIO read diagnostics should still pass every case with the embedded BIOS.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA SIO read diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_misc_edge_diagnostics_passes_every_case() {
let result = run_mgba_misc_edge_diagnostics();
assert_eq!(
result.total_count,
Some(12),
"raw mGBA Misc edge log:\n{}",
result.raw_log
);
assert!(
result.passed_count.is_some(),
"mGBA Misc edge diagnostics should include a parsed pass count"
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA Misc edge diagnostics should pass every case with the embedded BIOS.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA Misc edge diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_bios_math_diagnostics_passes_every_case() {
let result = run_mgba_bios_math_diagnostics();
assert_eq!(
result.total_count,
Some(615),
"raw mGBA BIOS math log:\n{}",
result.raw_log
);
assert!(
result.passed_count.is_some(),
"mGBA BIOS math diagnostics should include a parsed pass count"
);
assert_eq!(
result.passed_count, result.total_count,
"mGBA BIOS math diagnostics should pass every case with the embedded BIOS.\nfailures: {:?}\nraw log:\n{}",
result.failures, result.raw_log
);
assert!(
result.failures.is_empty(),
"mGBA BIOS math diagnostics reported failures with the embedded BIOS: {:?}\nraw log:\n{}",
result.failures,
result.raw_log
);
}
#[test]
fn gba_mgba_memory_proprietary_diagnostics_skip_without_bios_path() {
let result = run_mgba_memory_diagnostics_with_bios_path(None).unwrap();
assert!(result.is_none());
}
#[test]
fn gba_mgba_memory_proprietary_diagnostics_run_with_bios_path() {
let mut bios_file = tempfile::NamedTempFile::new().unwrap();
bios_file.write_all(EMBEDDED_BIOS).unwrap();
let result = run_mgba_memory_diagnostics_with_bios_path(Some(bios_file.path())).unwrap();
let result = result.expect("configured BIOS path should run diagnostics");
assert_eq!(
result.framebuffer_crc32,
approved_crc_for_suite_key("mgba_memory")
);
assert_eq!(
result.total_count,
Some(1552),
"raw mGBA Memory log: {:?}",
result.raw_log
);
}
#[test]
fn approvals_manifest_parses() {
let approvals = load_approved_crcs();
assert_eq!(approvals.get("arm"), Some(&0x12FD_AE0B));
assert_eq!(approvals.get("thumb"), Some(&0x12FD_AE0B));
assert_eq!(approvals.get("nes"), Some(&0x12FD_AE0B));
assert_eq!(approvals.get("memory"), Some(&0x12FD_AE0B));
assert_eq!(approvals.get("save_none"), Some(&0x12FD_AE0B));
assert_eq!(approvals.get("save_sram"), Some(&0x12FD_AE0B));
assert_eq!(approvals.get("save_flash64"), Some(&0x12FD_AE0B));
assert_eq!(approvals.get("save_flash128"), Some(&0x12FD_AE0B));
assert_eq!(approvals.get("ppu_hello"), Some(&0x52F9_B8A4));
assert_eq!(approvals.get("ppu_shades"), Some(&0x57EB_C4ED));
assert_eq!(approvals.get("ppu_stripes"), Some(&0xFBAB_D04A));
assert_eq!(approvals.get("fuzzarm_data_processing"), Some(&0x9A78_A7EB));
assert_eq!(approvals.get("fuzzarm_any"), Some(&0x9A78_A7EB));
assert_eq!(
approvals.get("fuzzthumb_data_processing"),
Some(&0x9A78_A7EB)
);
assert_eq!(approvals.get("fuzzthumb_any"), Some(&0x9A78_A7EB));
assert_eq!(approvals.get("fuzzarm_mixed"), Some(&0x9A78_A7EB));
assert_eq!(approvals.get("armwrestler_page0"), Some(&0xAA08_1A57));
assert_eq!(approvals.get("armwrestler_page1"), Some(&0x7725_D68D));
assert_eq!(approvals.get("armwrestler_page2"), Some(&0xF9C3_8336));
assert_eq!(approvals.get("armwrestler_page3"), Some(&0x76CE_D72B));
assert_eq!(approvals.get("armwrestler_page4"), Some(&0x6795_E0F8));
assert_eq!(approvals.get("armwrestler_page5"), Some(&0xE215_C2B0));
assert_eq!(approvals.get("armwrestler_page6"), Some(&0xA522_34A7));
assert_eq!(approvals.get("armwrestler_page7"), Some(&0x562A_5C65));
assert_eq!(approvals.get("mgba_memory"), Some(&0x61F6_5500));
assert_eq!(approvals.get("mgba_io_read"), Some(&0x7D32_0909));
assert_eq!(approvals.get("mgba_timing"), Some(&0xEABA_32BC));
assert_eq!(approvals.get("mgba_timers"), Some(&0xBAEF_CA7B));
assert_eq!(approvals.get("mgba_timer_irq"), Some(&0xE779_6772));
assert_eq!(approvals.get("mgba_shifter"), Some(&0x8B4A_12AA));
assert_eq!(approvals.get("mgba_carry"), Some(&0xFD9E_45E6));
assert_eq!(approvals.get("mgba_multiply_long"), Some(&0x6996_55AB));
assert_eq!(approvals.get("mgba_bios_math"), Some(&0x3C1B_28DE));
assert_eq!(approvals.get("mgba_dma"), Some(&0x06A6_24A4));
assert_eq!(approvals.get("mgba_sio_read"), Some(&0xF5D9_8687));
assert_eq!(approvals.get("mgba_sio_timing"), Some(&0x81BB_7F79));
assert_eq!(approvals.get("mgba_misc_edge"), Some(&0x9D9A_0C18));
assert_eq!(approvals.get("mgba_video"), Some(&0xAB7E_C249));
}
#[test]
fn mgba_suite_boots_to_menu() {
let (mut gba, _rom) = boot_mgba_suite();
let max_frames = 60;
let mut cycles: u64 = 0;
for _ in 0..10 {
advance_one_frame(&mut gba, &mut cycles);
}
let menu_crc = gba.screen_crc32();
gba.set_joypad_button_states(1, 0x20);
let mut crc_changed = false;
for _ in 0..max_frames {
advance_one_frame(&mut gba, &mut cycles);
if gba.screen_crc32() != menu_crc {
crc_changed = true;
break;
}
}
gba.set_joypad_button_states(1, 0x00);
assert!(
crc_changed,
"mgba suite menu did not respond to DOWN button after {max_frames} frames ({cycles} cycles). \
The BIOS IRQ dispatch or VBlankIntrWait may not be working correctly, \
so the ROM cannot process input. CPU PC=0x{:08X}",
gba.cpu_pc()
);
}
fn advance_one_frame(gba: &mut crate::gba::Gba, cycles: &mut u64) {
let budget = GBA_CYCLES_PER_FRAME * 2;
let mut spent: u64 = 0;
while spent < budget {
let tick = gba.run_tick_for_tests() as u64;
assert!(tick > 0, "CPU halted at PC=0x{:08X}", gba.cpu_pc());
*cycles += tick;
spent += tick;
if gba.is_ready_to_render() {
gba.clear_ready_to_render();
return;
}
}
panic!("no frame produced within cycle budget");
}
fn cached_mgba_video_result() -> &'static MgbaVideoResult {
static RESULT: OnceLock<MgbaVideoResult> = OnceLock::new();
RESULT.get_or_init(run_mgba_video_tests)
}
fn assert_mgba_video_test_matches_expected(test_idx: usize) {
let result = cached_mgba_video_result();
assert_eq!(
result.tests.len(),
7,
"expected 7 video test results, got {}",
result.tests.len()
);
let test = &result.tests[test_idx];
assert!(
test.matches,
"mGBA video '{}' (test {}) should match expected output: actual=0x{:08X} expected=0x{:08X}; total matching={}/7; cycles={}",
VIDEO_TEST_NAMES[test_idx],
test_idx + 1,
test.actual_crc,
test.expected_crc,
result.tests.iter().filter(|test| test.matches).count(),
result.cycles,
);
}
#[test]
fn gba_mgba_video_basic_mode_3_matches_expected() {
assert_mgba_video_test_matches_expected(0);
}
#[test]
fn gba_mgba_video_basic_mode_4_matches_expected() {
assert_mgba_video_test_matches_expected(1);
}
#[test]
fn gba_mgba_video_degenerate_obj_transforms_matches_expected() {
assert_mgba_video_test_matches_expected(2);
}
#[test]
#[ignore = "tracked by #2687"]
fn gba_mgba_video_layer_toggle_matches_expected() {
assert_mgba_video_test_matches_expected(3);
}
#[test]
#[ignore = "tracked by #2688"]
fn gba_mgba_video_layer_toggle_2_matches_expected() {
assert_mgba_video_test_matches_expected(4);
}
#[test]
#[ignore = "tracked by #2689"]
fn gba_mgba_video_oam_update_delay_matches_expected() {
assert_mgba_video_test_matches_expected(5);
}
#[test]
#[ignore = "tracked by #2690"]
fn gba_mgba_video_window_offscreen_reset_matches_expected() {
assert_mgba_video_test_matches_expected(6);
}