tudelft_nes_test/
lib.rs

1//! # `tudelft-nes-test`
2//! This is a helper crate for your NES emulator to run various test ROMs
3use crate::all_instrs::{all_instrs_status_code, read_status_string};
4use bitflags::bitflags;
5use std::error::Error;
6use std::thread;
7use std::thread::JoinHandle;
8use thiserror::Error;
9use tudelft_nes_ppu::{run_cpu_headless_for, Cpu, Mirroring};
10
11mod all_instrs;
12mod nestest;
13
14use crate::nestest::nestest_status_code;
15
16/// Raw bytes for the all_instr rom
17pub const ROM_ALL_INSTR: &[u8] = include_bytes!("roms/all_instrs.nes");
18/// Raw bytes for the nestest rom
19pub const ROM_NESTEST: &[u8] = include_bytes!("roms/nestest.nes");
20/// Raw bytes for the nrom rom
21pub const ROM_NROM_TEST: &[u8] = include_bytes!("roms/nrom-test.nes");
22/// Raw bytes for the official_only rom
23pub const ROM_OFFICIAL_ONLY: &[u8] = include_bytes!("roms/official_only.nes");
24
25/// Implement this trait to run our test on our CPU via the [`run_tests`] function.
26pub trait TestableCpu: Cpu + Sized + 'static {
27    type GetCpuError: Error;
28
29    /// This function is used by the test suite to get a handle on your CPU
30    /// `rom` is a rom file in INES format.
31    fn get_cpu(rom: &[u8]) -> Result<Self, Self::GetCpuError>;
32
33    /// [`set_program_counter`] is used to set the program counter of the cpu to a specific position
34    /// this is needed by some tests.
35    fn set_program_counter(&mut self, value: u16);
36
37    /// [`memory_read`] is used to test the succesfulness of tests by seeing if the CPU has expected values
38    /// at certain memory locations, it simply takes an address and should return the byte of data at that memory location
39    fn memory_read(&self, address: u16) -> u8;
40}
41
42bitflags! {
43    /// Select which tests you want to run
44    pub struct TestSelector: u32 {
45        /// `NESTEST` is a pretty much all inclusive test suite for a NES CPU. It was designed to test almost every combination of flags, instructions,
46        /// and registers. Some of these tests are very difficult.
47        /// More information about this test ROM can be found [here](https://github.com/christopherpow/nes-test-roms/blob/master/other/nestest.txt)
48        const NESTEST         = 0b00000001;
49
50        /// `ALL_INSTRS` tests all instructions (including unofficial ones).
51        /// More function about this test can be found [here](https://github.com/christopherpow/nes-test-roms/tree/master/instr_test-v5)
52        const ALL_INSTRS      = 0b00000010;
53
54        /// `OFFICIAL_INSTRS` tests all official nes instructions, a finished emulator should pass this.
55        /// More function about this test can be found [here](https://github.com/christopherpow/nes-test-roms/tree/master/instr_test-v5)
56        const OFFICIAL_INSTRS = 0b00000100;
57
58        /// `NROM_TEST` is a very simple rom that tests some basic functionality, this is a good starting test to try and pass.
59        /// The source for this rom can be found [here](https://gitlab.ewi.tudelft.nl/software-fundamentals/nes-nrom-test/-/blob/main/src/init.s)
60        const NROM_TEST       = 0b00001000;
61
62        /// This test selector runs all available tests
63        const ALL             = Self::NESTEST.bits() | Self::ALL_INSTRS.bits() | Self::NROM_TEST.bits();
64
65        /// This test selector runs a default selection of tests: `OFFICIAL_INSTRS` and `NROM_TEST`
66        const DEFAULT         = Self::OFFICIAL_INSTRS.bits() | Self::NROM_TEST.bits();
67    }
68}
69
70impl Default for TestSelector {
71    fn default() -> Self {
72        Self::DEFAULT
73    }
74}
75
76/// The main function of this crate, run this with your CPU as generic parameter and a [`TestSelector`] to run the tests
77pub fn run_tests<T: TestableCpu>(selector: TestSelector) -> Result<(), String> {
78    if selector.contains(TestSelector::NROM_TEST) {
79        nrom_test::<T>()?;
80    }
81
82    if selector.contains(TestSelector::OFFICIAL_INSTRS) {
83        all_instrs::<T>(true)?;
84    }
85
86    if selector.contains(TestSelector::ALL_INSTRS) {
87        all_instrs::<T>(false)?;
88    }
89
90    if selector.contains(TestSelector::NESTEST) {
91        nestest::<T>()?;
92    }
93    Ok(())
94}
95
96/// Tests the emulator using "all_instrs.nes" or "official_only.nes":
97/// https://github.com/christopherpow/nes-test-roms/tree/master/instr_test-v5
98fn all_instrs<T: TestableCpu + 'static>(only_official: bool) -> Result<(), String> {
99    let (rom, limit) = if only_official {
100        (ROM_OFFICIAL_ONLY, 350)
101    } else {
102        (ROM_ALL_INSTR, 500)
103    };
104
105    let handle = thread::spawn(move || {
106        // TODO: make initial program counter obsolete by modifying nestest
107        let mut cpu = T::get_cpu(rom).map_err(|i| TestError::Custom(i.to_string()))?;
108        let mut prev = String::new();
109
110        for i in 0..limit {
111            if let Err(e1) = run_cpu_headless_for(&mut cpu, Mirroring::Horizontal, 200_000) {
112                if let Err(e2) = all_instrs_status_code(&cpu) {
113                    return Err(TestError::Custom(format!(
114                        "{e1}, possibly due to a test that didn't pass: '{e2}'"
115                    )));
116                } else {
117                    return Err(TestError::Custom(format!("{e1}")));
118                }
119            }
120
121            let status = read_status_string(&cpu);
122
123            if status.contains("Failed") {
124                break;
125            }
126
127            let status = status.split('\n').next().unwrap().trim().to_string();
128            if !status.is_empty() && status != prev {
129                log::info!("{:05}k cycles passed: {}", i * 200, status);
130            }
131            prev = status;
132        }
133
134        let result = run_cpu_headless_for(&mut cpu, Mirroring::Horizontal, 200_000);
135
136        match result {
137            Err(e1) => {
138                if let Err(e2) = all_instrs_status_code(&cpu) {
139                    Err(TestError::Custom(format!(
140                        "{e1}, possibly due to a test that didn't pass: '{e2}'"
141                    )))
142                } else {
143                    Err(TestError::Custom(format!("{e1}")))
144                }
145            }
146            Ok(()) => all_instrs_status_code(&cpu),
147        }
148    });
149
150    process_handle(
151        &format!(
152            "all instructions{}",
153            if only_official {
154                " (official only)"
155            } else {
156                ""
157            }
158        ),
159        handle,
160    )
161}
162
163/// Runs the nestest rom:
164/// https://github.com/christopherpow/nes-test-roms/blob/master/other/nestest.nes
165fn nestest<T: TestableCpu + 'static>() -> Result<(), String> {
166    let rom = ROM_NESTEST;
167
168    let handle = thread::spawn(|| {
169        // TODO: make initial program counter obsolete by modifying nestest
170        let mut cpu = T::get_cpu(rom).map_err(|i| TestError::Custom(i.to_string()))?;
171        cpu.set_program_counter(0xC000);
172        let result = run_cpu_headless_for(&mut cpu, Mirroring::Horizontal, 1_000_000);
173
174        match result {
175            Err(e1) => {
176                if let Err(e2) =
177                    nestest_status_code(cpu.memory_read(0x0002), cpu.memory_read(0x0003))
178                {
179                    Err(TestError::Custom(format!(
180                        "{e1}, possibly due to a test that didn't pass: '{e2}'"
181                    )))
182                } else {
183                    Err(TestError::Custom(format!("{e1}")))
184                }
185            }
186            Ok(()) => nestest_status_code(cpu.memory_read(0x0002), cpu.memory_read(0x0003)),
187        }
188    });
189
190    process_handle("nestest", handle)
191}
192
193/// runs our own nrom test rom
194/// https://gitlab.ewi.tudelft.nl/software-fundamentals/nes-nrom-test
195fn nrom_test<T: TestableCpu + 'static>() -> Result<(), String> {
196    let rom = ROM_NROM_TEST;
197
198    let handle = thread::spawn(|| {
199        let mut cpu = T::get_cpu(rom).map_err(|i| TestError::Custom(i.to_string()))?;
200        run_cpu_headless_for(&mut cpu, Mirroring::Horizontal, 10)
201            .map_err(|i| TestError::Custom(i.to_string()))?;
202
203        if cpu.memory_read(0x42) != 0x43 {
204            Err(TestError::String(
205                "memory location 0x42 is wrong after executing nrom_test".to_owned(),
206            ))
207        } else if cpu.memory_read(0x43) != 0x6A {
208            Err(TestError::String(
209                "memory location 0x43 is wrong after executing nrom_test".to_owned(),
210            ))
211        } else {
212            Ok(())
213        }
214    });
215
216    process_handle("nrom_test", handle)
217}
218
219#[derive(Debug, Error)]
220enum TestError {
221    #[error("{0}")]
222    Custom(String),
223    #[error("{0}")]
224    String(String),
225}
226
227fn process_handle(name: &str, handle: JoinHandle<Result<(), TestError>>) -> Result<(), String> {
228    match handle.join() {
229        // <- waits for the thread to complete or panic
230        Ok(Ok(_)) => {
231            log::info!("{name} finished succesfully");
232            Ok(())
233        }
234        Ok(Err(e)) => match e {
235            TestError::Custom(e) => Err(format!(
236                "cpu failed while running test {name} with custom error message {e}"
237            )),
238            TestError::String(e) => Err(format!("cpu didn't pass test {name}: '{e}'")),
239        },
240        Err(e) => {
241            let err_msg = match (e.downcast_ref::<&str>(), e.downcast_ref::<String>()) {
242                (Some(&s), _) => s,
243                (_, Some(s)) => s,
244                (None, None) => "<No panic info>",
245            };
246
247            Err(format!(
248                "cpu implementation panicked while running test {name}: {err_msg}"
249            ))
250        }
251    }
252}