package cpu import ( "bufio" "os" "regexp" "strconv" "testing" ) type nestestBus struct { ram [0x0800]uint8 prg []uint8 } func (b *nestestBus) Read(addr uint16) uint8 { switch { case addr < 0x2000: return b.ram[addr%0x0800] case addr >= 0x8000: return b.prg[int(addr-0x8000)%len(b.prg)] default: return 0 } } func (b *nestestBus) Write(addr uint16, v uint8) { if addr < 0x2000 { b.ram[addr%0x0800] = v } } func loadNestestPRG(path string) ([]uint8, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } prgBanks := int(data[4]) prgSize := prgBanks * 16 * 1024 hasTrainer := data[6]&0x04 != 0 offset := 16 if hasTrainer { offset += 512 } return data[offset : offset+prgSize], nil } type cpuState struct { pc uint16 a, x, y, p, sp uint8 cyc uint64 } var nestestLogRE = regexp.MustCompile( `^([0-9A-F]{4}).*A:([0-9A-F]{2}) X:([0-9A-F]{2}) Y:([0-9A-F]{2}) P:([0-9A-F]{2}) SP:([0-9A-F]{2}).*CYC:\s*(\d+)`) func loadNestestLog(path string) ([]cpuState, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() var lines []cpuState scanner := bufio.NewScanner(f) for scanner.Scan() { m := nestestLogRE.FindStringSubmatch(scanner.Text()) if m == nil { continue } parseHex8 := func(s string) uint8 { v, _ := strconv.ParseUint(s, 16, 8) return uint8(v) } pc, _ := strconv.ParseUint(m[1], 16, 16) cyc, _ := strconv.ParseUint(m[7], 10, 64) lines = append(lines, cpuState{ pc: uint16(pc), a: parseHex8(m[2]), x: parseHex8(m[3]), y: parseHex8(m[4]), p: parseHex8(m[5]), sp: parseHex8(m[6]), cyc: cyc, }) } return lines, scanner.Err() } // TestNestest runs nestest.nes and diffs the state against a known good log before each instruction func TestNestest(t *testing.T) { const romPath = "testdata/nestest.nes" const logPath = "testdata/nestest.log" if _, err := os.Stat(romPath); err != nil { t.Skip("testdata/nestest.nes not present; add it and testdata/nestest.log to run this test") } prg, err := loadNestestPRG(romPath) if err != nil { t.Fatalf("loading ROM: %v", err) } want, err := loadNestestLog(logPath) if err != nil { t.Fatalf("loading log: %v", err) } if len(want) == 0 { t.Fatal("no log lines parsed") } bus := &nestestBus{prg: prg} c := NewCPU(bus) c.PC = 0xC000 c.SP = 0xFD c.Status = 0x24 c.Cycles = want[0].cyc for i, w := range want { got := cpuState{c.PC, c.A, c.X, c.Y, c.Status, c.SP, c.Cycles} if got != w { t.Fatalf("line %d: got %+v, want %+v", i+1, got, w) } stopped := func() (panicked bool) { defer func() { if r := recover(); r != nil { t.Logf("stopped at line %d: %v", i+1, r) panicked = true } }() c.Step() return false }() if stopped { t.Logf("matched %d instructions before stopping", i+1) return } } }