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 TestEveryOpcodeIsDefined(t *testing.T) { for op := 0; op < 256; op++ { func() { defer func() { if r := recover(); r != nil { t.Errorf("opcode 0x%02X panics: %v", op, r) } }() c, _ := newTestCPU([]uint8{uint8(op), 0x00, 0x00}) c.Step() }() } } func TestJamStopsCPU(t *testing.T) { c, _ := newTestCPU([]uint8{0xA9, 0x42, 0x02, 0xEA}) c.Step() c.Step() if !c.Jammed { t.Fatal("Jammed = false, want true") } if c.PC != 0x8002 { t.Fatalf("PC = 0x%04X, want 0x8002", c.PC) } for i := 0; i < 3; i++ { if cycles := c.Step(); cycles != 1 { t.Fatalf("cycles = %d, want 1", cycles) } } if c.PC != 0x8002 || c.A != 0x42 { t.Fatalf("PC = 0x%04X, A = 0x%02X; want 0x8002, 0x42", c.PC, c.A) } } func TestJammedIgnoresInterruptsUntilReset(t *testing.T) { c, bus := newTestCPU([]uint8{0x02}) bus.mem[0xFFFA], bus.mem[0xFFFB] = 0x00, 0x90 c.Step() c.NMI() if c.PC != 0x8000 { t.Fatalf("PC after NMI = 0x%04X, want 0x8000", c.PC) } c.Reset() if c.Jammed { t.Fatal("Jammed = true after Reset, want false") } } func runSteps(prog []uint8, steps int) (*cpu.CPU, *ramBus) { c, bus := newTestCPU(prog) for i := 0; i < steps; i++ { c.Step() } return c, bus } func TestANC(t *testing.T) { for _, op := range []uint8{0x0B, 0x2B} { c, _ := runSteps([]uint8{0xA9, 0xFF, op, 0x80}, 2) // lda #$ff ; anc #$80 if c.A != 0x80 || !has(c, cpu.FlagC) || !has(c, cpu.FlagN) || has(c, cpu.FlagZ) { t.Errorf("op 0x%02X: A = 0x%02X, P = 0x%02X; want A=0x80, C and N set", op, c.A, c.Status) } } } func TestALR(t *testing.T) { c, _ := runSteps([]uint8{0xA9, 0xFF, 0x4B, 0x03}, 2) // lda #$ff ; alr #$03 if c.A != 0x01 || !has(c, cpu.FlagC) || has(c, cpu.FlagN) || has(c, cpu.FlagZ) { t.Errorf("A = 0x%02X, P = 0x%02X; want A=0x01, C set", c.A, c.Status) } } func TestARR(t *testing.T) { tests := []struct { name string prog []uint8 wantA uint8 c, v bool }{ {"carry in, bits 6 and 5 set", []uint8{0x38, 0xA9, 0xFF, 0x6B, 0xFF}, 0xFF, true, false}, {"no carry in, bit 6 only", []uint8{0x18, 0xA9, 0xFF, 0x6B, 0x80}, 0x40, true, true}, } for _, tt := range tests { c, _ := runSteps(tt.prog, 3) if c.A != tt.wantA || has(c, cpu.FlagC) != tt.c || has(c, cpu.FlagV) != tt.v { t.Errorf("%s: A = 0x%02X, P = 0x%02X; want A=0x%02X, C=%v, V=%v", tt.name, c.A, c.Status, tt.wantA, tt.c, tt.v) } } } func TestAXS(t *testing.T) { // lda #$f0 ; ldx #$3c ; axs #imm (a & x = 0x30) c, _ := runSteps([]uint8{0xA9, 0xF0, 0xA2, 0x3C, 0xCB, 0x10}, 3) if c.X != 0x20 || !has(c, cpu.FlagC) { t.Errorf("X = 0x%02X, P = 0x%02X; want X=0x20, C set", c.X, c.Status) } c, _ = runSteps([]uint8{0xA9, 0xF0, 0xA2, 0x3C, 0xCB, 0x40}, 3) if c.X != 0xF0 || has(c, cpu.FlagC) || !has(c, cpu.FlagN) { t.Errorf("X = 0x%02X, P = 0x%02X; want X=0xF0, C clear, N set", c.X, c.Status) } } func TestLAS(t *testing.T) { c, bus := newTestCPU([]uint8{0xA0, 0x01, 0xBB, 0x00, 0x02}) // ldy #1 ; las $0200,y bus.mem[0x0201] = 0xF0 c.Step() if cycles := c.Step(); cycles != 4 { t.Errorf("cycles = %d, want 4", cycles) } // 0xF0 & SP(0xFD) = 0xF0 if c.A != 0xF0 || c.X != 0xF0 || c.SP != 0xF0 || !has(c, cpu.FlagN) { t.Errorf("A = 0x%02X, X = 0x%02X, SP = 0x%02X; want all 0xF0, N set", c.A, c.X, c.SP) } } func TestANE(t *testing.T) { // lda #$01 ; ldx #$0f ; ane #$ff -> (0x01 | 0xEE) & 0x0F & 0xFF c, _ := runSteps([]uint8{0xA9, 0x01, 0xA2, 0x0F, 0x8B, 0xFF}, 3) if c.A != 0x0F { t.Errorf("A = 0x%02X, want 0x0F", c.A) } } func TestLXA(t *testing.T) { // lda #$01 ; lxa #$f0 -> (0x01 | 0xEE) & 0xF0 c, _ := runSteps([]uint8{0xA9, 0x01, 0xAB, 0xF0}, 2) if c.A != 0xE0 || c.X != 0xE0 || !has(c, cpu.FlagN) { t.Errorf("A = 0x%02X, X = 0x%02X; want both 0xE0, N set", c.A, c.X) } } func TestStoreHighAnd(t *testing.T) { tests := []struct { name string prog []uint8 zp bool // pointer $0200 at $10 for the (zp),y form steps int addr uint16 want uint8 }{ // the stored value is reg & (high byte of the base + 1) = reg & 0x03 {"shx", []uint8{0xA2, 0xFF, 0xA0, 0x01, 0x9E, 0x00, 0x02}, false, 3, 0x0201, 0x03}, {"shy", []uint8{0xA0, 0xFF, 0xA2, 0x01, 0x9C, 0x00, 0x02}, false, 3, 0x0201, 0x03}, {"ahx abs,y", []uint8{0xA9, 0xFF, 0xA2, 0x0F, 0xA0, 0x01, 0x9F, 0x00, 0x02}, false, 4, 0x0201, 0x03}, {"ahx (zp),y", []uint8{0xA9, 0xFF, 0xA2, 0x0F, 0xA0, 0x01, 0x93, 0x10}, true, 4, 0x0201, 0x03}, {"tas", []uint8{0xA9, 0xFF, 0xA2, 0x07, 0xA0, 0x01, 0x9B, 0x00, 0x02}, false, 4, 0x0201, 0x03}, // $02ff + 1 crosses a page: the value (X & 0x03 = 1) becomes the new high byte {"shx page cross", []uint8{0xA2, 0x01, 0xA0, 0x01, 0x9E, 0xFF, 0x02}, false, 3, 0x0100, 0x01}, } for _, tt := range tests { c, bus := newTestCPU(tt.prog) if tt.zp { bus.mem[0x10], bus.mem[0x11] = 0x00, 0x02 } for i := 0; i < tt.steps; i++ { c.Step() } if got := bus.mem[tt.addr]; got != tt.want { t.Errorf("%s: mem[0x%04X] = 0x%02X, want 0x%02X", tt.name, tt.addr, got, tt.want) } } } func TestTASSetsStackPointer(t *testing.T) { // lda #$ff ; ldx #$07 ; ldy #$01 ; tas $0200,y c, _ := runSteps([]uint8{0xA9, 0xFF, 0xA2, 0x07, 0xA0, 0x01, 0x9B, 0x00, 0x02}, 4) if c.SP != 0x07 { t.Errorf("SP = 0x%02X, want 0x07", c.SP) } }