From 1d386c5503dfa114f23766494641b7d3a9109322 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com>
Date: Mon, 22 May 2023 00:23:07 +0100
Subject: [PATCH] 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.

---
 frontends/sdl/src/main.rs | 10 +++++--
 src/devices/buffer.rs     | 62 +++++++++++++++++++++++++++++++++++++++
 src/devices/mod.rs        |  1 +
 src/devices/printer.rs    |  4 +++
 src/devices/stdout.rs     |  8 +++--
 src/gb.rs                 |  5 ++--
 src/lib.rs                |  1 +
 src/serial.rs             | 12 ++++++++
 src/test.rs               | 53 +++++++++++++++++++++++++++++++++
 9 files changed, 150 insertions(+), 6 deletions(-)
 create mode 100644 src/devices/buffer.rs
 create mode 100644 src/test.rs

diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs
index c4137fda..5ab6c7fc 100644
--- a/frontends/sdl/src/main.rs
+++ b/frontends/sdl/src/main.rs
@@ -484,6 +484,8 @@ impl Emulator {
     }
 
     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
         // are going to move (because of overflow) from one tick to another
         let mut pending_cycles = 0u32;
@@ -540,7 +542,7 @@ impl Emulator {
                 // fot the current tick an in case the total number of cycles
                 // exceeds the allowed cycles then the loop is broken
                 total_cycles += cycle_limit as u64;
-                if total_cycles >= allowed_cycles.unwrap_or(u64::MAX) {
+                if total_cycles >= allowed_cycles {
                     break;
                 }
 
@@ -635,7 +637,7 @@ fn main() {
 
     // creates a new Game Boy instance and loads both the boot ROM
     // 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);
     game_boy.set_ppu_enabled(!args.no_ppu);
     game_boy.set_apu_enabled(!args.no_apu);
@@ -663,6 +665,10 @@ fn main() {
     emulator.start(SCREEN_SCALE);
     emulator.load_rom(Some(&args.rom_path));
     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 {
         emulator.run_headless(if args.cycles > 0 {
             Some(args.cycles)
diff --git a/src/devices/buffer.rs b/src/devices/buffer.rs
new file mode 100644
index 00000000..cf5707b7
--- /dev/null
+++ b/src/devices/buffer.rs
@@ -0,0 +1,62 @@
+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")
+    }
+}
diff --git a/src/devices/mod.rs b/src/devices/mod.rs
index ada19f91..339782b8 100644
--- a/src/devices/mod.rs
+++ b/src/devices/mod.rs
@@ -1,2 +1,3 @@
+pub mod buffer;
 pub mod printer;
 pub mod stdout;
diff --git a/src/devices/printer.rs b/src/devices/printer.rs
index cf0161a8..dade2045 100644
--- a/src/devices/printer.rs
+++ b/src/devices/printer.rs
@@ -316,6 +316,10 @@ impl SerialDevice for PrinterDevice {
     fn description(&self) -> String {
         format!("Printer [{}]", self.command)
     }
+
+    fn state(&self) -> String {
+        self.command.to_string()
+    }
 }
 
 impl Default for PrinterDevice {
diff --git a/src/devices/stdout.rs b/src/devices/stdout.rs
index 8a13eda0..75dbcd27 100644
--- a/src/devices/stdout.rs
+++ b/src/devices/stdout.rs
@@ -7,7 +7,7 @@ use std::{
 
 pub struct StdoutDevice {
     flush: bool,
-    callback: fn(image_buffer: &Vec<u8>),
+    callback: fn(buffer: &Vec<u8>),
 }
 
 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;
     }
 }
@@ -44,6 +44,10 @@ impl SerialDevice for StdoutDevice {
     fn description(&self) -> String {
         String::from("Stdout")
     }
+
+    fn state(&self) -> String {
+        String::from("")
+    }
 }
 
 impl Default for StdoutDevice {
diff --git a/src/gb.rs b/src/gb.rs
index 073fa08c..66c37c44 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -346,7 +346,8 @@ pub struct GameBoy {
 #[cfg_attr(feature = "wasm", wasm_bindgen)]
 impl GameBoy {
     #[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 {
             mode,
             ppu_enabled: true,
@@ -1052,7 +1053,7 @@ impl AudioProvider for GameBoy {
 
 impl Default for GameBoy {
     fn default() -> Self {
-        Self::new(GameBoyMode::Dmg)
+        Self::new(None)
     }
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index d25f5ac6..0f8c9a49 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,5 +14,6 @@ pub mod pad;
 pub mod ppu;
 pub mod rom;
 pub mod serial;
+pub mod test;
 pub mod timer;
 pub mod util;
diff --git a/src/serial.rs b/src/serial.rs
index a8f522b7..a9bd6205 100644
--- a/src/serial.rs
+++ b/src/serial.rs
@@ -1,7 +1,11 @@
 use crate::warnln;
 
 pub trait SerialDevice {
+    /// Sends a byte (u8) to the attached serial connection.
     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);
 
     /// Whether the serial device "driver" supports slave mode
@@ -11,6 +15,10 @@ pub trait SerialDevice {
 
     /// Returns a short description of the serial device.
     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 {
@@ -222,6 +230,10 @@ impl SerialDevice for NullDevice {
     fn description(&self) -> String {
         String::from("Null")
     }
+
+    fn state(&self) -> String {
+        String::from("")
+    }
 }
 
 impl Default for NullDevice {
diff --git a/src/test.rs b/src/test.rs
new file mode 100644
index 00000000..39dc1dba
--- /dev/null
+++ b/src/test.rs
@@ -0,0 +1,53 @@
+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");
+    }
+}
-- 
GitLab