diff options
Diffstat (limited to 'tests/cpu_test.go')
| -rw-r--r-- | tests/cpu_test.go | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/tests/cpu_test.go b/tests/cpu_test.go new file mode 100644 index 0000000..87743b1 --- /dev/null +++ b/tests/cpu_test.go @@ -0,0 +1,237 @@ +package tests + +import ( + "testing" + + "crecher.net/gones/v2/cpu" +) + +type ramBus struct { + mem [65536]uint8 +} + +func (b *ramBus) Read(addr uint16) uint8 { return b.mem[addr] } +func (b *ramBus) Write(addr uint16, v uint8) { b.mem[addr] = v } + +func has(c *cpu.CPU, flag uint8) bool { return c.Status&flag != 0 } + +func newTestCPU(prog []uint8) (*cpu.CPU, *ramBus) { + bus := &ramBus{} + copy(bus.mem[0x8000:], prog) + bus.mem[0xFFFC] = 0x00 + bus.mem[0xFFFD] = 0x80 + c := cpu.NewCPU(bus) + c.Reset() + return c, bus +} + +func TestResetVector(t *testing.T) { + c, _ := newTestCPU([]uint8{0xEA}) + if c.PC != 0x8000 { + t.Fatalf("PC = 0x%04X, want 0x8000", c.PC) + } + if c.SP != 0xFD { + t.Fatalf("SP = 0x%02X, want 0xFD", c.SP) + } +} + +func TestLDAImmediate(t *testing.T) { + c, _ := newTestCPU([]uint8{0xA9, 0x42}) + cycles := c.Step() + if c.A != 0x42 { + t.Fatalf("A = 0x%02X, want 0x42", c.A) + } + if cycles != 2 { + t.Fatalf("cycles = %d, want 2", cycles) + } + if has(c, cpu.FlagZ) || has(c, cpu.FlagN) { + t.Fatalf("unexpected flags: 0x%02X", c.Status) + } +} + +func TestLDAZeroAndNegativeFlags(t *testing.T) { + c, _ := newTestCPU([]uint8{0xA9, 0x00}) + c.Step() + if !has(c, cpu.FlagZ) { + t.Fatalf("Z flag should be set for A=0") + } + + c, _ = newTestCPU([]uint8{0xA9, 0x80}) + c.Step() + if !has(c, cpu.FlagN) { + t.Fatalf("N flag should be set for A=0x80") + } +} + +func TestSTAAbsolute(t *testing.T) { + c, bus := newTestCPU([]uint8{0xA9, 0x55, 0x8D, 0x00, 0x02}) + c.Step() + c.Step() + if bus.mem[0x0200] != 0x55 { + t.Fatalf("mem[0x0200] = 0x%02X, want 0x55", bus.mem[0x0200]) + } +} + +func TestAbsoluteXPageCross(t *testing.T) { + // 0x80FF + X(1): page cross, 5 cycles + c, bus := newTestCPU([]uint8{0xA2, 0x01, 0xBD, 0xFF, 0x80}) + bus.mem[0x8100] = 0x99 + c.Step() + cycles := c.Step() + if c.A != 0x99 { + t.Fatalf("A = 0x%02X, want 0x99", c.A) + } + if cycles != 5 { + t.Fatalf("cycles = %d, want 5 (4 base + 1 page cross)", cycles) + } +} + +func TestAdcCarryAndOverflow(t *testing.T) { + // 0x50 + 0x50 = 0xA0, V=1, C=0 + c, _ := newTestCPU([]uint8{0xA9, 0x50, 0x69, 0x50}) + c.Step() + c.Step() + if c.A != 0xA0 { + t.Fatalf("A = 0x%02X, want 0xA0", c.A) + } + if !has(c, cpu.FlagV) { + t.Fatalf("V flag should be set") + } + if has(c, cpu.FlagC) { + t.Fatalf("C flag should be clear") + } + + // 0xFF + 0x01 = 0x00, C=1, V=0 + c, _ = newTestCPU([]uint8{0xA9, 0xFF, 0x69, 0x01}) + c.Step() + c.Step() + if c.A != 0x00 { + t.Fatalf("A = 0x%02X, want 0x00", c.A) + } + if !has(c, cpu.FlagC) { + t.Fatalf("C flag should be set") + } + if !has(c, cpu.FlagZ) { + t.Fatalf("Z flag should be set") + } + if has(c, cpu.FlagV) { + t.Fatalf("V flag should be clear") + } +} + +func TestSbcBorrow(t *testing.T) { + // 0x10 - 0x01 = 0x0F, C=1 + c, _ := newTestCPU([]uint8{0x38, 0xA9, 0x10, 0xE9, 0x01}) + c.Step() + c.Step() + c.Step() + if c.A != 0x0F { + t.Fatalf("A = 0x%02X, want 0x0F", c.A) + } + if !has(c, cpu.FlagC) { + t.Fatalf("C flag should be set (no borrow)") + } +} + +func TestBranchTakenAndPageCross(t *testing.T) { + // beq taken, same page: 3 cycles + c, _ := newTestCPU([]uint8{0xA9, 0x00, 0xF0, 0x02, 0xEA, 0xEA, 0xEA}) + c.Step() + cycles := c.Step() + if cycles != 3 { + t.Fatalf("cycles = %d, want 3 (2 base + 1 taken)", cycles) + } + if c.PC != 0x8006 { + t.Fatalf("PC = 0x%04X, want 0x8006", c.PC) + } +} + +func TestJsrRts(t *testing.T) { + prog := []uint8{0x20, 0x04, 0x80, 0xEA, 0x60} + c, _ := newTestCPU(prog) + c.Step() + if c.PC != 0x8004 { + t.Fatalf("PC after JSR = 0x%04X, want 0x8004", c.PC) + } + c.Step() + if c.PC != 0x8003 { + t.Fatalf("PC after RTS = 0x%04X, want 0x8003", c.PC) + } +} + +func TestStackPushPullRoundTrip(t *testing.T) { + c, _ := newTestCPU([]uint8{0xA9, 0x7E, 0x48, 0xA9, 0x00, 0x68}) + c.Step() + c.Step() + c.Step() + c.Step() + if c.A != 0x7E { + t.Fatalf("A after PLA = 0x%02X, want 0x7E", c.A) + } +} + +func TestPhpPlpPreservesFlags(t *testing.T) { + c, _ := newTestCPU([]uint8{0x38, 0x08, 0x18, 0x28}) + c.Step() + c.Step() + c.Step() + if has(c, cpu.FlagC) { + t.Fatalf("C flag should be clear after CLC") + } + c.Step() + if !has(c, cpu.FlagC) { + t.Fatalf("C flag should be restored by PLP") + } +} + +func TestIndirectJmpPageBug(t *testing.T) { + // jmp ($30FF) reads high byte from $3000 + c, bus := newTestCPU([]uint8{0x6C, 0xFF, 0x30}) + bus.mem[0x30FF] = 0x34 + bus.mem[0x3000] = 0x12 + bus.mem[0x3100] = 0x99 + c.Step() + if c.PC != 0x1234 { + t.Fatalf("PC = 0x%04X, want 0x1234", c.PC) + } +} + +func TestNmiPushesStateAndJumps(t *testing.T) { + c, bus := newTestCPU([]uint8{0xEA}) + bus.mem[0xFFFA] = 0x00 + bus.mem[0xFFFB] = 0x90 + pcBefore := c.PC + spBefore := c.SP + c.NMI() + if c.PC != 0x9000 { + t.Fatalf("PC after NMI = 0x%04X, want 0x9000", c.PC) + } + if c.SP != spBefore-3 { + t.Fatalf("SP after NMI = 0x%02X, want 0x%02X", c.SP, spBefore-3) + } + stack := func(sp uint8) uint16 { return uint16(bus.mem[0x0100+uint16(sp)]) } + pc := stack(spBefore)<<8 | stack(spBefore-1) + if pc != pcBefore { + t.Fatalf("pushed PC = 0x%04X, want 0x%04X", pc, pcBefore) + } +} + +func TestCompareFlags(t *testing.T) { + // Z=1, C=1, N=0 + c, _ := newTestCPU([]uint8{0xA9, 0x10, 0xC9, 0x10}) + c.Step() + c.Step() + if !has(c, cpu.FlagZ) || !has(c, cpu.FlagC) || has(c, cpu.FlagN) { + t.Fatalf("flags = 0x%02X, want Z and C set, N clear", c.Status) + } +} + +func TestUnimplementedOpcodePanics(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("expected panic for illegal opcode") + } + }() + c, _ := newTestCPU([]uint8{0x02}) + c.Step() +} |