diff --git a/src/cpu/tests.rs b/src/cpu/tests.rs index b7bb860..754d93a 100644 --- a/src/cpu/tests.rs +++ b/src/cpu/tests.rs @@ -1,5 +1,9 @@ +//! Tests for cpu.rs +//! +//! These run instructions, and ensure their output is consistent with previous builds + use super::*; -use crate::{ +pub(self) use crate::{ bus, bus::{Bus, Region::*}, }; @@ -26,6 +30,8 @@ fn setup_environment() -> (CPU, Bus) { } /// Unused instructions +/// +/// TODO: Exhaustively test unused instructions #[test] #[should_panic] fn unimplemented() { @@ -35,623 +41,744 @@ fn unimplemented() { cpu.unimplemented(0xffff); } -/// 0aaa: Handles a "machine language function call" (lmao) -#[test] -#[should_panic] -fn sys() { - let (mut cpu, mut bus) = setup_environment(); - bus.write(0x200u16, 0x0200u16); // 0x0200 is not one of the defined ML routines - cpu.tick(&mut bus); - cpu.sys(0x200); -} - -/// 00e0: Clears the screen memory to 0 -#[test] -fn clear_screen() { - let (mut cpu, mut bus) = setup_environment(); - bus.write(0x200u16, 0x00e0u16); - // Check if screen RAM is cleared - cpu.tick(&mut bus); - bus.get_region(Screen) - .expect("Expected screen, got None") - .iter() - .for_each(|byte| assert_eq!(*byte, 0)); -} - -/// 00ee: Returns from subroutine -#[test] -fn ret() { - let test_addr = random::() & 0x7ff; - let (mut cpu, mut bus) = setup_environment(); - let sp_orig = cpu.sp; - // Place the address on the stack - bus.write(cpu.sp.wrapping_add(2), test_addr); - // Call an address - cpu.ret(&mut bus); - // Verify the current address is the address from the stack - assert_eq!(test_addr, cpu.pc); - assert!(dbg!(cpu.sp.wrapping_sub(sp_orig)) == 0x2); - // Verify the stack pointer has moved -} - -/// 1aaa: Sets the program counter to an absolute address -#[test] -fn jump() { - // Generate a random test address that's not 0x200 - let test_addr = random::() & !0x200; - let (mut cpu, _) = setup_environment(); - // Call an address - cpu.jump(test_addr); - // Verify the current address is the called address - assert_eq!(test_addr, cpu.pc); -} - -/// 2aaa: Pushes pc onto the stack, then jumps to a -#[test] -fn call() { - let test_addr = random::(); - let (mut cpu, mut bus) = setup_environment(); - // Save the current address - let curr_addr = cpu.pc; - // Call an address - cpu.call(test_addr, &mut bus); - // Verify the current address is the called address - assert_eq!(test_addr, cpu.pc); - // Verify the previous address was stored on the stack (sp+2) - let stack_addr: u16 = bus.read(cpu.sp.wrapping_add(2)); - assert_eq!(stack_addr, curr_addr); -} - -/// 3xbb: Skips the next instruction if register X == b -#[test] -fn skip_if_x_equal_byte() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b, addr) = (word as u8, (word >> 4) as u8, random::() & 0x7fe); - for x in 0..=0xf { - // set the PC to a random address - cpu.pc = addr; - // set the register under test to a - cpu.v[x] = a; - // do the thing - cpu.skip_if_x_equal_byte(x, b); - // validate the result - assert_eq!(cpu.pc, addr.wrapping_add(if dbg!(a == b) { 2 } else { 0 })); - } - } -} - -/// 4xbb: Skips the next instruction if register X != b -#[test] -fn skip_if_x_not_equal_byte() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b, addr) = (word as u8, (word >> 4) as u8, random::() & 0x7fe); - for x in 0..=0xf { - // set the PC to a random address - cpu.pc = addr; - // set the register under test to a - cpu.v[x] = a; - // do the thing - cpu.skip_if_x_not_equal_byte(x, b); - // validate the result - assert_eq!(cpu.pc, addr.wrapping_add(if a != b { 2 } else { 0 })); - } - } -} - -/// 5xy0: Skips the next instruction if register X != register Y -#[test] -fn skip_if_x_equal_y() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b, addr) = (word as u8, (word >> 4) as u8, random::() & 0x7fe); - for reg in 0..=0xff { - let (x, y) = (reg & 0xf, reg >> 4); - if x == y { - continue; - } - // set the PC to a random address - cpu.pc = addr; - // set the registers under test to a, b - (cpu.v[x], cpu.v[y]) = (a, b); - // do the thing - cpu.skip_if_x_equal_y(x, y); - // validate the result - assert_eq!(cpu.pc, addr.wrapping_add(if a == b { 2 } else { 0 })); - } - } -} -/// 6xbb: Loads immediate byte b into register vX -#[test] -fn load_immediate() { - let (mut cpu, _) = setup_environment(); - for test_register in 0x0..=0xf { - for test_byte in 0x0..=0xff { - cpu.load_immediate(test_register, test_byte); - assert_eq!(cpu.v[test_register], test_byte) - } - } -} - -/// 7xbb: Adds immediate byte b to register vX -#[test] -fn add_immediate() { - let (mut cpu, _) = setup_environment(); - for test_register in 0x0..=0xf { - let mut sum = 0u8; - for test_byte in 0x0..=0xff { - // Wrapping-add to the running total (Chip-8 allows unsigned overflow) - sum = sum.wrapping_add(test_byte); - // Perform add #byte, vReg - cpu.add_immediate(test_register, test_byte); - //Verify the running total in the register matches - assert_eq!(cpu.v[test_register], sum); - } - } -} - -/// 8xy0: Loads the value of y into x -#[test] -fn load_y_into_x() { - let (mut cpu, _) = setup_environment(); - // We use zero as a sentinel value for this test, so loop from 1 to 255 - for test_value in 1..=0xff { - for reg in 0..=0xff { - let (x, y) = (reg & 0xf, reg >> 4); - if x == y { - continue; - } - // Set vY to the test value - cpu.v[y] = test_value; - // zero X - cpu.v[x] = 0; - cpu.load_y_into_x(x, y); - // verify results - assert_eq!(cpu.v[x], test_value); - assert_eq!(cpu.v[y], test_value); - } - } -} - -/// 8xy1: Performs bitwise or of vX and vY, and stores the result in vX -#[test] -fn x_orequals_y() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b) = (word as u8, (word >> 4) as u8); - let expected_result = a | b; - for reg in 0..=0xff { - let (x, y) = (reg & 0xf, reg >> 4); - // set the registers under test to a, b - (cpu.v[x], cpu.v[y]) = (a, b); - - // do the thing - cpu.x_orequals_y(x, y); - - // validate the result - assert_eq!(cpu.v[x], if x == y { b } else { expected_result }); - } - } -} - -/// 8xy2: Performs bitwise and of vX and vY, and stores the result in vX -#[test] -fn x_andequals_y() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b) = (word as u8, (word >> 4) as u8); - let expected_result = a & b; - for reg in 0..=0xff { - let (x, y) = (reg & 0xf, reg >> 4); - // set the registers under test to a, b - (cpu.v[x], cpu.v[y]) = (a, b); - - // do the thing - cpu.x_andequals_y(x, y); - - // validate the result - assert_eq!(cpu.v[x], if x == y { b } else { expected_result }); - } - } -} - -/// 8xy3: Performs bitwise xor of vX and vY, and stores the result in vX -#[test] -fn x_xorequals_y() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b) = (word as u8, (word >> 4) as u8); - let expected_result = a ^ b; - for reg in 0..=0xff { - let (x, y) = (reg & 0xf, reg >> 4); - // set the registers under test to a, b - (cpu.v[x], cpu.v[y]) = (a, b); - - // do the thing - cpu.x_xorequals_y(x, y); - - // validate the result - assert_eq!(cpu.v[x], if x == y { 0 } else { expected_result }); - } - } -} - -/// 8xy4: Performs addition of vX and vY, and stores the result in vX, carry in vF -/// If X is F, *only* stores borrow -#[test] -fn x_addequals_y() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b) = (word as u8, (word >> 4) as u8); - for reg in 0..=0xff { - let (x, y) = (reg & 0xf, reg >> 4); - // calculate the expected result - // If x == y, a is discarded - let (expected, carry) = if x == y { b } else { a }.overflowing_add(b); - // set the registers under test to a, b - (cpu.v[x], cpu.v[y]) = (a, b); - - // do the thing - cpu.x_addequals_y(x, y); - - // validate the result - // if the destination is vF, the result was discarded, and only the carry was kept - if x != 0xf { - assert_eq!(cpu.v[x], expected); - } - assert_eq!(cpu.v[0xf], carry.into()); - } - } -} - -/// 8xy5: Performs subtraction of vX and vY, and stores the result in vX, borrow in vF -#[test] -fn x_subequals_y() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b) = (word as u8, (word >> 4) as u8); - for reg in 0..=0xff { - let (x, y) = (reg & 0xf, reg >> 4); - // calculate the expected result - let (expected, carry) = if x == y { b } else { a }.overflowing_sub(b); - // set the registers under test to a, b - (cpu.v[x], cpu.v[y]) = (a, b); - - // do the thing - cpu.x_subequals_y(x, y); - - // validate the result - // if the destination is vF, the result was discarded, and only the carry was kept - if x != 0xf { - assert_eq!(cpu.v[x], expected); - } - // The borrow flag for subtraction is inverted - assert_eq!(cpu.v[0xf], (!carry).into()); - } - } -} - -/// 8xy6: Performs bitwise right shift of vX, stores carry-out in vF -#[test] -fn shift_right_x() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xff { - for x in 0..=0xf { - // set the register under test to `word` - cpu.v[x] = word; - // calculate the expected result - let expected = word >> 1; - // do the thing - cpu.shift_right_x(x); - - // validate the result - // if the destination is vF, the result was discarded, and only the carry was kept - if x != 0xf { - assert_eq!(cpu.v[x], expected); - } - // The borrow flag for subtraction is inverted - assert_eq!(cpu.v[0xf], word & 1); - } - } -} - -/// 8xy7: Performs subtraction of vY and vX, and stores the result in vX and ~carry in vF -#[test] -fn backwards_subtract() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b) = (word as u8, (word >> 4) as u8); - for reg in 0..=0xff { - let (x, y) = (reg & 0xf, reg >> 4); - // calculate the expected result - let (expected, carry) = if x == y { a } else { b }.overflowing_sub(a); - // set the registers under test to a, b - (cpu.v[x], cpu.v[y]) = (a, b); - - // do the thing - cpu.backwards_subtract(x, y); - - // validate the result - // if the destination is vF, the result was discarded, and only the carry was kept - if x != 0xf { - assert_eq!(cpu.v[x], expected); - } - // The borrow flag for subtraction is inverted - assert_eq!(cpu.v[0xf], (!carry).into()); - } - } -} - -/// 8X_E: Performs bitwise left shift of vX -#[test] -fn shift_left_x() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xff { - for x in 0..=0xf { - // set the register under test to `word` - cpu.v[x] = word; - // calculate the expected result - let expected = word << 1; - // do the thing - cpu.shift_left_x(x); - - // validate the result - // if the destination is vF, the result was discarded, and only the carry was kept - if x != 0xf { - assert_eq!(cpu.v[x], expected); - } - // The borrow flag for subtraction is inverted - assert_eq!(cpu.v[0xf], word >> 7); - } - } -} - -/// 9xy0: Skip next instruction if X != y -#[test] -fn skip_if_x_not_equal_y() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xffff { - let (a, b, addr) = (word as u8, (word >> 4) as u8, random::() & 0x7fe); - for reg in 0..=0xff { - let (x, y) = (reg & 0xf, reg >> 4); - if x == y { - continue; - } - // set the PC to a random address - cpu.pc = addr; - // set the registers under test to a, b - (cpu.v[x], cpu.v[y]) = (a, b); - // do the thing - cpu.skip_if_x_not_equal_y(x, y); - // validate the result - assert_eq!(cpu.pc, addr.wrapping_add(if a != b { 2 } else { 0 })); - } - } -} - -/// Aadr: Load address #adr into register I -#[test] -fn load_indirect_register() { - let (mut cpu, _) = setup_environment(); - // For every valid address - for addr in 0..0x1000 { - // Load indirect register - cpu.load_indirect_register(addr); - // Validate register set - assert_eq!(cpu.i, addr); - } -} - -/// Badr: Jump to &adr + v0 -#[test] -fn jump_indexed() { - let (mut cpu, _) = setup_environment(); - // For every valid address - for addr in 0..0x1000 { - // For every valid offset - for v0 in 0..=0xff { - // set v[0] = v0 - cpu.v[0] = v0; - // jump indexed - cpu.jump_indexed(addr); - // Validate register set - assert_eq!(cpu.pc, addr.wrapping_add(v0.into())); - } - } -} - -/// Cxbb: Stores a random number & the provided byte into vX -//#[test] -#[allow(dead_code)] -fn rand() { - todo!() -} - -struct ScreenTest { - program: &'static [u8], - screen: &'static [u8], - steps: usize, - rate: usize, -} - -const SCREEN_TESTS: [ScreenTest; 4] = [ - // Passing BC_test - ScreenTest { - program: include_bytes!("../../chip-8/BC_test.ch8"), - screen: include_bytes!("tests/BC_test.ch8_197.bin"), - steps: 197, - rate: 8, - }, - // The IBM Logo - ScreenTest { - program: include_bytes!("../../chip-8/IBM Logo.ch8"), - screen: include_bytes!("tests/IBM Logo.ch8_20.bin"), - steps: 20, - rate: 8, - }, - // Rule 22 cellular automata - ScreenTest { - program: include_bytes!("../../chip-8/1dcell.ch8"), - screen: include_bytes!("tests/1dcell.ch8_123342.bin"), - steps: 123342, - rate: 8, - }, - // Rule 60 cellular automata - ScreenTest { - program: include_bytes!("../../chip-8/1dcell.ch8"), - screen: include_bytes!("tests/1dcell.ch8_2391162.bin"), - steps: 2391162, - rate: 8, - }, -]; - -/// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY) -#[test] -fn draw() { - for test in SCREEN_TESTS { +mod sys { + use super::*; + /// 0aaa: Handles a "machine language function call" (lmao) + #[test] + #[should_panic] + fn sys() { let (mut cpu, mut bus) = setup_environment(); - // Load the test program - bus = bus.load_region(Program, test.program); - // Run the test program for the specified number of steps - cpu.multistep(&mut bus, test.steps, test.rate); - // Compare the screen to the reference screen buffer - assert_eq!(bus.get_region(Screen).unwrap(), test.screen); + bus.write(0x200u16, 0x0200u16); // 0x0200 is not one of the allowed routines + cpu.tick(&mut bus); + cpu.sys(0x200); + } + + /// 00e0: Clears the screen memory to 0 + #[test] + fn clear_screen() { + let (mut cpu, mut bus) = setup_environment(); + bus.write(0x200u16, 0x00e0u16); + // Check if screen RAM is cleared + cpu.tick(&mut bus); + bus.get_region(Screen) + .expect("Expected screen, got None") + .iter() + .for_each(|byte| assert_eq!(*byte, 0)); + } + + /// 00ee: Returns from subroutine + #[test] + fn ret() { + let test_addr = random::() & 0x7ff; + let (mut cpu, mut bus) = setup_environment(); + let sp_orig = cpu.sp; + // Place the address on the stack + bus.write(cpu.sp.wrapping_add(2), test_addr); + // Call an address + cpu.ret(&mut bus); + // Verify the current address is the address from the stack + assert_eq!(test_addr, cpu.pc); + assert!(dbg!(cpu.sp.wrapping_sub(sp_orig)) == 0x2); + // Verify the stack pointer has moved } } -/// Ex9E: Skip next instruction if key == #X -//#[test] -#[allow(dead_code)] -fn skip_if_key_equals_x() { - todo!() -} +/// Tests control-flow instructions +/// +/// Basically anything that touches the program counter +mod cf { + use super::*; + /// 1aaa: Sets the program counter to an absolute address + #[test] + fn jump() { + let (mut cpu, _) = setup_environment(); + // Test all valid addresses + for addr in 0x000..0xffe { + // Call an address + cpu.jump(addr); + // Verify the current address is the called address + assert_eq!(addr, cpu.pc); + } + } -/// ExaE: Skip next instruction if key != #X -//#[test] -#[allow(dead_code)] -fn skip_if_key_not_x() { - todo!() -} + /// 2aaa: Pushes pc onto the stack, then jumps to a + #[test] + fn call() { + let test_addr = random::(); + let (mut cpu, mut bus) = setup_environment(); + // Save the current address + let curr_addr = cpu.pc; + // Call an address + cpu.call(test_addr, &mut bus); + // Verify the current address is the called address + assert_eq!(test_addr, cpu.pc); + // Verify the previous address was stored on the stack (sp+2) + let stack_addr: u16 = bus.read(cpu.sp.wrapping_add(2)); + assert_eq!(stack_addr, curr_addr); + } -/// Fx07: Get the current DT, and put it in vX -/// ```py -/// vX = DT -/// ``` -#[test] -fn get_delay_timer() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xff { - for x in 0..=0xf { - // set the register under test to `word` - cpu.delay = word; - // do the thing - cpu.get_delay_timer(x); - // validate the result - assert_eq!(cpu.v[x], word); + /// 3xbb: Skips the next instruction if register X == b + #[test] + fn skip_equals_immediate() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b, addr) = (word as u8, (word >> 4) as u8, random::() & 0x7fe); + for x in 0..=0xf { + // set the PC to a random address + cpu.pc = addr; + // set the register under test to a + cpu.v[x] = a; + // do the thing + cpu.skip_equals_immediate(x, b); + // validate the result + assert_eq!(cpu.pc, addr.wrapping_add(if dbg!(a == b) { 2 } else { 0 })); + } + } + } + + /// 4xbb: Skips the next instruction if register X != b + #[test] + fn skip_not_equals_immediate() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b, addr) = (word as u8, (word >> 4) as u8, random::() & 0x7fe); + for x in 0..=0xf { + // set the PC to a random address + cpu.pc = addr; + // set the register under test to a + cpu.v[x] = a; + // do the thing + cpu.skip_not_equals_immediate(x, b); + // validate the result + assert_eq!(cpu.pc, addr.wrapping_add(if a != b { 2 } else { 0 })); + } + } + } + + /// 5xy0: Skips the next instruction if register X != register Y + #[test] + fn skip_equals() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b, addr) = (word as u8, (word >> 4) as u8, random::() & 0x7fe); + for reg in 0..=0xff { + let (x, y) = (reg & 0xf, reg >> 4); + if x == y { + continue; + } + // set the PC to a random address + cpu.pc = addr; + // set the registers under test to a, b + (cpu.v[x], cpu.v[y]) = (a, b); + // do the thing + cpu.skip_equals(x, y); + // validate the result + assert_eq!(cpu.pc, addr.wrapping_add(if a == b { 2 } else { 0 })); + } + } + } + + /// 9xy0: Skip next instruction if X != y + #[test] + fn skip_not_equals() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b, addr) = (word as u8, (word >> 4) as u8, random::() & 0x7fe); + for reg in 0..=0xff { + let (x, y) = (reg & 0xf, reg >> 4); + if x == y { + continue; + } + // set the PC to a random address + cpu.pc = addr; + // set the registers under test to a, b + (cpu.v[x], cpu.v[y]) = (a, b); + // do the thing + cpu.skip_not_equals(x, y); + // validate the result + assert_eq!(cpu.pc, addr.wrapping_add(if a != b { 2 } else { 0 })); + } + } + } + + /// Badr: Jump to &adr + v0 + #[test] + fn jump_indexed() { + let (mut cpu, _) = setup_environment(); + // For every valid address + for addr in 0..0x1000 { + // For every valid offset + for v0 in 0..=0xff { + // set v[0] = v0 + cpu.v[0] = v0; + // jump indexed + cpu.jump_indexed(addr); + // Validate register set + assert_eq!(cpu.pc, addr.wrapping_add(v0.into())); + } } } } -/// Fx0A: Wait for key, then vX = K -//#[test] -#[allow(dead_code)] -fn wait_for_key() { - todo!() -} +mod math { + use super::*; + /// 6xbb: Loads immediate byte b into register vX + #[test] + fn load_immediate() { + let (mut cpu, _) = setup_environment(); + for test_register in 0x0..=0xf { + for test_byte in 0x0..=0xff { + cpu.load_immediate(test_register, test_byte); + assert_eq!(cpu.v[test_register], test_byte) + } + } + } -/// Fx15: Load vX into DT -/// ```py -/// DT = vX -/// ``` -#[test] -fn load_delay_timer() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xff { - for x in 0..=0xf { - // set the register under test to `word` - cpu.v[x] = word; - // do the thing - cpu.load_delay_timer(x); - // validate the result - assert_eq!(cpu.delay, word); + /// 7xbb: Adds immediate byte b to register vX + #[test] + fn add_immediate() { + let (mut cpu, _) = setup_environment(); + for test_register in 0x0..=0xf { + let mut sum = 0u8; + for test_byte in 0x0..=0xff { + // Wrapping-add to the running total (Chip-8 allows unsigned overflow) + sum = sum.wrapping_add(test_byte); + // Perform add #byte, vReg + cpu.add_immediate(test_register, test_byte); + //Verify the running total in the register matches + assert_eq!(cpu.v[test_register], sum); + } + } + } + + /// 8xy0: Loads the value of y into x + // TODO: Test with authentic flag set + #[test] + fn load() { + let (mut cpu, _) = setup_environment(); + // We use zero as a sentinel value for this test, so loop from 1 to 255 + for test_value in 1..=0xff { + for reg in 0..=0xff { + let (x, y) = (reg & 0xf, reg >> 4); + if x == y { + continue; + } + // Set vY to the test value + cpu.v[y] = test_value; + // zero X + cpu.v[x] = 0; + cpu.load(x, y); + // verify results + assert_eq!(cpu.v[x], test_value); + assert_eq!(cpu.v[y], test_value); + } + } + } + + /// 8xy1: Performs bitwise or of vX and vY, and stores the result in vX + // TODO: Test with authentic flag set + #[test] + fn or() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b) = (word as u8, (word >> 4) as u8); + let expected_result = a | b; + for reg in 0..=0xff { + let (x, y) = (reg & 0xf, reg >> 4); + // set the registers under test to a, b + (cpu.v[x], cpu.v[y]) = (a, b); + + // do the thing + cpu.or(x, y); + + // validate the result + assert_eq!(cpu.v[x], if x == y { b } else { expected_result }); + } + } + } + + /// 8xy2: Performs bitwise and of vX and vY, and stores the result in vX + // TODO: Test with authentic flag set + #[test] + fn and() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b) = (word as u8, (word >> 4) as u8); + let expected_result = a & b; + for reg in 0..=0xff { + let (x, y) = (reg & 0xf, reg >> 4); + // set the registers under test to a, b + (cpu.v[x], cpu.v[y]) = (a, b); + + // do the thing + cpu.and(x, y); + + // validate the result + assert_eq!(cpu.v[x], if x == y { b } else { expected_result }); + } + } + } + + /// 8xy3: Performs bitwise xor of vX and vY, and stores the result in vX + // TODO: Test with authentic flag set + #[test] + fn xor() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b) = (word as u8, (word >> 4) as u8); + let expected_result = a ^ b; + for reg in 0..=0xff { + let (x, y) = (reg & 0xf, reg >> 4); + // set the registers under test to a, b + (cpu.v[x], cpu.v[y]) = (a, b); + + // do the thing + cpu.xor(x, y); + + // validate the result + assert_eq!(cpu.v[x], if x == y { 0 } else { expected_result }); + } + } + } + + /// 8xy4: Performs addition of vX and vY, and stores the result in vX, carry in vF + /// If X is F, *only* stores borrow + #[test] + fn add() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b) = (word as u8, (word >> 4) as u8); + for reg in 0..=0xff { + let (x, y) = (reg & 0xf, reg >> 4); + // calculate the expected result + // If x == y, a is discarded + let (expected, carry) = if x == y { b } else { a }.overflowing_add(b); + // set the registers under test to a, b + (cpu.v[x], cpu.v[y]) = (a, b); + + // do the thing + cpu.add(x, y); + + // validate the result + // if the destination is vF, the result was discarded, and only the carry was kept + if x != 0xf { + assert_eq!(cpu.v[x], expected); + } + assert_eq!(cpu.v[0xf], carry.into()); + } + } + } + + /// 8xy5: Performs subtraction of vX and vY, and stores the result in vX, borrow in vF + #[test] + fn sub() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b) = (word as u8, (word >> 4) as u8); + for reg in 0..=0xff { + let (x, y) = (reg & 0xf, reg >> 4); + // calculate the expected result + let (expected, carry) = if x == y { b } else { a }.overflowing_sub(b); + // set the registers under test to a, b + (cpu.v[x], cpu.v[y]) = (a, b); + + // do the thing + cpu.sub(x, y); + + // validate the result + // if the destination is vF, the result was discarded, and only the carry was kept + if x != 0xf { + assert_eq!(cpu.v[x], expected); + } + // The borrow flag for subtraction is inverted + assert_eq!(cpu.v[0xf], (!carry).into()); + } + } + } + + /// 8xy6: Performs bitwise right shift of vX, stores carry-out in vF + // TODO: Test with authentic flag set + #[test] + fn shift_right() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xff { + for x in 0..=0xf { + // set the register under test to `word` + cpu.v[x] = word; + // do the thing + cpu.shift_right(x, x); + + // validate the result + // if the destination is vF, the result was discarded, and only the carry was kept + if x != 0xf { + assert_eq!(cpu.v[x], word >> 1); + } + // The borrow flag for subtraction is inverted + assert_eq!(cpu.v[0xf], word & 1); + } + } + } + + /// 8xy7: Performs subtraction of vY and vX, and stores the result in vX and ~carry in vF + #[test] + fn backwards_sub() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xffff { + let (a, b) = (word as u8, (word >> 4) as u8); + for reg in 0..=0xff { + let (x, y) = (reg & 0xf, reg >> 4); + // calculate the expected result + let (expected, carry) = if x == y { a } else { b }.overflowing_sub(a); + // set the registers under test to a, b + (cpu.v[x], cpu.v[y]) = (a, b); + + // do the thing + cpu.backwards_sub(x, y); + + // validate the result + // if the destination is vF, the result was discarded, and only the carry was kept + if x != 0xf { + assert_eq!(cpu.v[x], expected); + } + // The borrow flag for subtraction is inverted + assert_eq!(cpu.v[0xf], (!carry).into()); + } + } + } + + /// 8X_E: Performs bitwise left shift of vX + // TODO: Test with authentic flag set + #[test] + fn shift_left() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xff { + for x in 0..=0xf { + // set the register under test to `word` + cpu.v[x] = word; + // do the thing + cpu.shift_left(x, x); + + // validate the result + // if the destination is vF, the result was discarded, and only the carry was kept + if x != 0xf { + assert_eq!(cpu.v[x], word << 1); + } + // The borrow flag for subtraction is inverted + assert_eq!(cpu.v[0xf], word >> 7); + } } } } -/// Fx18: Load vX into ST -/// ```py -/// ST = vX; -/// ``` -#[test] -fn load_sound_timer() { - let (mut cpu, _) = setup_environment(); - for word in 0..=0xff { - for x in 0..=0xf { - // set the register under test to `word` - cpu.v[x] = word; - // do the thing - cpu.load_sound_timer(x); - // validate the result - assert_eq!(cpu.sound, word); +/// Test operations on the index/indirect register, I +mod i { + use super::*; + /// Aadr: Load address #adr into register I + #[test] + fn load_i_immediate() { + let (mut cpu, _) = setup_environment(); + for addr in 0..0x1000 { + // Load indirect register + cpu.load_i_immediate(addr); + // Validate register set to addr + assert_eq!(cpu.i, addr); + } + } + + /// Fx1e: Add vX to I + #[test] + fn add_i() { + let (mut cpu, _) = setup_environment(); + // For every valid address + for addr in 0..0x1000 { + // For every valid offset + for x in 0..=0xfff { + let (x, byte) = (x >> 8, x as u8); + // set v[x] = byte + (cpu.i, cpu.v[x]) = (addr as u16, byte); + // add vX to indirect register + cpu.add_i(x); + // Validate register set + assert_eq!(cpu.i, (addr + byte as usize) as u16) + } } } } -/// Fx1e: Add vX to I, -/// ```py -/// I += vX; -/// ``` -#[test] -fn add_to_indirect() { - let (mut cpu, _) = setup_environment(); - // For every valid address - for addr in 0..0x1000 { - // For every valid offset - for x in 0..=0xfff { - let (x, byte) = (x >> 8, x as u8); - // set v[x] = byte - (cpu.i, cpu.v[x]) = (addr as u16, byte); - // add vX to indirect register - cpu.add_to_indirect(x); - // Validate register set - assert_eq!(cpu.i, (addr + byte as usize) as u16) +/// Screen, buttons, other things that would be peripherals on a real architecture +/// # Includes: +/// - Random number generation +/// - Drawing to the display +mod io { + use std::io::Write; + + use super::*; + /// Cxbb: Stores a random number & the provided byte into vX + //#[test] + #[allow(dead_code)] + fn rand() { + todo!() + } + + mod display { + use super::*; + struct ScreenTest { + program: &'static [u8], + screen: &'static [u8], + steps: usize, + rate: usize, + } + + const SCREEN_TESTS: [ScreenTest; 4] = [ + // Passing BC_test + ScreenTest { + program: include_bytes!("../../chip-8/BC_test.ch8"), + screen: include_bytes!("tests/BC_test.ch8_197.bin"), + steps: 197, + rate: 8, + }, + // The IBM Logo + ScreenTest { + program: include_bytes!("../../chip-8/IBM Logo.ch8"), + screen: include_bytes!("tests/IBM Logo.ch8_20.bin"), + steps: 20, + rate: 8, + }, + // Rule 22 cellular automata + ScreenTest { + program: include_bytes!("../../chip-8/1dcell.ch8"), + screen: include_bytes!("tests/1dcell.ch8_123342.bin"), + steps: 123342, + rate: 8, + }, + // Rule 60 cellular automata + ScreenTest { + program: include_bytes!("../../chip-8/1dcell.ch8"), + screen: include_bytes!("tests/1dcell.ch8_2391162.bin"), + steps: 2391162, + rate: 8, + }, + ]; + + /// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY) + #[test] + fn draw() { + for test in SCREEN_TESTS { + let (mut cpu, mut bus) = setup_environment(); + // Load the test program + bus = bus.load_region(Program, test.program); + // Run the test program for the specified number of steps + cpu.multistep(&mut bus, test.steps, test.rate); + // Compare the screen to the reference screen buffer + assert_eq!(bus.get_region(Screen).unwrap(), test.screen); + } + } + } + + mod cf { + //use super::*; + /// Ex9E: Skip next instruction if key == #X + //#[test] + #[allow(dead_code)] + fn skip_key_equals() { + todo!() + } + + /// ExaE: Skip next instruction if key != #X + //#[test] + #[allow(dead_code)] + fn skip_key_not_equals() { + todo!() + } + + /// Fx0A: Wait for key, then vX = K + //#[test] + #[allow(dead_code)] + fn wait_for_key() { + todo!() + } + } + + /// Fx07: Get the current DT, and put it in vX + #[test] + fn get_delay_timer() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xff { + for x in 0..=0xf { + // set the register under test to `word` + cpu.delay = word; + // do the thing + cpu.load_delay_timer(x); + // validate the result + assert_eq!(cpu.v[x], word); + } + } + } + + /// Fx15: Load vX into DT + #[test] + fn load_delay_timer() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xff { + for x in 0..=0xf { + // set the register under test to `word` + cpu.v[x] = word; + // do the thing + cpu.store_delay_timer(x); + // validate the result + assert_eq!(cpu.delay, word); + } + } + } + + /// Fx18: Load vX into ST + #[test] + fn load_sound_timer() { + let (mut cpu, _) = setup_environment(); + for word in 0..=0xff { + for x in 0..=0xf { + // set the register under test to `word` + cpu.v[x] = word; + // do the thing + cpu.store_sound_timer(x); + // validate the result + assert_eq!(cpu.sound, word); + } + } + } + + mod sprite { + use super::*; + + struct SpriteTest { + input: u8, + output: &'static [u8], + } + + #[rustfmt::skip] + const TESTS: [SpriteTest; 16] = [ + SpriteTest { input: 0x0, output: &[0xf0, 0x90, 0x90, 0x90, 0xf0] }, + SpriteTest { input: 0x1, output: &[0x20, 0x60, 0x20, 0x20, 0x70] }, + SpriteTest { input: 0x2, output: &[0xf0, 0x10, 0xf0, 0x80, 0xf0] }, + SpriteTest { input: 0x3, output: &[0xf0, 0x10, 0xf0, 0x10, 0xf0] }, + SpriteTest { input: 0x4, output: &[0x90, 0x90, 0xf0, 0x10, 0x10] }, + SpriteTest { input: 0x5, output: &[0xf0, 0x80, 0xf0, 0x10, 0xf0] }, + SpriteTest { input: 0x6, output: &[0xf0, 0x80, 0xf0, 0x90, 0xf0] }, + SpriteTest { input: 0x7, output: &[0xf0, 0x10, 0x20, 0x40, 0x40] }, + SpriteTest { input: 0x8, output: &[0xf0, 0x90, 0xf0, 0x90, 0xf0] }, + SpriteTest { input: 0x9, output: &[0xf0, 0x90, 0xf0, 0x10, 0xf0] }, + SpriteTest { input: 0xa, output: &[0xf0, 0x90, 0xf0, 0x90, 0x90] }, + SpriteTest { input: 0xb, output: &[0xe0, 0x90, 0xe0, 0x90, 0xe0] }, + SpriteTest { input: 0xc, output: &[0xf0, 0x80, 0x80, 0x80, 0xf0] }, + SpriteTest { input: 0xd, output: &[0xe0, 0x90, 0x90, 0x90, 0xe0] }, + SpriteTest { input: 0xe, output: &[0xf0, 0x80, 0xf0, 0x80, 0xf0] }, + SpriteTest { input: 0xf, output: &[0xf0, 0x80, 0xf0, 0x80, 0x80] }, + ]; + + /// Fx29: Load sprite for character vX into I + #[test] + #[allow(dead_code)] + fn load_sprite() { + let (mut cpu, bus) = setup_environment(); + for test in TESTS { + let reg = 0xf & random::(); + // load number into CPU register + cpu.v[reg] = test.input; + + cpu.load_sprite(reg); + + let addr = cpu.i as usize; + assert_eq!( + bus.get(addr..addr.wrapping_add(5)).unwrap(), + test.output, + "\nTesting load_sprite({:x}) -> {:04x} {{\n\tout:{:02x?}\n\texp:{:02x?}\n}}", + test.input, + cpu.i, + bus.get(addr..addr.wrapping_add(5)).unwrap(), + test.output, + ); + } + } + } + + mod bcdtest { + pub(self) use super::*; + + struct BCDTest { + // value to test + input: u8, + // result + output: &'static [u8], + } + + const BCD_TESTS: [BCDTest; 3] = [ + BCDTest { + input: 000, + output: &[0, 0, 0], + }, + BCDTest { + input: 255, + output: &[2, 5, 5], + }, + BCDTest { + input: 127, + output: &[1, 2, 7], + }, + ]; + + /// Fx33: BCD convert X into I`[0..3]` + #[test] + #[allow(dead_code)] + fn bcd_convert() { + for test in BCD_TESTS { + let (mut cpu, mut bus) = setup_environment(); + let addr = 0xff0 & random::() as usize; + // load CPU registers + cpu.i = addr as u16; + cpu.v[5] = test.input; + // run instruction + cpu.bcd_convert(5, &mut bus); + // validate the results + assert_eq!(bus.get(addr..addr.saturating_add(3)), Some(test.output)) + } + } + } + + /// Fx55: DMA Stor from I to registers 0..X + // TODO: Test with authentic flag unset + // TODO: Test with authentic flag set + //#[test] + #[allow(dead_code)] + fn dma_store() { + todo!() + // Load values into registers + // Perform DMA store + // Check that + } + + /// Fx65: DMA Load from I to registers 0..X + // TODO: Test with authentic flag unset + // TODO: Test with authentic flag set + #[test] + #[allow(dead_code)] + fn dma_load() { + let (mut cpu, mut bus) = setup_environment(); + const DATA: &[u8] = b"ABCDEFGHIJKLMNOP"; + // Load some test data into memory + let addr = 0x456; + bus.get_mut(addr..addr + DATA.len()) + .unwrap() + .write(DATA) + .unwrap(); + for len in 0..16 { + // Perform DMA load + cpu.i = addr as u16; + cpu.load_dma(len, &mut bus); + // Check that registers grabbed the correct data + assert_eq!(cpu.v[0..=len], DATA[0..=len]); + assert_eq!(cpu.v[len + 1..], [0; 16][len + 1..]); + // clear + cpu.v = [0; 16]; } } } - -/// Fx29: Load sprite for character vX into I -/// ```py -/// I = sprite(vX); -/// ``` -//#[test] -#[allow(dead_code)] -fn load_sprite_x() { - todo!() -} - -/// Fx33: BCD convert X into I`[0..3]` -//#[test] -#[allow(dead_code)] -fn bcd_convert_i() { - todo!() -} - -/// Fx55: DMA Stor from I to registers 0..X -//#[test] -#[allow(dead_code)] -fn dma_store() { - todo!() - // Load values into registers - // Perform DMA store - // Check that -} - -/// Fx65: DMA Load from I to registers 0..X -//#[test] -#[allow(dead_code)] -fn dma_load() { - todo!() - // Perform DMA load - // Check that registers grabbed the correct data -}