Skip to content
Snippets Groups Projects
Verified Commit 1d386c55 authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

chore: support for ROM unit testing

Created infrastructure that allows the usage of the serial device to capture the result of the tests.
Then the test is executed agains a pre-defined number of cycles and the result is compared against expectation.
parent d96040f5
No related branches found
No related tags found
No related merge requests found
Pipeline #2774 passed
...@@ -484,6 +484,8 @@ impl Emulator { ...@@ -484,6 +484,8 @@ impl Emulator {
} }
pub fn run_headless(&mut self, allowed_cycles: Option<u64>) { pub fn run_headless(&mut self, allowed_cycles: Option<u64>) {
let allowed_cycles = allowed_cycles.unwrap_or(u64::MAX);
// starts the variable that will control the number of cycles that // starts the variable that will control the number of cycles that
// are going to move (because of overflow) from one tick to another // are going to move (because of overflow) from one tick to another
let mut pending_cycles = 0u32; let mut pending_cycles = 0u32;
...@@ -540,7 +542,7 @@ impl Emulator { ...@@ -540,7 +542,7 @@ impl Emulator {
// fot the current tick an in case the total number of cycles // fot the current tick an in case the total number of cycles
// exceeds the allowed cycles then the loop is broken // exceeds the allowed cycles then the loop is broken
total_cycles += cycle_limit as u64; total_cycles += cycle_limit as u64;
if total_cycles >= allowed_cycles.unwrap_or(u64::MAX) { if total_cycles >= allowed_cycles {
break; break;
} }
...@@ -635,7 +637,7 @@ fn main() { ...@@ -635,7 +637,7 @@ fn main() {
// creates a new Game Boy instance and loads both the boot ROM // creates a new Game Boy instance and loads both the boot ROM
// and the initial game ROM to "start the engine" // and the initial game ROM to "start the engine"
let mut game_boy = GameBoy::new(mode); let mut game_boy = GameBoy::new(Some(mode));
let device = build_device(&args.device); let device = build_device(&args.device);
game_boy.set_ppu_enabled(!args.no_ppu); game_boy.set_ppu_enabled(!args.no_ppu);
game_boy.set_apu_enabled(!args.no_apu); game_boy.set_apu_enabled(!args.no_apu);
...@@ -663,6 +665,10 @@ fn main() { ...@@ -663,6 +665,10 @@ fn main() {
emulator.start(SCREEN_SCALE); emulator.start(SCREEN_SCALE);
emulator.load_rom(Some(&args.rom_path)); emulator.load_rom(Some(&args.rom_path));
emulator.toggle_palette(); emulator.toggle_palette();
// determines if the emulator should run in headless mode or
// not and runs it accordingly, note that if running in headless
// mode the number of cycles to be run may be specified
if args.headless { if args.headless {
emulator.run_headless(if args.cycles > 0 { emulator.run_headless(if args.cycles > 0 {
Some(args.cycles) Some(args.cycles)
......
use crate::serial::SerialDevice;
use std::fmt::{self, Display, Formatter};
pub struct BufferDevice {
buffer: Vec<u8>,
callback: fn(image_buffer: &Vec<u8>),
}
impl BufferDevice {
pub fn new() -> Self {
Self {
buffer: Vec::new(),
callback: |_| {},
}
}
pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) {
self.callback = callback;
}
pub fn buffer(&self) -> &Vec<u8> {
&self.buffer
}
}
impl SerialDevice for BufferDevice {
fn send(&mut self) -> u8 {
0xff
}
fn receive(&mut self, byte: u8) {
self.buffer.push(byte);
let data = vec![byte];
(self.callback)(&data);
}
fn allow_slave(&self) -> bool {
false
}
fn description(&self) -> String {
String::from("Buffer")
}
fn state(&self) -> String {
let buffer = self.buffer.clone();
String::from_utf8(buffer).unwrap()
}
}
impl Default for BufferDevice {
fn default() -> Self {
Self::new()
}
}
impl Display for BufferDevice {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Buffer")
}
}
pub mod buffer;
pub mod printer; pub mod printer;
pub mod stdout; pub mod stdout;
...@@ -316,6 +316,10 @@ impl SerialDevice for PrinterDevice { ...@@ -316,6 +316,10 @@ impl SerialDevice for PrinterDevice {
fn description(&self) -> String { fn description(&self) -> String {
format!("Printer [{}]", self.command) format!("Printer [{}]", self.command)
} }
fn state(&self) -> String {
self.command.to_string()
}
} }
impl Default for PrinterDevice { impl Default for PrinterDevice {
......
...@@ -7,7 +7,7 @@ use std::{ ...@@ -7,7 +7,7 @@ use std::{
pub struct StdoutDevice { pub struct StdoutDevice {
flush: bool, flush: bool,
callback: fn(image_buffer: &Vec<u8>), callback: fn(buffer: &Vec<u8>),
} }
impl StdoutDevice { impl StdoutDevice {
...@@ -18,7 +18,7 @@ impl StdoutDevice { ...@@ -18,7 +18,7 @@ impl StdoutDevice {
} }
} }
pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) { pub fn set_callback(&mut self, callback: fn(buffer: &Vec<u8>)) {
self.callback = callback; self.callback = callback;
} }
} }
...@@ -44,6 +44,10 @@ impl SerialDevice for StdoutDevice { ...@@ -44,6 +44,10 @@ impl SerialDevice for StdoutDevice {
fn description(&self) -> String { fn description(&self) -> String {
String::from("Stdout") String::from("Stdout")
} }
fn state(&self) -> String {
String::from("")
}
} }
impl Default for StdoutDevice { impl Default for StdoutDevice {
......
...@@ -346,7 +346,8 @@ pub struct GameBoy { ...@@ -346,7 +346,8 @@ pub struct GameBoy {
#[cfg_attr(feature = "wasm", wasm_bindgen)] #[cfg_attr(feature = "wasm", wasm_bindgen)]
impl GameBoy { impl GameBoy {
#[cfg_attr(feature = "wasm", wasm_bindgen(constructor))] #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
pub fn new(mode: GameBoyMode) -> Self { pub fn new(mode: Option<GameBoyMode>) -> Self {
let mode = mode.unwrap_or(GameBoyMode::Dmg);
let gbc = Rc::new(RefCell::new(GameBoyConfig { let gbc = Rc::new(RefCell::new(GameBoyConfig {
mode, mode,
ppu_enabled: true, ppu_enabled: true,
...@@ -1052,7 +1053,7 @@ impl AudioProvider for GameBoy { ...@@ -1052,7 +1053,7 @@ impl AudioProvider for GameBoy {
impl Default for GameBoy { impl Default for GameBoy {
fn default() -> Self { fn default() -> Self {
Self::new(GameBoyMode::Dmg) Self::new(None)
} }
} }
......
...@@ -14,5 +14,6 @@ pub mod pad; ...@@ -14,5 +14,6 @@ pub mod pad;
pub mod ppu; pub mod ppu;
pub mod rom; pub mod rom;
pub mod serial; pub mod serial;
pub mod test;
pub mod timer; pub mod timer;
pub mod util; pub mod util;
use crate::warnln; use crate::warnln;
pub trait SerialDevice { pub trait SerialDevice {
/// Sends a byte (u8) to the attached serial connection.
fn send(&mut self) -> u8; fn send(&mut self) -> u8;
/// Receives a byte (u8) from the attached serial connection,
/// can be either another device or the host.
fn receive(&mut self, byte: u8); fn receive(&mut self, byte: u8);
/// Whether the serial device "driver" supports slave mode /// Whether the serial device "driver" supports slave mode
...@@ -11,6 +15,10 @@ pub trait SerialDevice { ...@@ -11,6 +15,10 @@ pub trait SerialDevice {
/// Returns a short description of the serial device. /// Returns a short description of the serial device.
fn description(&self) -> String; fn description(&self) -> String;
/// Returns a string describing the current state of the
/// serial device. Could be used for debugging purposes.
fn state(&self) -> String;
} }
pub struct Serial { pub struct Serial {
...@@ -222,6 +230,10 @@ impl SerialDevice for NullDevice { ...@@ -222,6 +230,10 @@ impl SerialDevice for NullDevice {
fn description(&self) -> String { fn description(&self) -> String {
String::from("Null") String::from("Null")
} }
fn state(&self) -> String {
String::from("")
}
} }
impl Default for NullDevice { impl Default for NullDevice {
......
use crate::{devices::buffer::BufferDevice, gb::GameBoy};
#[derive(Default)]
pub struct TestOptions {
ppu_enabled: Option<bool>,
apu_enabled: Option<bool>,
dma_enabled: Option<bool>,
timer_enabled: Option<bool>,
}
pub fn build_test(options: TestOptions) -> GameBoy {
let device = Box::<BufferDevice>::default();
let mut game_boy = GameBoy::new(None);
game_boy.set_ppu_enabled(options.ppu_enabled.unwrap_or(true));
game_boy.set_apu_enabled(options.apu_enabled.unwrap_or(true));
game_boy.set_dma_enabled(options.dma_enabled.unwrap_or(true));
game_boy.set_timer_enabled(options.timer_enabled.unwrap_or(true));
game_boy.attach_serial(device);
game_boy.load(true);
game_boy
}
pub fn run_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) -> String {
let mut cycles = 0u64;
let max_cycles = max_cycles.unwrap_or(u64::MAX);
let mut game_boy = build_test(options);
game_boy.load_rom_file(rom_path);
loop {
cycles += game_boy.clock() as u64;
if cycles >= max_cycles {
break;
}
}
game_boy.serial().device().state()
}
#[cfg(test)]
mod tests {
use super::{run_test, TestOptions};
#[test]
fn test_blargg_cpu_instrs() {
let result = run_test(
"res/roms/test/blargg/cpu/cpu_instrs.gb",
Some(300000000),
TestOptions::default(),
);
assert_eq!(result, "cpu_instrs\n\n01:ok 02:ok 03:ok 04:ok 05:ok 06:ok 07:ok 08:ok 09:ok 10:ok 11:ok \n\nPassed all tests\n");
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment