diff options
Diffstat (limited to 'cpu/nestest_test.go')
| -rw-r--r-- | cpu/nestest_test.go | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/cpu/nestest_test.go b/cpu/nestest_test.go new file mode 100644 index 0000000..4786c5b --- /dev/null +++ b/cpu/nestest_test.go @@ -0,0 +1,139 @@ +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 + } + } +} |