cge_nes 0.1.2

Cycle-accurate NES (Nintendo Entertainment System) emulator library: CPU, PPU, cartridge, input, and iNES ROM loading.
Documentation
//! Integration tests for `rom_loader`.

use super::*;
use std::io::Read;

/// Build a valid iNES blob with the requested header fields and zero-filled
/// PRG / CHR data. Used as the canonical input for [`load_rom`] tests.
///
/// * `prg_banks` — number of 16 KB PRG-ROM banks (header[4]).
/// * `chr_banks` — number of 8 KB CHR-ROM banks (header[5]; pass 0 for CHR RAM).
/// * `mapper` — mapper number; occupies bits 4–7 of flags6 plus bits 0–3 of
///   flags7 (the standard iNES layout).
/// * `flags6` / `flags7` — the raw header flag bytes (mirroring, battery,
///   four-screen, trainer, VS Unisystem, PlayChoice, NES 2.0 marker, etc.).
fn make_ines(prg_banks: u8, chr_banks: u8, mapper: u8, flags6: u8, flags7: u8) -> Vec<u8> {
    let mut header = [0u8; 16];
    header[0..4].copy_from_slice(b"NES\x1A");
    header[4] = prg_banks;
    header[5] = chr_banks;
    header[6] = (flags6 & 0x0F) | ((mapper & 0x0F) << 4);
    header[7] = (flags7 & 0x0F) | ((mapper & 0xF0) >> 4);
    let mut blob = Vec::from(header);
    blob.extend(std::iter::repeat(0xEA).take(prg_banks as usize * 16 * 1024));
    blob.extend(std::iter::repeat(0x00).take(chr_banks as usize * 8 * 1024));
    blob
}

/// Wrap a `Vec<u8>` so it can be passed to [`load_rom`] via `&mut impl Read`.
struct SliceReader<'a>(&'a [u8]);

impl<'a> Read for SliceReader<'a> {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        let n = buf.len().min(self.0.len());
        buf[..n].copy_from_slice(&self.0[..n]);
        self.0 = &self.0[n..];
        Ok(n)
    }
}

#[test]
fn load_nrom_256_horizontal_chr_rom() {
    // Mapper 0 (NROM-256), horizontal mirroring, 32 KB PRG + 8 KB CHR ROM.
    let blob = make_ines(2, 1, 0, 0b0000_0000, 0);
    let cart = load_rom(&mut SliceReader(&blob)).expect("NROM-256 should load");
    assert!(cart.irq_pin() == false);
}

#[test]
fn load_nrom_128_chr_ram() {
    // Mapper 0, CHR RAM (header[5] = 0).
    let blob = make_ines(1, 0, 0, 0b0000_0000, 0);
    load_rom(&mut SliceReader(&blob)).expect("NROM-128 + CHR RAM should load");
}

#[test]
fn load_uxrom_chr_ram_works() {
    // Mapper 2 (UxROM), 128 KB PRG, CHR RAM, vertical mirroring.
    let blob = make_ines(8, 0, 2, 0b0000_0001, 0);
    load_rom(&mut SliceReader(&blob)).expect("UxROM should load");
}

#[test]
fn load_cnrom_chr_rom_works() {
    // Mapper 3 (CNROM), 32 KB PRG, 4 × 8 KB CHR ROM, vertical mirroring.
    let blob = make_ines(2, 4, 3, 0b0000_0001, 0);
    load_rom(&mut SliceReader(&blob)).expect("CNROM should load");
}

#[test]
fn load_mmc3_256kb_chr_works() {
    // Mapper 4 (MMC3), 16 × 16 KB PRG (256 KB), 32 × 8 KB CHR ROM (256 KB).
    // Regression test for the CHR-ROM cap fix: must accept the full 256 KB
    // ceiling, not the older 255 KB cap.
    let blob = make_ines(16, 32, 4, 0b0000_0000, 0);
    load_rom(&mut SliceReader(&blob)).expect("MMC3 with 256 KB CHR ROM should load");
}

#[test]
fn load_unsupported_mapper_returns_error() {
    // Mapper 7 is not implemented; expect UnsupportedMapper(7), not a panic.
    let blob = make_ines(2, 1, 7, 0, 0);
    let result = load_rom(&mut SliceReader(&blob));
    let err = match result {
        Ok(_) => panic!("expected error for mapper 7"),
        Err(e) => e,
    };
    match err {
        RomError::UnsupportedMapper(n) => assert_eq!(n, 7),
        other => panic!("expected UnsupportedMapper(7), got {other:?}"),
    }
}

#[test]
fn unsupported_mapper_display_zero_pads_to_three_digits() {
    // Display formatting follows the NESDev mapper-numbering convention:
    // mapper ids are always printed as three digits (004, 099, 256).
    assert_eq!(
        format!("{}", RomError::UnsupportedMapper(4)),
        "unsupported mapper: 004 (not implemented)"
    );
    assert_eq!(
        format!("{}", RomError::UnsupportedMapper(99)),
        "unsupported mapper: 099 (not implemented)"
    );
    assert_eq!(
        format!("{}", RomError::UnsupportedMapper(255)),
        "unsupported mapper: 255 (not implemented)"
    );
}

#[test]
fn load_rom_without_header_returns_format_error() {
    // Bytes that do not start with the iNES magic.
    let bogus = b"NOT_A_NES_ROM_AT_ALL_____";
    let result = load_rom(&mut SliceReader(bogus.as_slice()));
    let err = match result {
        Ok(_) => panic!("expected error for non-iNES bytes"),
        Err(e) => e,
    };
    match err {
        RomError::RomFormat(msg) => {
            assert!(
                msg.contains("magic"),
                "expected magic-byte error, got: {msg}"
            );
        }
        other => panic!("expected RomFormat, got {other:?}"),
    }
}

#[test]
fn load_rom_truncated_returns_io_error() {
    // Valid header but no PRG / CHR data following it.
    let mut header = [0u8; 16];
    header[0..4].copy_from_slice(b"NES\x1A");
    header[4] = 1; // claims one 16 KB PRG bank but provides nothing
    header[5] = 0;
    let result = load_rom(&mut SliceReader(header.as_slice()));
    let err = match result {
        Ok(_) => panic!("expected error for truncated ROM"),
        Err(e) => e,
    };
    match err {
        RomError::Io(io_err) => {
            assert_eq!(io_err.kind(), std::io::ErrorKind::UnexpectedEof);
        }
        other => panic!("expected Io(UnexpectedEof), got {other:?}"),
    }
}