442 lines
14 KiB
Rust
442 lines
14 KiB
Rust
use ferris_boi::{
|
|
cpu::{instructions::Instruction, Processor},
|
|
register::{self, Flag},
|
|
};
|
|
use test_case::{test_case, test_matrix};
|
|
|
|
#[test]
|
|
fn test_can_jump_to_immediate() {
|
|
let mut processor = Processor::default();
|
|
|
|
let data = [0xC3, 0x37, 0x13, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(0x1337, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_jump_to_address_in_hl() {
|
|
let mut processor = Processor::default();
|
|
processor
|
|
.registers
|
|
.set_combined_register(register::Combined::HL, 0x1337);
|
|
|
|
let data = [0xE9, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(0x1337, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test]
|
|
fn test_call_adjusts_program_counter_to_given_value() {
|
|
let mut processor = Processor::default();
|
|
|
|
let data = [0xCD, 0x37, 0x13, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(0x1337, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test]
|
|
fn test_call_pushes_pc_onto_stack() {
|
|
let mut processor = Processor::default();
|
|
processor.registers.program_counter = 0xBEEF;
|
|
|
|
let data = [0xCD, 0x37, 0x13, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
let old_sp = processor.registers.stack_pointer;
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(0xBE, processor.memory.get((old_sp - 1).into()).unwrap());
|
|
// HACK: This SHOULD be F2 in reality (0xEF + 3, the size of the call instruction)
|
|
// however since run_instruction does not advance the PC, we need to adjust our test
|
|
assert_eq!(0xEF, processor.memory.get((old_sp - 2).into()).unwrap());
|
|
assert_eq!(old_sp - 2, processor.registers.stack_pointer);
|
|
}
|
|
|
|
#[test_case(0xC7, 0x00)]
|
|
#[test_case(0xCF, 0x08)]
|
|
#[test_case(0xD7, 0x10)]
|
|
#[test_case(0xDF, 0x18)]
|
|
#[test_case(0xE7, 0x20)]
|
|
#[test_case(0xEF, 0x28)]
|
|
#[test_case(0xF7, 0x30)]
|
|
#[test_case(0xFF, 0x38)]
|
|
fn test_restart_adjusts_program_counter_to_encoded_value(opcode: u8, addr: u8) {
|
|
let mut processor = Processor::default();
|
|
|
|
let data = [opcode, 0x13];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x13]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(u16::from(addr), processor.registers.program_counter);
|
|
}
|
|
|
|
#[test_case(0xC7)]
|
|
#[test_case(0xCF)]
|
|
#[test_case(0xD7)]
|
|
#[test_case(0xDF)]
|
|
#[test_case(0xE7)]
|
|
#[test_case(0xEF)]
|
|
#[test_case(0xF7)]
|
|
#[test_case(0xFF)]
|
|
fn test_restart_adjusts_pushes_current_program_counter_to_stack(opcode: u8) {
|
|
let mut processor = Processor::default();
|
|
processor.registers.program_counter = 0xDEAD;
|
|
|
|
let data = [opcode, 0x13];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x13]);
|
|
|
|
let old_sp = processor.registers.stack_pointer;
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(0xDE, processor.memory.get((old_sp - 1).into()).unwrap());
|
|
// HACK: This SHOULD be AE in reality (0xAD + 1, the size of the rst instruction)
|
|
// however since run_instruction does not advance the PC, we need to adjust our test
|
|
assert_eq!(0xAD, processor.memory.get((old_sp - 2).into()).unwrap());
|
|
assert_eq!(old_sp - 2, processor.registers.stack_pointer);
|
|
}
|
|
|
|
#[test_case(0xCC, Flag::Zero, 1; "CALL Z")]
|
|
#[test_case(0xDC, Flag::Carry, 1; "CALL C")]
|
|
#[test_case(0xC4, Flag::Zero, 0; "CALL NZ")]
|
|
#[test_case(0xD4, Flag::Carry, 0; "CALL NC")]
|
|
#[test_case(0xCA, Flag::Zero, 1; "JP Z")]
|
|
#[test_case(0xDA, Flag::Carry, 1; "JP C")]
|
|
#[test_case(0xC2, Flag::Zero, 0; "JP NZ")]
|
|
#[test_case(0xD2, Flag::Carry, 0; "JP NC")]
|
|
fn test_call_and_jump_jumps_if_condition_matches(opcode: u8, flag: Flag, expected_value: u8) {
|
|
let mut processor = Processor::default();
|
|
processor.registers.set_flag_bit(flag, expected_value);
|
|
|
|
let data = [opcode, 0x37, 0x13, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(0x1337, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test_case(0xCC, Flag::Zero, 1; "CALL Z")]
|
|
#[test_case(0xDC, Flag::Carry, 1; "CALL C")]
|
|
#[test_case(0xC4, Flag::Zero, 0; "CALL NZ")]
|
|
#[test_case(0xD4, Flag::Carry, 0; "CALL NC")]
|
|
fn test_call_grows_stack_if_condition_matches(opcode: u8, flag: Flag, expected_value: u8) {
|
|
let mut processor = Processor::default();
|
|
processor.registers.set_flag_bit(flag, expected_value);
|
|
|
|
let data = [opcode, 0x37, 0x13, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
let old_sp = processor.registers.stack_pointer;
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(old_sp - 2, processor.registers.stack_pointer);
|
|
}
|
|
|
|
#[test_case(0xCC, Flag::Zero, 1; "CALL Z")]
|
|
#[test_case(0xDC, Flag::Carry, 1; "CALL C")]
|
|
#[test_case(0xC4, Flag::Zero, 0; "CALL NZ")]
|
|
#[test_case(0xD4, Flag::Carry, 0; "CALL NC")]
|
|
#[test_case(0xCA, Flag::Zero, 1; "JP Z")]
|
|
#[test_case(0xDA, Flag::Carry, 1; "JP C")]
|
|
#[test_case(0xC2, Flag::Zero, 0; "JP NZ")]
|
|
#[test_case(0xD2, Flag::Carry, 0; "JP NC")]
|
|
fn test_call_and_jump_does_nothing_if_condition_fails(opcode: u8, flag: Flag, expected_value: u8) {
|
|
let mut processor = Processor::default();
|
|
processor
|
|
.registers
|
|
.set_flag_bit(flag, if expected_value == 0 { 1 } else { 0 });
|
|
|
|
let data = [opcode, 0x37, 0x13, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
let old_sp = processor.registers.stack_pointer;
|
|
let old_pc = processor.registers.program_counter;
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(old_sp, processor.registers.stack_pointer);
|
|
// run_instruction does not advance the program counter, so this will be the same. Normally it would be old_pc plus 3
|
|
assert_eq!(old_pc, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test]
|
|
fn test_jump_to_same_addr_does_not_advance_pc() {
|
|
let mut processor = Processor::default();
|
|
processor.registers.program_counter = 0x1337;
|
|
|
|
[0xC3, 0x37, 0x13]
|
|
.iter()
|
|
.copied()
|
|
.enumerate()
|
|
.for_each(|(idx, opcode)| {
|
|
processor
|
|
.memory
|
|
.set(
|
|
usize::from(processor.registers.program_counter) + idx,
|
|
opcode,
|
|
)
|
|
.expect("could not program data");
|
|
});
|
|
|
|
processor.run_next_instruction();
|
|
assert_eq!(0x1337, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test_case(10)]
|
|
#[test_case(0)]
|
|
#[test_case(-8)]
|
|
fn test_jump_relative_jumps_by_the_given_number_of_bytes(distance: i8) {
|
|
let mut processor = Processor::default();
|
|
|
|
let starting_pc = processor.registers.program_counter;
|
|
let data = [0x18, i8::to_be_bytes(distance)[0], 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
let expected_pc = if distance > 0 {
|
|
starting_pc + u16::from(distance.unsigned_abs())
|
|
} else {
|
|
starting_pc - u16::from(distance.unsigned_abs())
|
|
};
|
|
|
|
assert_eq!(expected_pc, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test_matrix(
|
|
[
|
|
(0x20, Flag::Zero, 0),
|
|
(0x30, Flag::Carry, 0),
|
|
(0x28, Flag::Zero, 1),
|
|
(0x38, Flag::Carry, 1),
|
|
],
|
|
[10, 0, -8]
|
|
)]
|
|
fn test_jump_relative_jumps_by_the_given_number_of_bytes_if_condition_matches(
|
|
(opcode, flag, flag_value): (u8, Flag, u8),
|
|
distance: i8,
|
|
) {
|
|
let mut processor = Processor::default();
|
|
processor.registers.set_flag_bit(flag, flag_value);
|
|
|
|
let starting_pc = processor.registers.program_counter;
|
|
let data = [opcode, i8::to_be_bytes(distance)[0], 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
let expected_pc = if distance > 0 {
|
|
starting_pc + u16::from(distance.unsigned_abs())
|
|
} else {
|
|
starting_pc - u16::from(distance.unsigned_abs())
|
|
};
|
|
|
|
assert_eq!(expected_pc, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test_matrix(
|
|
[
|
|
(0x20, Flag::Zero, 0),
|
|
(0x30, Flag::Carry, 0),
|
|
(0x28, Flag::Zero, 1),
|
|
(0x38, Flag::Carry, 1),
|
|
],
|
|
[10, 0, -8]
|
|
)]
|
|
fn test_jump_relative_does_nothing_if_condition_does_not_match(
|
|
(opcode, flag, flag_value): (u8, Flag, u8),
|
|
distance: i8,
|
|
) {
|
|
let mut processor = Processor::default();
|
|
processor
|
|
.registers
|
|
.set_flag_bit(flag, if flag_value == 0 { 1 } else { 0 });
|
|
|
|
let starting_pc = processor.registers.program_counter;
|
|
let data = [opcode, i8::to_be_bytes(distance)[0], 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
// Would normally advance, but run_instruction does not advance PC
|
|
assert_eq!(starting_pc, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test_case(0xC9; "RET")]
|
|
#[test_case(0xD9; "RETI")]
|
|
fn test_ret_jumps_to_address_on_stack(opcode: u8) {
|
|
let mut processor = Processor::default();
|
|
|
|
let starting_sp = processor.registers.stack_pointer;
|
|
processor
|
|
.memory
|
|
.set_both((
|
|
((starting_sp - 1).into(), 0x13),
|
|
((starting_sp - 2).into(), 0x37),
|
|
))
|
|
.expect("failed to write to stack");
|
|
|
|
processor.registers.stack_pointer -= 2;
|
|
|
|
let data = [opcode, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(0x1337, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test_case(0xC9; "RET")]
|
|
#[test_case(0xD9; "RETI")]
|
|
fn test_ret_moves_sp_two_bytes(opcode: u8) {
|
|
let mut processor = Processor::default();
|
|
|
|
let starting_sp = processor.registers.stack_pointer;
|
|
processor
|
|
.memory
|
|
.set_both((
|
|
((starting_sp - 1).into(), 0x13),
|
|
((starting_sp - 2).into(), 0x37),
|
|
))
|
|
.expect("failed to write to stack");
|
|
|
|
processor.registers.stack_pointer -= 2;
|
|
|
|
let data = [opcode, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(starting_sp, processor.registers.stack_pointer);
|
|
}
|
|
|
|
#[test]
|
|
fn test_reti_enables_interrupts_immediately() {
|
|
let mut processor = Processor::default();
|
|
|
|
let starting_sp = processor.registers.stack_pointer;
|
|
processor
|
|
.memory
|
|
.set_both((
|
|
((starting_sp - 1).into(), 0x13),
|
|
((starting_sp - 2).into(), 0x37),
|
|
))
|
|
.expect("failed to write to stack");
|
|
|
|
processor.disable_interrupts();
|
|
|
|
processor.registers.stack_pointer -= 2;
|
|
|
|
let data = [0xD9, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert!(processor.interrupts_enabled());
|
|
}
|
|
|
|
#[test_case(0xC8, Flag::Zero, 1; "RET Z")]
|
|
#[test_case(0xD8, Flag::Carry, 1; "RET C")]
|
|
#[test_case(0xC0, Flag::Zero, 0; "RET NZ")]
|
|
#[test_case(0xD0, Flag::Carry, 0; "RET NC")]
|
|
fn test_conditional_ret_jumps_to_address_on_stack(opcode: u8, flag: Flag, expected_value: u8) {
|
|
let mut processor = Processor::default();
|
|
processor.registers.set_flag_bit(flag, expected_value);
|
|
|
|
let starting_sp = processor.registers.stack_pointer;
|
|
processor
|
|
.memory
|
|
.set_both((
|
|
((starting_sp - 1).into(), 0xBE),
|
|
((starting_sp - 2).into(), 0xEF),
|
|
))
|
|
.expect("failed to write to stack");
|
|
|
|
processor.registers.stack_pointer -= 2;
|
|
|
|
let data = [opcode, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
assert_eq!(0xBEEF, processor.registers.program_counter);
|
|
}
|
|
|
|
#[test_case(0xC8, Flag::Zero, 1; "RET Z")]
|
|
#[test_case(0xD8, Flag::Carry, 1; "RET C")]
|
|
#[test_case(0xC0, Flag::Zero, 0; "RET NZ")]
|
|
#[test_case(0xD0, Flag::Carry, 0; "RET NC")]
|
|
fn test_conditional_ret_does_nothing_if_condition_fails(
|
|
opcode: u8,
|
|
flag: Flag,
|
|
expected_value: u8,
|
|
) {
|
|
let mut processor = Processor::default();
|
|
processor
|
|
.registers
|
|
.set_flag_bit(flag, if expected_value == 0 { 1 } else { 0 });
|
|
|
|
let starting_pc = processor.registers.program_counter;
|
|
let starting_sp = processor.registers.stack_pointer;
|
|
processor
|
|
.memory
|
|
.set_both((
|
|
((starting_sp - 1).into(), 0xBE),
|
|
((starting_sp - 2).into(), 0xEF),
|
|
))
|
|
.expect("failed to write to stack");
|
|
|
|
processor.registers.stack_pointer -= 2;
|
|
|
|
let data = [opcode, 0x06];
|
|
let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction");
|
|
|
|
assert_eq!(extra_data, &[0x06]);
|
|
|
|
processor.run_instruction(ins);
|
|
|
|
// run_instruction does not advance the program_counter. Normally this would add one
|
|
assert_eq!(starting_pc, processor.registers.program_counter);
|
|
assert_eq!(starting_sp - 2, processor.registers.stack_pointer);
|
|
}
|