Files
8bitworkshop/test/unit/teststepover.ts
T

116 lines
3.7 KiB
TypeScript

import assert from "assert";
import { describe, it } from "mocha";
import {
BaseDebugPlatform,
CpuState,
EmuState,
} from "../../src/common/baseplatform";
import { disassemble6502 } from "../../src/common/cpu/disasm6502";
// Minimal 6502-ish platform for exercising BaseDebugPlatform.stepOver().
// readAddress() relies on 'this' like real platforms (e.g. c64.ts), so it
// catches bugs where the method reference is detached from its object.
class TestStepPlatform extends BaseDebugPlatform {
mem = new Uint8Array(0x10000);
cpu = { PC: 0, SP: 0xff };
paused = false;
getPC(): number { return this.cpu.PC; }
getSP(): number { return this.cpu.SP; }
getCPUState(): CpuState { return { PC: this.cpu.PC, SP: this.cpu.SP }; }
isStable(): boolean { return true; }
// uses 'this' -- must not be called unbound
readAddress(a: number): number { return this.mem[a & 0xffff]; }
// like real platforms, goes through the passed-in read callback
disassemble(pc: number, read: (a: number) => number) {
return disassemble6502(pc, read(pc), read(pc + 1), read(pc + 2));
}
saveState(): EmuState { return { c: { PC: this.cpu.PC, SP: this.cpu.SP } }; }
loadState(state: EmuState) { this.cpu.PC = state.c.PC; this.cpu.SP = state.c.SP; }
pause() { this.paused = true; }
// like a real platform, resuming starts the emulation loop running
// until the next breakpoint
resume() { this.paused = false; this.advance(); }
isRunning() { return !this.paused; }
advance(novideo?: boolean): number {
// execute instructions until breakpoint hit or budget exceeded
var n = 0;
while (!this.wasBreakpointHit() && n < 1000) {
this.execInstruction();
n++;
this.debugClock++;
this.evalDebugCondition();
}
return n;
}
execInstruction() {
var op = this.readAddress(this.cpu.PC);
switch (op) {
case 0x20: { // JSR abs
var target = this.readAddress(this.cpu.PC + 1) | (this.readAddress(this.cpu.PC + 2) << 8);
var ret = (this.cpu.PC + 2) & 0xffff;
this.push((ret >> 8) & 0xff);
this.push(ret & 0xff);
this.cpu.PC = target;
break;
}
case 0x60: { // RTS
var lo = this.pop();
var hi = this.pop();
this.cpu.PC = ((lo | (hi << 8)) + 1) & 0xffff;
break;
}
default: // NOP and anything else: treat as 1-byte instruction
this.cpu.PC = (this.cpu.PC + 1) & 0xffff;
break;
}
}
push(b: number) {
this.mem[0x100 + this.cpu.SP] = b & 0xff;
this.cpu.SP = (this.cpu.SP - 1) & 0xff;
}
pop(): number {
this.cpu.SP = (this.cpu.SP + 1) & 0xff;
return this.mem[0x100 + this.cpu.SP];
}
}
describe('BaseDebugPlatform stepOver', function () {
function makePlatform(): TestStepPlatform {
var p = new TestStepPlatform();
// JSR $0005 ; NOP ; BRK
p.mem.set([0x20, 0x05, 0x00, 0xEA, 0x00], 0x0000);
// subroutine at $0005: LDA #$01 ; RTS
p.mem.set([0xA9, 0x01, 0x60], 0x0005);
p.cpu.PC = 0x0000;
return p;
}
it('should step over a JSR to the following instruction', function () {
var p = makePlatform();
var sp0 = p.getSP();
p.stepOver(); // runs the runEval loop via advance()
assert.strictEqual(p.getPC(), 0x0003); // not inside the subroutine
assert.strictEqual(p.getSP(), sp0); // subroutine returned
});
it('should single-step when instruction is not a call', function () {
var p = makePlatform();
p.cpu.PC = 0x0003; // NOP
p.stepOver();
assert.strictEqual(p.getPC(), 0x0004);
});
it('should detect iscall on JSR opcode', function () {
var p = makePlatform();
var d = p.disassemble(0x0000, (a) => p.readAddress(a));
assert.strictEqual(d.iscall, true);
assert.strictEqual(d.nbytes, 3);
});
});