aboutsummaryrefslogtreecommitdiff
path: root/cpu/nestest_test.go
diff options
context:
space:
mode:
authorcrecher <crecher@crecher.net>2026-10-12 03:44:25 +0200
committercrecher <crecher@crecher.net>2026-10-12 03:44:25 +0200
commit7775f9ad5407f2be4b03c300d1db4c745fda2f69 (patch)
tree857bed544f0c3161aa5d561c54f0a112feb0b178 /cpu/nestest_test.go
downloadgones-7775f9ad5407f2be4b03c300d1db4c745fda2f69.tar.gz
gones-7775f9ad5407f2be4b03c300d1db4c745fda2f69.zip
initial commit
Diffstat (limited to 'cpu/nestest_test.go')
-rw-r--r--cpu/nestest_test.go139
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
+ }
+ }
+}