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

99 lines
4.1 KiB
TypeScript

import assert from "assert";
import { describe, it } from "mocha";
import { CallGraphBuilder, InsnKind } from "../../src/common/callgraph";
import { ProbeFlags } from "../../src/common/probe";
// Feed a builder the events the probe would log: before each instruction,
// the SP change the previous one made, then the instruction's address.
function run(kinds: { [pc: number]: InsnKind }, steps: [number, number][]) {
const b = new CallGraphBuilder(pc => kinds[pc] ?? 'other', pc => '$' + pc.toString(16));
let sp = -1;
for (const [pc, newsp] of steps) {
if (newsp !== sp) {
b.event(newsp < sp ? ProbeFlags.SP_PUSH : ProbeFlags.SP_POP, newsp, 0);
sp = newsp;
}
b.event(ProbeFlags.EXECUTE, pc, 0);
}
return b;
}
describe('Call graph', function () {
it('follows calls and returns, not pushes and jumps', function () {
const b = run({ 0x102: 'call', 0x205: 'return', 0x110: 'call' }, [
[0x100, 0xf004],
[0x102, 0xf004], // CALL $200
[0x200, 0xf002],
[0x201, 0xf002], // PUSH HL, then a jump: not a call
[0x202, 0xf000],
[0x300, 0xf000],
[0x204, 0xf002], // POP HL
[0x205, 0xf002], // RET
[0x103, 0xf004],
[0x110, 0xf004], // CALL C,$200, not taken: SP stays
[0x113, 0xf004],
]);
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$200']);
assert.strictEqual(b.graph.calls['$200'].count, 1);
assert.deepStrictEqual(Object.keys(b.graph.calls['$200'].calls), []);
assert.strictEqual(b.stack.length, 1);
});
it('counts an interrupt as a call', function () {
const b = new CallGraphBuilder(() => 'other', pc => '$' + pc.toString(16));
b.event(ProbeFlags.SP_POP, 0x1ff, 0);
b.event(ProbeFlags.EXECUTE, 0x8000, 0);
b.event(ProbeFlags.EXECUTE, 0x8001, 0);
b.event(ProbeFlags.INTERRUPT, 0, 0);
b.event(ProbeFlags.SP_PUSH, 0x1fc, 0);
b.event(ProbeFlags.EXECUTE, 0x9000, 0);
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$9000']);
});
it('counts an unreported interrupt as a call, so its return is not a new root', function () {
// an NMI: the interrupted instruction is not a call, SP drops by 3 and the PC jumps
const b = run({ 0x9002: 'return' }, [
[0x100, 0x1ff], [0x101, 0x1ff],
[0x9000, 0x1fc], [0x9001, 0x1fc], [0x9002, 0x1fc], // NMI handler, ends in RTI
[0x102, 0x1ff], [0x103, 0x1ff],
]);
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$9000']);
assert.strictEqual(b.graph.calls['$9000'].count, 1);
assert.strictEqual(b.stack.length, 1);
assert.strictEqual(b.graph.$$PC, null); // the root was not re-parented
});
it('returns past where it started into a new root', function () {
const b = run({ 0x205: 'return' }, [[0x204, 0xf000], [0x205, 0xf000], [0x103, 0xf002]]);
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$205']);
assert.strictEqual(b.stack.length, 1);
});
it('guesses from SP and PC when it cannot tell the instruction', function () {
const kinds = new Proxy({}, { get: () => 'unknown' }) as any;
const b = run(kinds, [[0x100, 0x1ff], [0x101, 0x1ff], [0x200, 0x1fd]]);
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$200']);
});
it('charges clocks to the routine running and to its callers', function () {
const b = new CallGraphBuilder(pc => pc === 0x102 ? 'call' : pc === 0x201 ? 'return' : 'other', pc => '$' + pc.toString(16));
const step = (pc: number, sp: number, clocks: number) => {
if (sp !== (step as any).sp) b.event(sp < (step as any).sp ? ProbeFlags.SP_PUSH : ProbeFlags.SP_POP, sp, 0);
(step as any).sp = sp;
b.event(ProbeFlags.EXECUTE, pc, 0);
b.event(ProbeFlags.CLOCKS, clocks, 0);
};
(step as any).sp = -1;
step(0x100, 0xf004, 4);
step(0x102, 0xf004, 6); // CALL $200
step(0x200, 0xf002, 10);
step(0x201, 0xf002, 6); // RET
step(0x103, 0xf004, 4);
const call = b.graph.calls['$200'];
assert.strictEqual(call.self, 16);
assert.strictEqual(call.total, 16);
assert.strictEqual(b.graph.self, 4 + 6 + 4); // the CALL counts where it started; so does the RET, in $200
assert.strictEqual(b.graph.total, 30);
});
});