1use 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
16pub const ROM_ALL_INSTR: &[u8] = include_bytes!("roms/all_instrs.nes");
18pub const ROM_NESTEST: &[u8] = include_bytes!("roms/nestest.nes");
20pub const ROM_NROM_TEST: &[u8] = include_bytes!("roms/nrom-test.nes");
22pub const ROM_OFFICIAL_ONLY: &[u8] = include_bytes!("roms/official_only.nes");
24
25pub trait TestableCpu: Cpu + Sized + 'static {
27 type GetCpuError: Error;
28
29 fn get_cpu(rom: &[u8]) -> Result<Self, Self::GetCpuError>;
32
33 fn set_program_counter(&mut self, value: u16);
36
37 fn memory_read(&self, address: u16) -> u8;
40}
41
42bitflags! {
43 pub struct TestSelector: u32 {
45 const NESTEST = 0b00000001;
49
50 const ALL_INSTRS = 0b00000010;
53
54 const OFFICIAL_INSTRS = 0b00000100;
57
58 const NROM_TEST = 0b00001000;
61
62 const ALL = Self::NESTEST.bits() | Self::ALL_INSTRS.bits() | Self::NROM_TEST.bits();
64
65 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
76pub 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
96fn 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 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
163fn nestest<T: TestableCpu + 'static>() -> Result<(), String> {
166 let rom = ROM_NESTEST;
167
168 let handle = thread::spawn(|| {
169 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
193fn 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 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}