mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
added some more unit tests
This commit is contained in:
1 parent
4ae7b5541d
commit
1122de18a5
4 files changed
+1799
No files matched your search
@@ -0,0 +1,660 @@
|
||||
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import {
|
||||
DebugSymbols,
|
||||
isDebuggable,
|
||||
Debuggable,
|
||||
BreakpointList,
|
||||
BasePlatform,
|
||||
BaseDebugPlatform,
|
||||
Base6502Platform,
|
||||
BaseZ80Platform,
|
||||
Base6809Platform,
|
||||
EmuState,
|
||||
CpuState,
|
||||
OpcodeMetadata,
|
||||
Platform,
|
||||
getToolForFilename_6502,
|
||||
getToolForFilename_z80,
|
||||
getToolForFilename_6809,
|
||||
getOpcodeMetadata_6502,
|
||||
cpuStateToLongString_6502,
|
||||
cpuStateToLongString_Z80,
|
||||
cpuStateToLongString_6809,
|
||||
inspectSymbol,
|
||||
lookupSymbol,
|
||||
dumpStackToString,
|
||||
} from "../../src/common/baseplatform";
|
||||
|
||||
describe('DebugSymbols', function () {
|
||||
it('should create symbol map with correct structure', function () {
|
||||
const symbolmap = { main: 0x0000, loop: 0x0010, end: 0x0100 };
|
||||
const debuginfo = { platform: 'test' };
|
||||
const symbols = new DebugSymbols(symbolmap, debuginfo);
|
||||
|
||||
assert.strictEqual(symbols.symbolmap['main'], 0x0000);
|
||||
assert.strictEqual(symbols.symbolmap['loop'], 0x0010);
|
||||
assert.strictEqual(symbols.symbolmap['end'], 0x0100);
|
||||
});
|
||||
|
||||
it('should create inverted address-to-symbol map', function () {
|
||||
const symbolmap = { main: 0x0000, loop: 0x0010, end: 0x0100 };
|
||||
const debuginfo = {};
|
||||
const symbols = new DebugSymbols(symbolmap, debuginfo);
|
||||
|
||||
assert.strictEqual(symbols.addr2symbol[0x0000], 'main');
|
||||
assert.strictEqual(symbols.addr2symbol[0x0010], 'loop');
|
||||
assert.strictEqual(symbols.addr2symbol[0x0100], 'end');
|
||||
});
|
||||
|
||||
it('should ensure sentinel values in address map', function () {
|
||||
const symbolmap = { loop: 0x1000 };
|
||||
const debuginfo = {};
|
||||
const symbols = new DebugSymbols(symbolmap, debuginfo);
|
||||
|
||||
// Empty symbol map gets $00 sentinel
|
||||
assert.strictEqual(symbols.addr2symbol[0x0], '$00');
|
||||
assert.strictEqual(symbols.addr2symbol[0x10000], '__END__');
|
||||
});
|
||||
|
||||
it('should store debug info', function () {
|
||||
const debuginfo = { platform: 'c64', version: '1.0' };
|
||||
const symbols = new DebugSymbols({}, debuginfo);
|
||||
|
||||
assert.deepStrictEqual(symbols.debuginfo, debuginfo);
|
||||
});
|
||||
|
||||
it('should handle empty symbol map', function () {
|
||||
const symbols = new DebugSymbols({}, {});
|
||||
|
||||
assert.deepStrictEqual(symbols.symbolmap, {});
|
||||
assert.ok(symbols.addr2symbol[0x0] !== undefined);
|
||||
});
|
||||
|
||||
it('should handle duplicate addresses with last symbol winning', function () {
|
||||
const symbolmap = { sym1: 0x100, sym2: 0x100 };
|
||||
const symbols = new DebugSymbols(symbolmap, {});
|
||||
|
||||
// One of them should be in the inverted map
|
||||
assert.ok(symbols.addr2symbol[0x100] === 'sym1' || symbols.addr2symbol[0x100] === 'sym2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDebuggable type guard', function () {
|
||||
it('should identify debuggable objects', function () {
|
||||
const debuggable: Debuggable = {
|
||||
getDebugCategories: () => ['CPU', 'Memory'],
|
||||
getDebugInfo: (category: string) => 'debug info'
|
||||
};
|
||||
|
||||
assert.ok(isDebuggable(debuggable));
|
||||
});
|
||||
|
||||
it('should reject non-debuggable objects', function () {
|
||||
assert.ok(!isDebuggable({}));
|
||||
assert.ok(!isDebuggable(null));
|
||||
assert.ok(!isDebuggable(undefined));
|
||||
assert.ok(!isDebuggable({ foo: 'bar' }));
|
||||
});
|
||||
|
||||
it('should accept partial implementations', function () {
|
||||
const partial = {
|
||||
getDebugCategories: () => ['CPU']
|
||||
};
|
||||
|
||||
assert.ok(isDebuggable(partial));
|
||||
});
|
||||
});
|
||||
|
||||
describe('BreakpointList', function () {
|
||||
it('should return null when no breakpoints exist', function () {
|
||||
const list = new BreakpointList();
|
||||
const cond = list.getDebugCondition();
|
||||
|
||||
assert.strictEqual(cond, null);
|
||||
});
|
||||
|
||||
it('should create debug condition for single breakpoint', function () {
|
||||
const list = new BreakpointList();
|
||||
let hitCount = 0;
|
||||
list.id2bp['bp1'] = { cond: () => { hitCount++; return true; } };
|
||||
|
||||
const cond = list.getDebugCondition();
|
||||
assert.ok(cond !== null);
|
||||
assert.ok(cond());
|
||||
assert.strictEqual(hitCount, 1);
|
||||
});
|
||||
|
||||
it('should evaluate multiple breakpoints with OR logic', function () {
|
||||
const list = new BreakpointList();
|
||||
let hits1 = 0, hits2 = 0;
|
||||
|
||||
list.id2bp['bp1'] = { cond: () => { hits1++; return false; } };
|
||||
list.id2bp['bp2'] = { cond: () => { hits2++; return true; } };
|
||||
|
||||
const cond = list.getDebugCondition();
|
||||
const result = cond();
|
||||
|
||||
assert.ok(result);
|
||||
assert.strictEqual(hits1, 1);
|
||||
assert.strictEqual(hits2, 1);
|
||||
});
|
||||
|
||||
it('should remove breakpoint', function () {
|
||||
const list = new BreakpointList();
|
||||
list.id2bp['bp1'] = { cond: () => true };
|
||||
|
||||
delete list.id2bp['bp1'];
|
||||
const cond = list.getDebugCondition();
|
||||
|
||||
assert.strictEqual(cond, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool Filename Detection', function () {
|
||||
describe('6502 tools', function () {
|
||||
it('should detect C language', function () {
|
||||
assert.strictEqual(getToolForFilename_6502('test.c'), 'cc65');
|
||||
assert.strictEqual(getToolForFilename_6502('program.c'), 'cc65');
|
||||
});
|
||||
|
||||
it('should detect assembly language', function () {
|
||||
assert.strictEqual(getToolForFilename_6502('test.s'), 'ca65');
|
||||
assert.strictEqual(getToolForFilename_6502('code.ca65'), 'ca65');
|
||||
assert.strictEqual(getToolForFilename_6502('test.dasm'), 'dasm');
|
||||
});
|
||||
|
||||
it('should detect various assembler formats', function () {
|
||||
assert.strictEqual(getToolForFilename_6502('test.acme'), 'acme');
|
||||
assert.strictEqual(getToolForFilename_6502('test.xa'), 'xa');
|
||||
assert.strictEqual(getToolForFilename_6502('test.wiz'), 'wiz');
|
||||
});
|
||||
|
||||
it('should detect special formats', function () {
|
||||
assert.strictEqual(getToolForFilename_6502('test.ecs'), 'ecs');
|
||||
assert.strictEqual(getToolForFilename_6502('test.cpp'), 'oscar64');
|
||||
assert.strictEqual(getToolForFilename_6502('test.cc'), 'oscar64');
|
||||
assert.strictEqual(getToolForFilename_6502('test.o64'), 'oscar64');
|
||||
});
|
||||
|
||||
it('should default to dasm for unknown extensions', function () {
|
||||
assert.strictEqual(getToolForFilename_6502('test.a'), 'dasm');
|
||||
assert.strictEqual(getToolForFilename_6502('test.asm'), 'dasm');
|
||||
assert.strictEqual(getToolForFilename_6502('unknown'), 'dasm');
|
||||
});
|
||||
|
||||
it('should detect llvm-mos for special C files', function () {
|
||||
assert.strictEqual(getToolForFilename_6502('test-llvm.c'), 'remote:llvm-mos');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Z80 tools', function () {
|
||||
it('should detect C language', function () {
|
||||
assert.strictEqual(getToolForFilename_z80('test.c'), 'sdcc');
|
||||
assert.strictEqual(getToolForFilename_z80('code.h'), 'sdcc');
|
||||
});
|
||||
|
||||
it('should detect assembly language', function () {
|
||||
assert.strictEqual(getToolForFilename_z80('test.s'), 'sdasz80');
|
||||
assert.strictEqual(getToolForFilename_z80('test.sgb'), 'sdasgb');
|
||||
assert.strictEqual(getToolForFilename_z80('test.ns'), 'naken');
|
||||
});
|
||||
|
||||
it('should detect special formats', function () {
|
||||
assert.strictEqual(getToolForFilename_z80('test.scc'), 'sccz80');
|
||||
assert.strictEqual(getToolForFilename_z80('test.z'), 'zmac');
|
||||
assert.strictEqual(getToolForFilename_z80('test.wiz'), 'wiz');
|
||||
});
|
||||
|
||||
it('should default to zmac for unknown extensions', function () {
|
||||
assert.strictEqual(getToolForFilename_z80('unknown'), 'zmac');
|
||||
});
|
||||
});
|
||||
|
||||
describe('6809 tools', function () {
|
||||
it('should detect C language', function () {
|
||||
assert.strictEqual(getToolForFilename_6809('test.c'), 'cmoc');
|
||||
assert.strictEqual(getToolForFilename_6809('code.h'), 'cmoc');
|
||||
});
|
||||
|
||||
it('should detect assembly language', function () {
|
||||
assert.strictEqual(getToolForFilename_6809('test.xasm'), 'xasm6809');
|
||||
assert.strictEqual(getToolForFilename_6809('test.lwasm'), 'lwasm');
|
||||
});
|
||||
|
||||
it('should default to cmoc', function () {
|
||||
assert.strictEqual(getToolForFilename_6809('unknown'), 'cmoc');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Opcode Metadata', function () {
|
||||
it('should return valid metadata for 6502 opcodes', function () {
|
||||
// Test NOP (0xEA)
|
||||
const nop = getOpcodeMetadata_6502(0xEA, 0);
|
||||
assert.strictEqual(nop.opcode, 0xEA);
|
||||
assert.ok(nop.minCycles > 0);
|
||||
assert.ok(nop.maxCycles >= nop.minCycles);
|
||||
assert.ok(nop.insnlength > 0);
|
||||
});
|
||||
|
||||
it('should return consistent metadata for same opcode', function () {
|
||||
const meta1 = getOpcodeMetadata_6502(0x69, 0x1000); // ADC immediate
|
||||
const meta2 = getOpcodeMetadata_6502(0x69, 0x2000);
|
||||
|
||||
assert.strictEqual(meta1.opcode, meta2.opcode);
|
||||
assert.strictEqual(meta1.minCycles, meta2.minCycles);
|
||||
assert.strictEqual(meta1.maxCycles, meta2.maxCycles);
|
||||
assert.strictEqual(meta1.insnlength, meta2.insnlength);
|
||||
});
|
||||
|
||||
it('should handle all 256 6502 opcodes', function () {
|
||||
for (let op = 0; op < 256; op++) {
|
||||
const meta = getOpcodeMetadata_6502(op, 0);
|
||||
assert.ok(meta.minCycles >= 0);
|
||||
assert.ok(meta.maxCycles >= meta.minCycles);
|
||||
assert.ok(meta.insnlength >= 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('CPU State Formatting', function () {
|
||||
describe('6502 CPU state', function () {
|
||||
it('should format basic CPU state', function () {
|
||||
const state: any = {
|
||||
PC: 0x1234,
|
||||
SP: 0xFF,
|
||||
A: 0x42,
|
||||
X: 0x10,
|
||||
Y: 0x20,
|
||||
N: false, V: false, D: false, Z: false, C: false, I: false, R: true
|
||||
};
|
||||
|
||||
const output = cpuStateToLongString_6502(state);
|
||||
assert.ok(output.includes('1234')); // PC
|
||||
assert.ok(output.includes('FF')); // SP
|
||||
});
|
||||
|
||||
it('should show flag status', function () {
|
||||
const state1: any = {
|
||||
PC: 0,
|
||||
SP: 0,
|
||||
N: true, V: false, D: false, Z: true, C: false, I: false, R: true
|
||||
};
|
||||
|
||||
const output = cpuStateToLongString_6502(state1);
|
||||
assert.ok(output.includes('N'));
|
||||
assert.ok(output.includes('Z'));
|
||||
});
|
||||
|
||||
it('should show BUSY status when R is false', function () {
|
||||
const state: any = { PC: 0, SP: 0, R: false };
|
||||
const output = cpuStateToLongString_6502(state);
|
||||
assert.ok(output.includes('BUSY'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Z80 CPU state', function () {
|
||||
it('should format Z80 state', function () {
|
||||
const state: any = {
|
||||
PC: 0x4000,
|
||||
SP: 0x9000,
|
||||
AF: 0x1234,
|
||||
BC: 0x5678,
|
||||
DE: 0x9ABC,
|
||||
HL: 0xDEF0,
|
||||
IX: 0x0001,
|
||||
IY: 0x0002,
|
||||
IR: 0x3344,
|
||||
iff1: true,
|
||||
iff2: false
|
||||
};
|
||||
|
||||
const output = cpuStateToLongString_Z80(state);
|
||||
assert.ok(output.includes('4000')); // PC
|
||||
assert.ok(output.includes('9000')); // SP
|
||||
assert.ok(output.includes('1234')); // AF
|
||||
});
|
||||
});
|
||||
|
||||
describe('6809 CPU state', function () {
|
||||
it('should format 6809 state', function () {
|
||||
const state: any = {
|
||||
PC: 0x1000,
|
||||
SP: 0x2000,
|
||||
DP: 0x00,
|
||||
A: 0x11,
|
||||
B: 0x22,
|
||||
X: 0x3333,
|
||||
Y: 0x4444,
|
||||
U: 0x5555,
|
||||
CC: 0xF0
|
||||
};
|
||||
|
||||
const output = cpuStateToLongString_6809(state);
|
||||
assert.ok(output.includes('1000')); // PC
|
||||
assert.ok(output.includes('2000')); // SP
|
||||
assert.ok(output.includes('11')); // A
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Symbol lookup', function () {
|
||||
it('should find symbol at address', function () {
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({ main: 0x1000, loop: 0x1010 }, {}),
|
||||
readAddress: (addr: number) => addr & 0xFF
|
||||
};
|
||||
|
||||
const result = lookupSymbol(platform, 0x1010, false);
|
||||
assert.ok(result.includes('loop'));
|
||||
});
|
||||
|
||||
it('should find symbol before address when extra is true', function () {
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({ main: 0x1000 }, {}),
|
||||
readAddress: (addr: number) => 0
|
||||
};
|
||||
|
||||
const result = lookupSymbol(platform, 0x1005, true);
|
||||
assert.ok(result.includes('main'));
|
||||
assert.ok(result.includes('$05'));
|
||||
});
|
||||
|
||||
it('should return empty string when no symbol found', function () {
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({}, {}),
|
||||
readAddress: (addr: number) => 0
|
||||
};
|
||||
|
||||
const result = lookupSymbol(platform, 0x5000, false);
|
||||
assert.strictEqual(result, '');
|
||||
});
|
||||
|
||||
it('should handle missing debug symbols', function () {
|
||||
const platform: any = { debugSymbols: null };
|
||||
|
||||
const result = lookupSymbol(platform, 0x1000, false);
|
||||
assert.strictEqual(result, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stack dumping', function () {
|
||||
it('should dump stack memory', function () {
|
||||
const mem = new Uint8Array([
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x20, 0x10, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
||||
]);
|
||||
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({ func: 0x100E }, {}),
|
||||
readAddress: (addr: number) => addr < mem.length ? mem[addr] : 0
|
||||
};
|
||||
|
||||
const result = dumpStackToString(platform, mem, 0, 15, 9, 0x20);
|
||||
assert.ok(result.includes('$'));
|
||||
assert.ok(result.length > 0);
|
||||
});
|
||||
|
||||
it('should handle big endian byte order', function () {
|
||||
const mem = new Uint8Array([0x10, 0x20, 0x30, 0x40]);
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({}, {}),
|
||||
readAddress: (addr: number) => mem[addr]
|
||||
};
|
||||
|
||||
const result = dumpStackToString(platform, mem, 0, 3, 2, 0, true);
|
||||
assert.ok(result.includes('$'));
|
||||
});
|
||||
|
||||
it('should show JSR addresses', function () {
|
||||
const mem = new Uint8Array(256);
|
||||
// Set up JSR at 0x100 (6502 JSR is 0x20)
|
||||
mem[0x100] = 0x20;
|
||||
// Stack has return address pointing near 0x100
|
||||
mem[0x50] = 0xFE;
|
||||
mem[0x51] = 0x01;
|
||||
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({ func: 0x0100 }, {}),
|
||||
readAddress: (addr: number) => {
|
||||
if (addr < mem.length) return mem[addr];
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const result = dumpStackToString(platform, mem, 0, 255, 0x50, 0x20);
|
||||
// The function returns a string with stack content
|
||||
assert.ok(result.includes('$'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('BasePlatform', function () {
|
||||
class TestPlatform extends BasePlatform {
|
||||
loadState(state: EmuState): void { }
|
||||
saveState(): EmuState { return {}; }
|
||||
pause(): void { }
|
||||
resume(): void { }
|
||||
advance(novideo?: boolean): number { return 0; }
|
||||
}
|
||||
|
||||
it('should initialize with empty internal files', function () {
|
||||
const platform = new TestPlatform();
|
||||
assert.deepStrictEqual(platform.internalFiles, {});
|
||||
});
|
||||
|
||||
it('should set and get recorder', function () {
|
||||
const platform = new TestPlatform();
|
||||
const recorder: any = { frameRequested: () => false };
|
||||
platform.setRecorder(recorder);
|
||||
assert.strictEqual(platform.recorder, recorder);
|
||||
});
|
||||
|
||||
it('should read and write files', function () {
|
||||
const platform = new TestPlatform();
|
||||
const data: any = { data: [1, 2, 3] };
|
||||
|
||||
platform.writeFile('test.txt', data);
|
||||
const read = platform.readFile('test.txt');
|
||||
|
||||
assert.deepStrictEqual(read, data);
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent file', function () {
|
||||
const platform = new TestPlatform();
|
||||
const read = platform.readFile('nonexistent.txt');
|
||||
assert.strictEqual(read, undefined);
|
||||
});
|
||||
|
||||
it('should generate debug tree', function () {
|
||||
const platform = new TestPlatform();
|
||||
platform.debugSymbols = new DebugSymbols({ test: 0x1000 }, { info: 'test' });
|
||||
|
||||
const tree: any = platform.getDebugTree();
|
||||
assert.ok(tree.state);
|
||||
assert.deepStrictEqual(tree.debuginfo, { info: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('BaseDebugPlatform', function () {
|
||||
class TestDebugPlatform extends BaseDebugPlatform {
|
||||
loadState(state: EmuState): void { }
|
||||
saveState(): EmuState { return { c: { PC: 0 } }; }
|
||||
pause(): void { }
|
||||
resume(): void { }
|
||||
advance(novideo?: boolean): number { return 0; }
|
||||
getCPUState(): CpuState { return { PC: 0x1000, SP: 0x100 }; }
|
||||
getSP(): number { return 0x100; }
|
||||
getPC(): number { return 0x1000; }
|
||||
isStable(): boolean { return true; }
|
||||
}
|
||||
|
||||
it('should manage breakpoints', function () {
|
||||
const platform = new TestDebugPlatform();
|
||||
const cond = () => true;
|
||||
|
||||
platform.setBreakpoint('bp1', cond);
|
||||
assert.ok(platform.hasBreakpoint('bp1'));
|
||||
|
||||
platform.clearBreakpoint('bp1');
|
||||
assert.ok(!platform.hasBreakpoint('bp1'));
|
||||
});
|
||||
|
||||
it('should clear all debug state', function () {
|
||||
const platform = new TestDebugPlatform();
|
||||
platform.debugSavedState = { c: { PC: 0 } };
|
||||
platform.debugBreakState = { c: { PC: 100 } };
|
||||
platform.onBreakpointHit = () => { };
|
||||
|
||||
platform.clearDebug();
|
||||
|
||||
assert.strictEqual(platform.debugSavedState, null);
|
||||
assert.strictEqual(platform.debugBreakState, null);
|
||||
assert.strictEqual(platform.onBreakpointHit, null);
|
||||
});
|
||||
|
||||
it('should track frame count', function () {
|
||||
const platform = new TestDebugPlatform();
|
||||
assert.strictEqual(platform.frameCount, 0);
|
||||
|
||||
platform.postFrame();
|
||||
assert.strictEqual(platform.frameCount, 1);
|
||||
|
||||
platform.postFrame();
|
||||
assert.strictEqual(platform.frameCount, 2);
|
||||
});
|
||||
|
||||
it('should detect breakpoint hits', function () {
|
||||
const platform = new TestDebugPlatform();
|
||||
assert.ok(!platform.wasBreakpointHit());
|
||||
|
||||
platform.debugBreakState = { c: { PC: 0 } };
|
||||
assert.ok(platform.wasBreakpointHit());
|
||||
});
|
||||
});
|
||||
|
||||
describe('Base6502Platform', function () {
|
||||
class TestNES6502 extends Base6502Platform {
|
||||
loadState(state: EmuState): void { }
|
||||
saveState(): EmuState { return { c: { PC: 0, SP: 0 } }; }
|
||||
pause(): void { }
|
||||
resume(): void { }
|
||||
advance(novideo?: boolean): number { return 0; }
|
||||
getCPUState(): CpuState { return { PC: 0x8000, SP: 0xFF }; }
|
||||
getSP(): number { return 0xFF; }
|
||||
getPC(): number { return 0x8000; }
|
||||
isStable(): boolean { return true; }
|
||||
readAddress(addr: number): number { return 0; }
|
||||
}
|
||||
|
||||
it('should get default extensions', function () {
|
||||
const platform = new TestNES6502();
|
||||
const exts = platform.getDefaultExtensions();
|
||||
assert.ok(exts.includes('.c'));
|
||||
assert.ok(exts.includes('.ca65'));
|
||||
assert.ok(exts.includes('.dasm'));
|
||||
});
|
||||
|
||||
it('should get 6502 opcode metadata', function () {
|
||||
const platform = new TestNES6502();
|
||||
const meta = platform.getOpcodeMetadata(0x69, 0);
|
||||
assert.ok(meta.minCycles > 0);
|
||||
});
|
||||
|
||||
it('should get origin PC from reset vector', function () {
|
||||
const platform = new TestNES6502();
|
||||
const originPC = platform.getOriginPC();
|
||||
assert.ok(typeof originPC === 'number');
|
||||
});
|
||||
|
||||
it('should apply PC delta correction', function () {
|
||||
const platform = new TestNES6502();
|
||||
const state: CpuState = { PC: 0x1000 };
|
||||
|
||||
const fixed = platform.fixPC(state);
|
||||
assert.strictEqual(fixed.PC, 0x0FFF); // PC - 1
|
||||
|
||||
const unfixed = platform.unfixPC(fixed);
|
||||
assert.strictEqual(unfixed.PC, 0x1000);
|
||||
});
|
||||
|
||||
it('should handle PC wrapping at 16-bit boundary', function () {
|
||||
const platform = new TestNES6502();
|
||||
const state: CpuState = { PC: 0x0000 };
|
||||
|
||||
const fixed = platform.fixPC(state);
|
||||
assert.strictEqual(fixed.PC, 0xFFFF);
|
||||
});
|
||||
|
||||
it('should get debug categories', function () {
|
||||
const platform = new TestNES6502();
|
||||
const cats = platform.getDebugCategories();
|
||||
assert.ok(cats.includes('CPU'));
|
||||
assert.ok(cats.includes('Stack'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspectSymbol', function () {
|
||||
it('should inspect symbol value at address', function () {
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({ myvar: 0x100 }, {}),
|
||||
readAddress: (addr: number) => addr === 0x100 ? 0x42 : 0x00
|
||||
};
|
||||
|
||||
const result = inspectSymbol(platform, 'myvar');
|
||||
assert.ok(result.includes('100'));
|
||||
assert.ok(result.includes('42'));
|
||||
});
|
||||
|
||||
it('should handle two-byte values', function () {
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({ word: 0x200 }, {}),
|
||||
readAddress: (addr: number) => {
|
||||
if (addr === 0x200) return 0x34;
|
||||
if (addr === 0x201) return 0x12;
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const result = inspectSymbol(platform, 'word');
|
||||
assert.ok(result.includes('200'));
|
||||
});
|
||||
|
||||
it('should look for C and asm symbol variants', function () {
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({ _myvar: 0x300 }, {}),
|
||||
readAddress: (addr: number) => addr === 0x300 ? 0x99 : 0
|
||||
};
|
||||
|
||||
const result = inspectSymbol(platform, 'myvar');
|
||||
assert.ok(result.includes('99'));
|
||||
});
|
||||
|
||||
it('should return undefined for missing symbol', function () {
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({}, {}),
|
||||
readAddress: (addr: number) => 0
|
||||
};
|
||||
|
||||
const result = inspectSymbol(platform, 'nonexistent');
|
||||
assert.ok(result === undefined || result === null);
|
||||
});
|
||||
|
||||
it('should handle missing debug symbols', function () {
|
||||
const platform: any = { debugSymbols: null };
|
||||
const result = inspectSymbol(platform, 'symbol');
|
||||
assert.strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
it('should handle missing readAddress method', function () {
|
||||
const platform: any = {
|
||||
debugSymbols: new DebugSymbols({ test: 0x100 }, {}),
|
||||
readAddress: undefined
|
||||
};
|
||||
|
||||
const result = inspectSymbol(platform, 'test');
|
||||
assert.ok(result === null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import {
|
||||
lpad, rpad, byte2signed, getFilenameForPath, getFolderForPath,
|
||||
getFilenamePrefix, hex, tobin, toradix, arrayCompare, invertMap,
|
||||
stringToByteArray, byteArrayToString, byteArrayToUTF8, removeBOM,
|
||||
isProbablyBinary, clamp, safeident, rgb2bgr, RGBA, findIntegerFactors,
|
||||
replaceAll, highlightDifferences, decodeQueryString, parseBool,
|
||||
safe_extend, printFlags, byteToASCII, coerceToArray
|
||||
} from "../../src/common/util";
|
||||
|
||||
describe('String Padding Functions', function () {
|
||||
it('lpad should pad strings on the left', function () {
|
||||
assert.strictEqual(lpad('5', 3), ' 5');
|
||||
assert.strictEqual(lpad('hello', 8), ' hello');
|
||||
assert.strictEqual(lpad('x', 1), 'x');
|
||||
assert.strictEqual(lpad('123', 5), ' 123');
|
||||
});
|
||||
|
||||
it('rpad should pad strings on the right', function () {
|
||||
assert.strictEqual(rpad('5', 3), '5 ');
|
||||
assert.strictEqual(rpad('hello', 8), 'hello ');
|
||||
assert.strictEqual(rpad('x', 1), 'x');
|
||||
assert.strictEqual(rpad('123', 5), '123 ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Number Conversion Functions', function () {
|
||||
it('hex should format numbers in hexadecimal', function () {
|
||||
assert.strictEqual(hex(255), 'FF');
|
||||
assert.strictEqual(hex(0), '00');
|
||||
assert.strictEqual(hex(255, 4), '00FF');
|
||||
assert.strictEqual(hex(65535, 4), 'FFFF');
|
||||
assert.strictEqual(hex(0x12345678, 8), '12345678');
|
||||
assert.strictEqual(hex(15, 2), '0F');
|
||||
});
|
||||
|
||||
it('tobin should format numbers in binary', function () {
|
||||
assert.strictEqual(tobin(0), '00000000');
|
||||
assert.strictEqual(tobin(255), '11111111');
|
||||
assert.strictEqual(tobin(5), '00000101');
|
||||
assert.strictEqual(tobin(15, 4), '1111');
|
||||
assert.strictEqual(tobin(128), '10000000');
|
||||
});
|
||||
|
||||
it('toradix should convert to various bases', function () {
|
||||
assert.strictEqual(toradix(10, 4, 16), '000A');
|
||||
assert.strictEqual(toradix(255, 8, 16), '000000FF');
|
||||
assert.strictEqual(toradix(7, 3, 2), '111');
|
||||
assert.strictEqual(toradix(100, 4, 10), '0100');
|
||||
});
|
||||
|
||||
it('byte2signed should convert unsigned to signed bytes', function () {
|
||||
assert.strictEqual(byte2signed(0), 0);
|
||||
assert.strictEqual(byte2signed(127), 127);
|
||||
assert.strictEqual(byte2signed(128), -128);
|
||||
assert.strictEqual(byte2signed(255), -1);
|
||||
assert.strictEqual(byte2signed(200), -56);
|
||||
assert.strictEqual(byte2signed(0x80), -128);
|
||||
});
|
||||
});
|
||||
|
||||
describe('File Path Functions', function () {
|
||||
it('getFilenameForPath should extract filename from path', function () {
|
||||
assert.strictEqual(getFilenameForPath('path/to/file.txt'), 'file.txt');
|
||||
assert.strictEqual(getFilenameForPath('file.txt'), 'file.txt');
|
||||
assert.strictEqual(getFilenameForPath('/a/b/c/d.js'), 'd.js');
|
||||
assert.strictEqual(getFilenameForPath('a/b'), 'b');
|
||||
});
|
||||
|
||||
it('getFolderForPath should extract folder from path', function () {
|
||||
assert.strictEqual(getFolderForPath('path/to/file.txt'), 'path/to');
|
||||
assert.strictEqual(getFolderForPath('file.txt'), '');
|
||||
assert.strictEqual(getFolderForPath('/a/b/c/d.js'), '/a/b/c');
|
||||
assert.strictEqual(getFolderForPath('a/b/c'), 'a/b');
|
||||
});
|
||||
|
||||
it('getFilenamePrefix should extract name without extension', function () {
|
||||
assert.strictEqual(getFilenamePrefix('file.txt'), 'file');
|
||||
assert.strictEqual(getFilenamePrefix('archive.tar.gz'), 'archive.tar');
|
||||
assert.strictEqual(getFilenamePrefix('noext'), 'noext');
|
||||
assert.strictEqual(getFilenamePrefix('.hidden'), '.hidden');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Array/Collection Functions', function () {
|
||||
it('arrayCompare should compare arrays', function () {
|
||||
assert.ok(arrayCompare([1, 2, 3], [1, 2, 3]));
|
||||
assert.ok(!arrayCompare([1, 2, 3], [1, 2, 4]));
|
||||
assert.ok(!arrayCompare([1, 2], [1, 2, 3]));
|
||||
assert.ok(arrayCompare([], []));
|
||||
assert.ok(!arrayCompare([1], []));
|
||||
assert.ok(arrayCompare(null, null));
|
||||
assert.ok(!arrayCompare(null, [1]));
|
||||
assert.ok(!arrayCompare([1], null));
|
||||
const arr1 = new Uint8Array([1, 2, 3]);
|
||||
const arr2 = new Uint8Array([1, 2, 3]);
|
||||
assert.ok(arrayCompare(arr1, arr2));
|
||||
});
|
||||
|
||||
it('invertMap should invert key-value pairs', function () {
|
||||
const map = { a: '1', b: '2', c: '3' };
|
||||
const inv = invertMap(map);
|
||||
assert.strictEqual(inv['1'], 'a');
|
||||
assert.strictEqual(inv['2'], 'b');
|
||||
assert.strictEqual(inv['3'], 'c');
|
||||
});
|
||||
|
||||
it('coerceToArray should convert various types to arrays', function () {
|
||||
assert.deepStrictEqual(coerceToArray([1, 2, 3]), [1, 2, 3]);
|
||||
assert.deepStrictEqual(coerceToArray({ a: 1, b: 2 }), [1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Byte Array Conversion Functions', function () {
|
||||
it('stringToByteArray should convert string to bytes', function () {
|
||||
const bytes = stringToByteArray('hello');
|
||||
assert.strictEqual(bytes.length, 5);
|
||||
assert.strictEqual(bytes[0], 104); // 'h'
|
||||
assert.strictEqual(bytes[4], 111); // 'o'
|
||||
});
|
||||
|
||||
it('byteArrayToString should convert bytes to string', function () {
|
||||
const bytes = [72, 101, 108, 108, 111]; // "Hello"
|
||||
assert.strictEqual(byteArrayToString(bytes), 'Hello');
|
||||
assert.strictEqual(byteArrayToString(new Uint8Array([65, 66, 67])), 'ABC');
|
||||
});
|
||||
|
||||
it('byteArrayToUTF8 should decode UTF-8', function () {
|
||||
const ascii = byteArrayToUTF8([72, 101, 108, 108, 111]);
|
||||
assert.strictEqual(ascii, 'Hello');
|
||||
});
|
||||
|
||||
it('removeBOM should strip BOM if present', function () {
|
||||
assert.strictEqual(removeBOM('hello'), 'hello');
|
||||
assert.strictEqual(removeBOM('hello'), 'hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Binary Detection', function () {
|
||||
it('isProbablyBinary should detect binary data', function () {
|
||||
assert.ok(!isProbablyBinary('test.txt', [104, 101, 108, 108, 111])); // "hello"
|
||||
assert.ok(isProbablyBinary('test.bin', [1, 2, 3]));
|
||||
assert.ok(isProbablyBinary('test.chr', [255, 255]));
|
||||
assert.ok(isProbablyBinary('test.dat', [0]));
|
||||
assert.ok(!isProbablyBinary('test.c', [47, 47, 105, 102])); // "//if"
|
||||
assert.ok(isProbablyBinary(null, [0, 0, 0, 0])); // null bytes
|
||||
});
|
||||
});
|
||||
|
||||
describe('Color Functions', function () {
|
||||
it('rgb2bgr should swap red and blue channels', function () {
|
||||
assert.strictEqual(rgb2bgr(0xFF0000), 0x0000FF);
|
||||
assert.strictEqual(rgb2bgr(0x00FF00), 0x00FF00);
|
||||
assert.strictEqual(rgb2bgr(0x0000FF), 0xFF0000);
|
||||
assert.strictEqual(rgb2bgr(0xFFFFFF), 0xFFFFFF);
|
||||
});
|
||||
|
||||
it('RGBA should create color value', function () {
|
||||
const white = RGBA(255, 255, 255);
|
||||
assert.strictEqual(white & 0xFFFFFF, 0xFFFFFF);
|
||||
const black = RGBA(0, 0, 0);
|
||||
assert.strictEqual(black & 0xFFFFFF, 0x000000);
|
||||
});
|
||||
|
||||
it('clamp should constrain values', function () {
|
||||
assert.strictEqual(clamp(0, 10, 5), 5);
|
||||
assert.strictEqual(clamp(0, 10, -5), 0);
|
||||
assert.strictEqual(clamp(0, 10, 15), 10);
|
||||
assert.strictEqual(clamp(1, 1, 5), 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('String Manipulation', function () {
|
||||
it('safeident should create valid identifiers', function () {
|
||||
assert.strictEqual(safeident('valid_name'), 'valid_name');
|
||||
assert.strictEqual(safeident('123abc'), '_123abc');
|
||||
assert.strictEqual(safeident('-name'), '__name');
|
||||
assert.strictEqual(safeident('name-with-dashes'), 'name_with_dashes');
|
||||
assert.strictEqual(safeident('name@#$%'), 'name_');
|
||||
});
|
||||
|
||||
it('replaceAll should replace all occurrences', function () {
|
||||
assert.strictEqual(replaceAll('aaa', 'a', 'b'), 'bbb');
|
||||
assert.strictEqual(replaceAll('hello world', ' ', '-'), 'hello-world');
|
||||
assert.strictEqual(replaceAll('test', 'x', 'y'), 'test');
|
||||
assert.strictEqual(replaceAll('', 'a', 'b'), '');
|
||||
});
|
||||
|
||||
it('byteToASCII should convert bytes to readable chars', function () {
|
||||
assert.strictEqual(byteToASCII(65), 'A');
|
||||
assert.strictEqual(byteToASCII(32), ' ');
|
||||
assert.strictEqual(byteToASCII(0), '␀'); // NULL control char
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query String Functions', function () {
|
||||
it('decodeQueryString should parse query parameters', function () {
|
||||
const qs1 = decodeQueryString('?key=value&foo=bar');
|
||||
assert.strictEqual(qs1['key'], 'value');
|
||||
assert.strictEqual(qs1['foo'], 'bar');
|
||||
|
||||
const qs2 = decodeQueryString('a=1&b=2');
|
||||
assert.strictEqual(qs2['a'], '1');
|
||||
assert.strictEqual(qs2['b'], '2');
|
||||
|
||||
const qs3 = decodeQueryString('key=value+with+spaces');
|
||||
assert.ok(qs3['key'].includes(' '));
|
||||
});
|
||||
|
||||
it('parseBool should parse boolean strings', function () {
|
||||
assert.ok(parseBool('true'));
|
||||
assert.ok(parseBool('1'));
|
||||
assert.ok(parseBool('yes'));
|
||||
assert.ok(!parseBool('false'));
|
||||
assert.ok(!parseBool('0'));
|
||||
assert.ok(!parseBool(''));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex Utility Functions', function () {
|
||||
it('findIntegerFactors should find factor pairs', function () {
|
||||
const f1 = findIntegerFactors(64, 1, 1, 1);
|
||||
assert.ok(f1.a >= f1.b);
|
||||
assert.strictEqual(f1.a * f1.b >= 64, true);
|
||||
|
||||
const f2 = findIntegerFactors(100, 5, 5, 1);
|
||||
assert.ok(f2.a >= 5);
|
||||
assert.ok(f2.b >= 5);
|
||||
});
|
||||
|
||||
it('safe_extend should copy primitive values and arrays', function () {
|
||||
const dest = { x: 1 };
|
||||
const src = { y: 2, arr: [1, 2, 3] };
|
||||
safe_extend(false, dest, src);
|
||||
assert.strictEqual(dest['x'], 1);
|
||||
assert.strictEqual(dest['y'], 2);
|
||||
assert.deepStrictEqual(dest['arr'], [1, 2, 3]);
|
||||
assert.ok(dest['arr'] !== src['arr']); // should be copy
|
||||
});
|
||||
|
||||
it('printFlags should format flag values', function () {
|
||||
const names = ['CARRY', 'ZERO', 'IRQ_DISABLE', 'DECIMAL'];
|
||||
const result = printFlags(0x01, names, false);
|
||||
assert.ok(result.includes('CARRY'));
|
||||
const result2 = printFlags(0x00, names, false);
|
||||
assert.ok(result2.includes('-'));
|
||||
});
|
||||
|
||||
it('highlightDifferences should mark changes in text', function () {
|
||||
const diff = highlightDifferences('hello world', 'hello there');
|
||||
assert.ok(diff.includes('hilite'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import {
|
||||
rgbToPixel, DMG_PALETTE, DMG_PALETTE_RGB, DMG_PALETTE_RGB24, dmgShadeFromRgb
|
||||
} from "../../src/common/gbpalette";
|
||||
import { SourceFile } from "../../src/common/workertypes";
|
||||
import { ProbeRecorder, ProbeFlags } from "../../src/common/probe";
|
||||
|
||||
describe('Game Boy Palette Functions', function () {
|
||||
it('rgbToPixel should convert RGB to packed pixel format', function () {
|
||||
const white = rgbToPixel(255, 255, 255);
|
||||
assert.strictEqual(white & 0xFFFFFF, 0xFFFFFF);
|
||||
assert.strictEqual((white >> 24) & 0xFF, 0xFF); // alpha
|
||||
|
||||
const black = rgbToPixel(0, 0, 0);
|
||||
assert.strictEqual(black & 0xFFFFFF, 0x000000);
|
||||
|
||||
const red = rgbToPixel(255, 0, 0);
|
||||
assert.strictEqual(red & 0xFF, 255); // red channel
|
||||
assert.strictEqual((red >> 8) & 0xFF, 0); // green channel
|
||||
assert.strictEqual((red >> 16) & 0xFF, 0); // blue channel
|
||||
});
|
||||
|
||||
it('DMG_PALETTE should contain 4 shades', function () {
|
||||
assert.strictEqual(DMG_PALETTE.length, 4);
|
||||
// Should be in light to dark order
|
||||
assert.ok(DMG_PALETTE[0] > DMG_PALETTE[3]); // lightest > darkest numerically
|
||||
});
|
||||
|
||||
it('DMG_PALETTE_RGB should match DMG_PALETTE', function () {
|
||||
assert.strictEqual(DMG_PALETTE_RGB.length, 4);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const [r, g, b] = DMG_PALETTE_RGB[i];
|
||||
const expected = rgbToPixel(r, g, b);
|
||||
assert.strictEqual(DMG_PALETTE[i], expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('DMG_PALETTE_RGB24 should be RGB values without alpha', function () {
|
||||
assert.strictEqual(DMG_PALETTE_RGB24.length, 4);
|
||||
assert.strictEqual(DMG_PALETTE_RGB24[0], 0xd0d884); // lightest
|
||||
assert.strictEqual(DMG_PALETTE_RGB24[3], 0x2d3122); // darkest
|
||||
});
|
||||
|
||||
it('dmgShadeFromRgb should map greyscale to shade index', function () {
|
||||
// Very light colors should map to shade 0
|
||||
assert.strictEqual(dmgShadeFromRgb(255, 255, 255), 0);
|
||||
assert.strictEqual(dmgShadeFromRgb(200, 200, 200), 0);
|
||||
|
||||
// Medium-light should be shade 1
|
||||
assert.strictEqual(dmgShadeFromRgb(128, 128, 128), 1);
|
||||
|
||||
// Medium-dark should be shade 2
|
||||
assert.strictEqual(dmgShadeFromRgb(80, 80, 80), 2);
|
||||
|
||||
// Very dark should be shade 3
|
||||
assert.strictEqual(dmgShadeFromRgb(0, 0, 0), 3);
|
||||
assert.strictEqual(dmgShadeFromRgb(30, 30, 30), 3);
|
||||
});
|
||||
|
||||
it('dmgShadeFromRgb should work with non-greyscale colors', function () {
|
||||
// Red (255,0,0) avg=85 → 2
|
||||
assert.strictEqual(dmgShadeFromRgb(255, 0, 0), 2);
|
||||
// Red (128,0,0) avg≈43 → 3
|
||||
assert.strictEqual(dmgShadeFromRgb(128, 0, 0), 3);
|
||||
|
||||
// Green (0,255,0) avg=85 → 2
|
||||
assert.strictEqual(dmgShadeFromRgb(0, 255, 0), 2);
|
||||
// Green (0,128,0) avg≈43 → 3
|
||||
assert.strictEqual(dmgShadeFromRgb(0, 128, 0), 3);
|
||||
|
||||
// Blue (0,0,255) avg=85 → 2
|
||||
assert.strictEqual(dmgShadeFromRgb(0, 0, 255), 2);
|
||||
// Blue (0,0,128) avg≈43 → 3
|
||||
assert.strictEqual(dmgShadeFromRgb(0, 0, 128), 3);
|
||||
});
|
||||
|
||||
it('dmgShadeFromRgb thresholds should be accurate', function () {
|
||||
// Test boundary at 192
|
||||
assert.strictEqual(dmgShadeFromRgb(192, 192, 192), 0);
|
||||
assert.strictEqual(dmgShadeFromRgb(191, 191, 191), 1);
|
||||
|
||||
// Test boundary at 128
|
||||
assert.strictEqual(dmgShadeFromRgb(128, 128, 128), 1);
|
||||
assert.strictEqual(dmgShadeFromRgb(127, 127, 127), 2);
|
||||
|
||||
// Test boundary at 64
|
||||
assert.strictEqual(dmgShadeFromRgb(64, 64, 64), 2);
|
||||
assert.strictEqual(dmgShadeFromRgb(63, 63, 63), 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SourceFile Class', function () {
|
||||
it('should create SourceFile with lines and text', function () {
|
||||
const lines = [
|
||||
{ line: 1, offset: 0, insns: 'LDA #$00' },
|
||||
{ line: 2, offset: 2, insns: 'STA $80' },
|
||||
{ line: 3, offset: 4, insns: 'RTS' },
|
||||
];
|
||||
const text = 'LDA #$00\nSTA $80\nRTS\n';
|
||||
const sf = new SourceFile(lines, text);
|
||||
|
||||
assert.strictEqual(sf.lines.length, 3);
|
||||
assert.strictEqual(sf.text, text);
|
||||
});
|
||||
|
||||
it('should build offset to line mapping', function () {
|
||||
const lines = [
|
||||
{ line: 1, offset: 0, insns: 'LDA #$00' },
|
||||
{ line: 2, offset: 2, insns: 'STA $80' },
|
||||
{ line: 3, offset: 4, insns: 'RTS' },
|
||||
];
|
||||
const sf = new SourceFile(lines, '');
|
||||
|
||||
assert.ok(sf.offset2loc[0]);
|
||||
assert.ok(sf.offset2loc[2]);
|
||||
assert.ok(sf.offset2loc[4]);
|
||||
});
|
||||
|
||||
it('should build line to offset mapping', function () {
|
||||
const lines = [
|
||||
{ line: 1, offset: 0 },
|
||||
{ line: 2, offset: 2 },
|
||||
{ line: 3, offset: 4 },
|
||||
];
|
||||
const sf = new SourceFile(lines, '');
|
||||
|
||||
assert.strictEqual(sf.line2offset[1], 0);
|
||||
assert.strictEqual(sf.line2offset[2], 2);
|
||||
assert.strictEqual(sf.line2offset[3], 4);
|
||||
});
|
||||
|
||||
it('should handle negative offsets gracefully', function () {
|
||||
const lines = [
|
||||
{ line: 1, offset: -1 },
|
||||
{ line: 2, offset: 0 },
|
||||
{ line: 3, offset: 2 },
|
||||
];
|
||||
const sf = new SourceFile(lines, '');
|
||||
|
||||
// Negative offsets should not be added to mapping
|
||||
assert.ok(!sf.offset2loc[-1]);
|
||||
assert.ok(sf.offset2loc[0]);
|
||||
});
|
||||
|
||||
it('findLineForOffset should locate source line by address', function () {
|
||||
const lines = [
|
||||
{ line: 1, offset: 0, insns: 'LDA #$00' },
|
||||
{ line: 2, offset: 2, insns: 'STA $80' },
|
||||
{ line: 3, offset: 4, insns: 'RTS' },
|
||||
];
|
||||
const sf = new SourceFile(lines, '');
|
||||
|
||||
const line0 = sf.findLineForOffset(0, 0);
|
||||
assert.strictEqual(line0.line, 1);
|
||||
assert.strictEqual(line0.offset, 0);
|
||||
|
||||
const line2 = sf.findLineForOffset(2, 0);
|
||||
assert.strictEqual(line2.line, 2);
|
||||
assert.strictEqual(line2.offset, 2);
|
||||
});
|
||||
|
||||
it('findLineForOffset should look behind for nearest line', function () {
|
||||
const lines = [
|
||||
{ line: 1, offset: 0, insns: 'LDA #$00' },
|
||||
{ line: 2, offset: 2, insns: 'STA $80' },
|
||||
{ line: 3, offset: 4, insns: 'RTS' },
|
||||
];
|
||||
const sf = new SourceFile(lines, '');
|
||||
|
||||
// Looking for offset 3 with lookbehind=1 should find offset 2
|
||||
const line = sf.findLineForOffset(3, 1);
|
||||
assert.strictEqual(line.line, 2);
|
||||
assert.strictEqual(line.offset, 2);
|
||||
});
|
||||
|
||||
it('findLineForOffset should return null if not found', function () {
|
||||
const lines = [
|
||||
{ line: 1, offset: 0 },
|
||||
{ line: 2, offset: 2 },
|
||||
];
|
||||
const sf = new SourceFile(lines, '');
|
||||
|
||||
// Looking for offset 10 should not find anything
|
||||
const line = sf.findLineForOffset(10, 0);
|
||||
assert.strictEqual(line, null);
|
||||
});
|
||||
|
||||
it('lineCount should return number of lines', function () {
|
||||
const lines = [
|
||||
{ line: 1, offset: 0 },
|
||||
{ line: 2, offset: 2 },
|
||||
{ line: 3, offset: 4 },
|
||||
];
|
||||
const sf = new SourceFile(lines, '');
|
||||
|
||||
assert.strictEqual(sf.lineCount(), 3);
|
||||
});
|
||||
|
||||
it('should handle duplicate offsets (first wins)', function () {
|
||||
const lines = [
|
||||
{ line: 1, offset: 0, insns: 'LDA #$00' },
|
||||
{ line: 2, offset: 0, insns: 'STA $80' }, // Same offset
|
||||
{ line: 3, offset: 2, insns: 'RTS' },
|
||||
];
|
||||
const sf = new SourceFile(lines, '');
|
||||
|
||||
// First line at offset 0 should be returned
|
||||
const line = sf.findLineForOffset(0, 0);
|
||||
assert.strictEqual(line.line, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProbeRecorder Class', function () {
|
||||
class MockMachine {
|
||||
probe: any;
|
||||
connectProbe(probe) {
|
||||
this.probe = probe;
|
||||
}
|
||||
}
|
||||
|
||||
it('should create ProbeRecorder with buffer', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 1024);
|
||||
|
||||
assert.ok(pr.buf);
|
||||
assert.strictEqual(pr.buf.length, 1024);
|
||||
assert.strictEqual(pr.idx, 0);
|
||||
});
|
||||
|
||||
it('should log data to buffer', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 100);
|
||||
|
||||
pr.log(0x12000080); // MEM_READ at address 0x80
|
||||
assert.strictEqual(pr.idx, 1);
|
||||
assert.strictEqual(pr.buf[0], 0x12000080);
|
||||
|
||||
pr.log(0x13000081); // MEM_WRITE at address 0x81
|
||||
assert.strictEqual(pr.idx, 2);
|
||||
assert.strictEqual(pr.buf[1], 0x13000081);
|
||||
});
|
||||
|
||||
it('should not exceed buffer length', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 2);
|
||||
|
||||
pr.log(0x12000080);
|
||||
pr.log(0x13000081);
|
||||
pr.log(0x14000082); // Should not write (buffer full)
|
||||
|
||||
assert.strictEqual(pr.idx, 2);
|
||||
assert.strictEqual(pr.buf[0], 0x12000080);
|
||||
assert.strictEqual(pr.buf[1], 0x13000081);
|
||||
});
|
||||
|
||||
it('should retrieve last operation', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 100);
|
||||
|
||||
pr.log(0x12000080);
|
||||
pr.log(0x13000081);
|
||||
|
||||
const lastOp = pr.lastOp();
|
||||
assert.strictEqual(lastOp, 0x13000000); // MEM_WRITE flag
|
||||
});
|
||||
|
||||
it('should retrieve last address', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 100);
|
||||
|
||||
pr.log(0x12000080);
|
||||
pr.log(0x13AABBCC);
|
||||
|
||||
const lastAddr = pr.lastAddr();
|
||||
assert.strictEqual(lastAddr, 0xAABBCC);
|
||||
});
|
||||
|
||||
it('should return -1 for lastOp on empty buffer', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 100);
|
||||
|
||||
const lastOp = pr.lastOp();
|
||||
assert.strictEqual(lastOp, -1);
|
||||
});
|
||||
|
||||
it('should clear buffer', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 100);
|
||||
|
||||
pr.log(0x12000080);
|
||||
pr.log(0x13000081);
|
||||
assert.strictEqual(pr.idx, 2);
|
||||
|
||||
pr.clear();
|
||||
assert.strictEqual(pr.idx, 0);
|
||||
});
|
||||
|
||||
it('should reset buffer', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 100);
|
||||
|
||||
pr.log(0x12000080);
|
||||
assert.strictEqual(pr.buf.length, 100);
|
||||
|
||||
pr.reset(200);
|
||||
assert.strictEqual(pr.buf.length, 200);
|
||||
assert.strictEqual(pr.idx, 0);
|
||||
});
|
||||
|
||||
it('should relog last entry', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 100);
|
||||
|
||||
pr.log(0x12000080);
|
||||
pr.relog(0x13000080);
|
||||
|
||||
assert.strictEqual(pr.idx, 1);
|
||||
assert.strictEqual(pr.buf[0], 0x13000080);
|
||||
});
|
||||
|
||||
it('should track scanline', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 100);
|
||||
|
||||
assert.strictEqual(pr.sl, 0);
|
||||
pr.sl = 100;
|
||||
assert.strictEqual(pr.sl, 100);
|
||||
});
|
||||
|
||||
it('should track stack pointer', function () {
|
||||
const machine = new MockMachine();
|
||||
const pr = new ProbeRecorder(machine, 100);
|
||||
|
||||
assert.strictEqual(pr.cur_sp, -1);
|
||||
pr.cur_sp = 0xFF;
|
||||
assert.strictEqual(pr.cur_sp, 0xFF);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProbeFlags Enumeration', function () {
|
||||
it('should have correct flag values', function () {
|
||||
assert.strictEqual(ProbeFlags.CLOCKS, 0x00000000);
|
||||
assert.strictEqual(ProbeFlags.EXECUTE, 0x01000000);
|
||||
assert.strictEqual(ProbeFlags.INTERRUPT, 0x08000000);
|
||||
assert.strictEqual(ProbeFlags.MEM_READ, 0x12000000);
|
||||
assert.strictEqual(ProbeFlags.MEM_WRITE, 0x13000000);
|
||||
assert.strictEqual(ProbeFlags.FRAME, 0x7f000000);
|
||||
});
|
||||
|
||||
it('should extract operation from probe value', function () {
|
||||
const probeValue = 0x12000080;
|
||||
const opcode = probeValue & 0xFF000000;
|
||||
assert.strictEqual(opcode, ProbeFlags.MEM_READ);
|
||||
|
||||
const address = probeValue & 0xFFFFFF;
|
||||
assert.strictEqual(address, 0x80);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import {
|
||||
noise, getNoiseSeed, setNoiseSeed, KeyFlags, RAM, EmuHalt,
|
||||
AnimationTimer, dumpRAM, Keys, makeKeycodeMap, newKeyboardHandler,
|
||||
_setKeyboardEvents, padBytes, AddressDecoder, newAddressDecoder,
|
||||
getMousePos, drawCrosshair, gtia_ntsc_to_rgb,
|
||||
} from "../../src/common/emu";
|
||||
|
||||
describe('Noise PRNG', function () {
|
||||
it('should produce a deterministic sequence from seed 1', function () {
|
||||
setNoiseSeed(1);
|
||||
const seq = [];
|
||||
for (let i = 0; i < 8; i++) seq.push(noise());
|
||||
assert.deepStrictEqual(seq, [33, 1, 34, 25, 250, 201, 8, 180]);
|
||||
});
|
||||
|
||||
it('should track and restore the seed', function () {
|
||||
setNoiseSeed(0x1234);
|
||||
assert.strictEqual(getNoiseSeed(), 0x1234);
|
||||
noise();
|
||||
assert.notStrictEqual(getNoiseSeed(), 0x1234);
|
||||
});
|
||||
|
||||
it('should return 0 forever for seed 0', function () {
|
||||
setNoiseSeed(0);
|
||||
assert.strictEqual(noise(), 0);
|
||||
assert.strictEqual(noise(), 0);
|
||||
assert.strictEqual(noise(), 0);
|
||||
assert.strictEqual(getNoiseSeed(), 0);
|
||||
});
|
||||
|
||||
it('should always return a byte', function () {
|
||||
setNoiseSeed(1);
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const n = noise();
|
||||
assert.ok(n >= 0 && n <= 255);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('KeyFlags Enumeration', function () {
|
||||
it('should have correct flag values', function () {
|
||||
assert.strictEqual(KeyFlags.KeyDown, 1);
|
||||
assert.strictEqual(KeyFlags.Shift, 2);
|
||||
assert.strictEqual(KeyFlags.Ctrl, 4);
|
||||
assert.strictEqual(KeyFlags.Alt, 8);
|
||||
assert.strictEqual(KeyFlags.Meta, 16);
|
||||
assert.strictEqual(KeyFlags.KeyUp, 64);
|
||||
assert.strictEqual(KeyFlags.KeyPress, 128);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RAM Class', function () {
|
||||
it('should allocate zero-initialized memory', function () {
|
||||
const ram = new RAM(16);
|
||||
assert.ok(ram.mem instanceof Uint8Array);
|
||||
assert.strictEqual(ram.mem.length, 16);
|
||||
assert.strictEqual(ram.mem[0], 0);
|
||||
assert.strictEqual(ram.mem[15], 0);
|
||||
});
|
||||
|
||||
it('should allow reading and writing', function () {
|
||||
const ram = new RAM(4);
|
||||
ram.mem[2] = 0xAA;
|
||||
assert.strictEqual(ram.mem[2], 0xAA);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EmuHalt', function () {
|
||||
it('should be an Error subclass with default properties', function () {
|
||||
const e = new EmuHalt("Halt!");
|
||||
assert.ok(e instanceof Error);
|
||||
assert.ok(e instanceof EmuHalt);
|
||||
assert.strictEqual(e.message, "Halt!");
|
||||
assert.ok(e.squelchError);
|
||||
assert.strictEqual(e.$loc, undefined);
|
||||
});
|
||||
|
||||
it('should carry a source location', function () {
|
||||
const e = new EmuHalt("bad opcode", { line: 42 } as any);
|
||||
assert.deepStrictEqual(e.$loc, { line: 42 });
|
||||
});
|
||||
|
||||
it('should be catchable via instanceof', function () {
|
||||
let caught = null;
|
||||
try {
|
||||
throw new EmuHalt("boom");
|
||||
} catch (err) {
|
||||
caught = err;
|
||||
}
|
||||
assert.ok(caught instanceof EmuHalt);
|
||||
assert.ok(caught instanceof Error);
|
||||
assert.strictEqual(caught.message, "boom");
|
||||
});
|
||||
});
|
||||
|
||||
describe('dumpRAM', function () {
|
||||
it('should format a Uint8Array dump', function () {
|
||||
const ram = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
|
||||
assert.strictEqual(dumpRAM(ram, 0, 16),
|
||||
"$00: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\n");
|
||||
});
|
||||
|
||||
it('should show the base offset in addresses', function () {
|
||||
const ram = new Uint8Array([1, 2, 3]);
|
||||
assert.strictEqual(dumpRAM(ram, 0x8000, 8),
|
||||
"$8000: 01 02 03\n");
|
||||
});
|
||||
|
||||
it('should format a Uint16Array dump', function () {
|
||||
const ram = new Uint16Array([0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x1111, 0x2222, 0x3333, 0x4444]);
|
||||
assert.strictEqual(dumpRAM(ram, 0, 16),
|
||||
"$00: 1234 5678 9ABC DEF0 1111 2222 3333 4444\n$08:\n");
|
||||
});
|
||||
|
||||
it('should format a Float32Array dump with precision', function () {
|
||||
const ram = new Float32Array([1.5, 2.25, 3.125, 0.5]);
|
||||
assert.strictEqual(dumpRAM(ram, 0, 8),
|
||||
"$00: 1.5000000 2.2500000 3.1250000 0.50000000\n$04:\n");
|
||||
});
|
||||
|
||||
it('should handle plain arrays (BYTES_PER_ELEMENT fallback)', function () {
|
||||
assert.strictEqual(dumpRAM([0x0A, 0x0B], 0, 2), "$00: 0A 0B\n");
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keys Table', function () {
|
||||
it('should define directional and action keys', function () {
|
||||
assert.strictEqual(Keys.UP.c, 38);
|
||||
assert.strictEqual(Keys.DOWN.c, 40);
|
||||
assert.strictEqual(Keys.LEFT.c, 37);
|
||||
assert.strictEqual(Keys.RIGHT.c, 39);
|
||||
assert.strictEqual(Keys.A.c, 32); // Space
|
||||
assert.strictEqual(Keys.B.c, 16); // Shift
|
||||
assert.strictEqual(Keys.SELECT.c, 220);
|
||||
assert.strictEqual(Keys.START.c, 13);
|
||||
assert.strictEqual(Keys.ANYKEY.c, 0);
|
||||
});
|
||||
|
||||
it('should annotate gamepad mapping metadata', function () {
|
||||
assert.strictEqual(Keys.UP.plyr, 0);
|
||||
assert.strictEqual(Keys.UP.button, 12);
|
||||
assert.strictEqual(Keys.UP.yaxis, -1);
|
||||
assert.strictEqual(Keys.RIGHT.xaxis, 1);
|
||||
assert.strictEqual(Keys.A.button, 0);
|
||||
assert.strictEqual(Keys.P2_UP.plyr, 1);
|
||||
assert.strictEqual(Keys.P2_UP.c, 87); // W
|
||||
});
|
||||
|
||||
it('should define function keys and misc keys', function () {
|
||||
assert.strictEqual(Keys.VK_ESCAPE.c, 27);
|
||||
assert.strictEqual(Keys.VK_F1.c, 112);
|
||||
assert.strictEqual(Keys.VK_F12.c, 123);
|
||||
assert.strictEqual(Keys.VK_ENTER.c, 13);
|
||||
assert.strictEqual(Keys.VK_SPACE.c, 32);
|
||||
});
|
||||
|
||||
it('should give every key a name and numeric code', function () {
|
||||
const entries = Object.keys(Keys);
|
||||
assert.ok(entries.length >= 100);
|
||||
for (const name of entries) {
|
||||
assert.ok(name.length > 0);
|
||||
const def = Keys[name];
|
||||
assert.strictEqual(typeof def.c, 'number');
|
||||
assert.ok(def.n.length > 0, `key ${name} should have a name`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeKeycodeMap', function () {
|
||||
it('should build a map keyed by keycode', function () {
|
||||
const map = makeKeycodeMap([[Keys.A, 0, 0x01], [Keys.B, 0, 0x02]]);
|
||||
assert.ok(map instanceof Map);
|
||||
assert.strictEqual(map[32].index, 0);
|
||||
assert.strictEqual(map[32].mask, 0x01);
|
||||
assert.strictEqual(map[32].def, Keys.A);
|
||||
assert.strictEqual(map[16].index, 0);
|
||||
assert.strictEqual(map[16].mask, 0x02);
|
||||
assert.strictEqual(map[9999], undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('newKeyboardHandler', function () {
|
||||
it('should set switches on keydown and clear on keyup', function () {
|
||||
const map = makeKeycodeMap([[Keys.A, 0, 0x01]]);
|
||||
const switches = new Uint8Array(2);
|
||||
const handler = newKeyboardHandler(switches, map);
|
||||
handler(32, 0, KeyFlags.KeyDown); // A (Space)
|
||||
assert.strictEqual(switches[0], 0x01);
|
||||
handler(32, 0, KeyFlags.KeyDown); // idempotent
|
||||
assert.strictEqual(switches[0], 0x01);
|
||||
handler(32, 0, KeyFlags.KeyUp);
|
||||
assert.strictEqual(switches[0], 0x00);
|
||||
});
|
||||
|
||||
it('should support multiple keys and switch indices', function () {
|
||||
const map = makeKeycodeMap([
|
||||
[Keys.A, 0, 0x01],
|
||||
[Keys.B, 0, 0x02],
|
||||
[Keys.UP, 1, 0x80],
|
||||
]);
|
||||
const switches = new Uint8Array(2);
|
||||
const handler = newKeyboardHandler(switches, map);
|
||||
handler(32, 0, KeyFlags.KeyDown);
|
||||
handler(16, 0, KeyFlags.KeyDown);
|
||||
handler(38, 0, KeyFlags.KeyDown);
|
||||
assert.strictEqual(switches[0], 0x03);
|
||||
assert.strictEqual(switches[1], 0x80);
|
||||
handler(32, 0, KeyFlags.KeyUp);
|
||||
assert.strictEqual(switches[0], 0x02);
|
||||
});
|
||||
|
||||
it('should support active-low (negative mask) switches', function () {
|
||||
const map = makeKeycodeMap([[Keys.START, 0, -0x01]]);
|
||||
const switches = new Uint8Array(1);
|
||||
const handler = newKeyboardHandler(switches, map);
|
||||
switches[0] = 0x01;
|
||||
handler(13, 0, KeyFlags.KeyDown); // active-low press clears the bit
|
||||
assert.strictEqual(switches[0], 0x00);
|
||||
handler(13, 0, KeyFlags.KeyUp); // release sets the bit
|
||||
assert.strictEqual(switches[0], 0x01);
|
||||
});
|
||||
|
||||
it('should fall back to ANYKEY entry for unknown keys', function () {
|
||||
const map = makeKeycodeMap([[Keys.ANYKEY, 0, 0x40]]);
|
||||
const switches = new Uint8Array(1);
|
||||
const handler = newKeyboardHandler(switches, map);
|
||||
handler(9999, 0, KeyFlags.KeyDown);
|
||||
assert.strictEqual(switches[0], 0x40);
|
||||
handler(9999, 0, KeyFlags.KeyUp);
|
||||
assert.strictEqual(switches[0], 0x00);
|
||||
});
|
||||
|
||||
it('should ignore unmapped keys when no ANYKEY entry exists', function () {
|
||||
const map = makeKeycodeMap([[Keys.A, 0, 0x01]]);
|
||||
const switches = new Uint8Array(1);
|
||||
const handler = newKeyboardHandler(switches, map);
|
||||
handler(9999, 0, KeyFlags.KeyDown);
|
||||
assert.strictEqual(switches[0], 0x00);
|
||||
});
|
||||
|
||||
it('should invoke the callback function with key info', function () {
|
||||
const map = makeKeycodeMap([[Keys.A, 0, 0x01]]);
|
||||
const switches = new Uint8Array(1);
|
||||
const calls = [];
|
||||
const handler = newKeyboardHandler(switches, map, (o, key, code, flags) => {
|
||||
calls.push([o && o.def.n, key, code, flags]);
|
||||
});
|
||||
handler(32, 65, KeyFlags.KeyDown | KeyFlags.Shift);
|
||||
assert.deepStrictEqual(calls, [['Space', 32, 65, KeyFlags.KeyDown | KeyFlags.Shift]]);
|
||||
});
|
||||
|
||||
it('should invoke callback for unmapped keys only with alwaysfunc', function () {
|
||||
const map = makeKeycodeMap([[Keys.A, 0, 0x01]]);
|
||||
const switches = new Uint8Array(1);
|
||||
const calls = [];
|
||||
const handler = newKeyboardHandler(switches, map, (o) => {
|
||||
calls.push(o ? o.def.n : null);
|
||||
}, true);
|
||||
handler(9999, 0, KeyFlags.KeyDown);
|
||||
assert.deepStrictEqual(calls, [null]);
|
||||
assert.strictEqual(switches[0], 0x00);
|
||||
});
|
||||
|
||||
it('should pass through to callback when no map is given', function () {
|
||||
const switches = new Uint8Array(1);
|
||||
const calls = [];
|
||||
const handler = newKeyboardHandler(switches, null, (o, key, code, flags) => {
|
||||
calls.push([o, key, code, flags]);
|
||||
});
|
||||
handler(65, 65, KeyFlags.KeyDown);
|
||||
assert.deepStrictEqual(calls, [[null, 65, 65, KeyFlags.KeyDown]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_setKeyboardEvents', function () {
|
||||
it('should wire keydown and keyup handlers', function () {
|
||||
const calls = [];
|
||||
const canvas: any = { onkeydown: null, onkeyup: null };
|
||||
_setKeyboardEvents(canvas, (which, keyCode, flags) => {
|
||||
calls.push([which, keyCode, flags]);
|
||||
});
|
||||
canvas.onkeydown({ which: 65, keyCode: 65, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, preventDefault() { } });
|
||||
assert.deepStrictEqual(calls[0], [65, 65, KeyFlags.KeyDown]);
|
||||
canvas.onkeyup({ which: 65, keyCode: 65, shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, preventDefault() { } });
|
||||
assert.deepStrictEqual(calls[1], [65, 65, KeyFlags.KeyUp | KeyFlags.Shift]);
|
||||
});
|
||||
|
||||
it('should pass through modifier flags', function () {
|
||||
const calls = [];
|
||||
const canvas: any = { onkeydown: null, onkeyup: null };
|
||||
_setKeyboardEvents(canvas, (which, keyCode, flags) => {
|
||||
calls.push(flags);
|
||||
});
|
||||
canvas.onkeydown({ which: 13, keyCode: 13, shiftKey: true, ctrlKey: true, altKey: true, metaKey: true, preventDefault() { } });
|
||||
assert.strictEqual(calls[0], KeyFlags.KeyDown | KeyFlags.Shift | KeyFlags.Ctrl | KeyFlags.Alt | KeyFlags.Meta);
|
||||
});
|
||||
|
||||
it('should preventDefault on unmodified keydown only', function () {
|
||||
const prevented = [];
|
||||
const canvas: any = { onkeydown: null, onkeyup: null };
|
||||
_setKeyboardEvents(canvas, () => { });
|
||||
canvas.onkeydown({ which: 32, keyCode: 32, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, preventDefault() { prevented.push('plain'); } });
|
||||
canvas.onkeydown({ which: 32, keyCode: 32, shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, preventDefault() { prevented.push('shift'); } });
|
||||
assert.deepStrictEqual(prevented, ['plain']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('padBytes', function () {
|
||||
it('should pad at the end by default', function () {
|
||||
const out = padBytes([1, 2, 3], 8);
|
||||
assert.ok(out instanceof Uint8Array);
|
||||
assert.deepStrictEqual(Array.from(out), [1, 2, 3, 0, 0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
it('should pad at the start when requested', function () {
|
||||
const out = padBytes(new Uint8Array([1, 2, 3]), 8, true);
|
||||
assert.deepStrictEqual(Array.from(out), [0, 0, 0, 0, 0, 1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should return unchanged data when length matches', function () {
|
||||
const out = padBytes([0xAA, 0xBB], 2);
|
||||
assert.deepStrictEqual(Array.from(out), [0xAA, 0xBB]);
|
||||
});
|
||||
|
||||
it('should throw when data is too long', function () {
|
||||
assert.throws(() => padBytes([1, 2, 3], 2), /Data too long, 3 > 2/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AddressDecoder', function () {
|
||||
it('should decode reads from a table', function () {
|
||||
const ram = new Uint8Array(256);
|
||||
ram[0x10] = 0x5A;
|
||||
const decode = newAddressDecoder([[0x0000, 0x00FF, 0, (a) => ram[a]]]);
|
||||
assert.strictEqual(decode(0x10), 0x5A);
|
||||
assert.strictEqual(decode(0x99), 0x00);
|
||||
});
|
||||
|
||||
it('should return default value for out-of-range addresses', function () {
|
||||
const ram = new Uint8Array(256);
|
||||
const decode = newAddressDecoder([[0x0000, 0x00FF, 0, (a) => ram[a]]]);
|
||||
assert.strictEqual(decode(0x1000), 0);
|
||||
});
|
||||
|
||||
it('should apply address masks', function () {
|
||||
const ram = new Uint8Array(0x800);
|
||||
ram[0x000] = 0x11;
|
||||
ram[0x7FF] = 0x22;
|
||||
const decode = newAddressDecoder([[0x0000, 0x0FFF, 0x07FF, (a) => ram[a]]]);
|
||||
assert.strictEqual(decode(0x0000), 0x11);
|
||||
assert.strictEqual(decode(0x0800), 0x11); // 0x0800 & 0x07FF = 0x0000
|
||||
assert.strictEqual(decode(0x0FFF), 0x22); // 0x0FFF & 0x07FF = 0x07FF
|
||||
});
|
||||
|
||||
it('should support write functions and mask the result', function () {
|
||||
const ram = new Uint8Array(0x800);
|
||||
const decode = newAddressDecoder([[0x0000, 0x0FFF, 0x07FF,
|
||||
(a, v) => { if (v !== undefined) ram[a] = v; return ram[a]; }]]);
|
||||
decode(0x0801, 0x99); // writes to 0x0801 & 0x07FF = 0x0001
|
||||
assert.strictEqual(ram[0x001], 0x99);
|
||||
assert.strictEqual(ram[0x801], undefined);
|
||||
assert.strictEqual(decode(0x0801), 0x99);
|
||||
});
|
||||
|
||||
it('should apply a global mask before decoding', function () {
|
||||
const ram = new Uint8Array(0x100);
|
||||
ram[0x00] = 0x11;
|
||||
ram[0x20] = 0x22;
|
||||
const decode = newAddressDecoder([[0x0000, 0x00FF, 0, (a) => ram[a]]],
|
||||
{ gmask: 0x0FFF, defaultval: 0xFF });
|
||||
assert.strictEqual(decode(0x1000), 0x11); // 0x1000 & 0x0FFF = 0x0000
|
||||
assert.strictEqual(decode(0x0020), 0x22);
|
||||
assert.strictEqual(decode(0x1234), 0xFF); // masked to 0x0234, out of range
|
||||
});
|
||||
|
||||
it('should handle an empty table with a default value', function () {
|
||||
const decode = newAddressDecoder([], { defaultval: 0xEE });
|
||||
assert.strictEqual(decode(0x0000), 0xEE);
|
||||
const decode2 = newAddressDecoder([]);
|
||||
assert.strictEqual(decode2(0x0000), 0);
|
||||
});
|
||||
|
||||
it('should be usable as a constructor directly', function () {
|
||||
const decode = new (AddressDecoder as any)([[0x0000, 0xFFFF, 0, () => 0x42]]);
|
||||
assert.strictEqual(decode(0x1234), 0x42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AnimationTimer', function () {
|
||||
it('should invoke callback at scheduled intervals', function () {
|
||||
let count = 0;
|
||||
const timer = new AnimationTimer(100, () => count++); // 10ms per frame
|
||||
timer.running = true;
|
||||
timer.nextFrame(50); // ts > nextts(0)
|
||||
assert.strictEqual(count, 1);
|
||||
timer.nextFrame(60); // ts(60) < nextts(100)
|
||||
assert.strictEqual(count, 2);
|
||||
timer.nextFrame(150); // ts > nextts(100)
|
||||
assert.strictEqual(count, 3); //
|
||||
timer.running = false;
|
||||
});
|
||||
|
||||
it('should not invoke callback when not running', function () {
|
||||
let count = 0;
|
||||
const timer = new AnimationTimer(100, () => count++);
|
||||
timer.nextFrame(50);
|
||||
assert.strictEqual(count, 0);
|
||||
timer.running = true;
|
||||
timer.nextFrame(60);
|
||||
assert.strictEqual(count, 1);
|
||||
timer.running = false;
|
||||
});
|
||||
|
||||
it('should catch up after large gaps', function () {
|
||||
let count = 0;
|
||||
const timer = new AnimationTimer(100, () => count++);
|
||||
timer.running = true;
|
||||
timer.nextts = 500;
|
||||
timer.nextFrame(3000); // > 1s gap: should catch up
|
||||
assert.strictEqual(count, 1);
|
||||
assert.strictEqual(timer.nextts, 3000);
|
||||
timer.running = false;
|
||||
});
|
||||
|
||||
it('should set frame timing properties', function () {
|
||||
const timer = new AnimationTimer(60, () => { });
|
||||
assert.strictEqual(timer.frameRate, 60);
|
||||
assert.strictEqual(timer.intervalMsec, 1000 / 60);
|
||||
});
|
||||
|
||||
it('should start/stop and report running state', function () {
|
||||
const timer = new AnimationTimer(60, () => { });
|
||||
assert.strictEqual(timer.isRunning(), false);
|
||||
timer.start();
|
||||
assert.strictEqual(timer.isRunning(), true);
|
||||
timer.stop();
|
||||
assert.strictEqual(timer.isRunning(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('drawCrosshair', function () {
|
||||
it('should no-op without setLineDash (unit testing support)', function () {
|
||||
let called = false;
|
||||
const ctx: any = { fillRect: () => { called = true; } };
|
||||
drawCrosshair(ctx, 10, 20, 2);
|
||||
assert.strictEqual(called, false);
|
||||
});
|
||||
|
||||
it('should draw vertical and horizontal lines', function () {
|
||||
const calls: any[][] = [];
|
||||
const ctx: any = {
|
||||
fillStyle: '', strokeStyle: '', lineWidth: 0,
|
||||
setLineDash: (d) => calls.push(['setLineDash', String(d)]),
|
||||
fillRect: (x, y, w, h) => calls.push(['fillRect', x, y, w, h]),
|
||||
beginPath: () => calls.push(['beginPath']),
|
||||
moveTo: (x, y) => calls.push(['moveTo', x, y]),
|
||||
lineTo: (x, y) => calls.push(['lineTo', x, y]),
|
||||
stroke: () => calls.push(['stroke']),
|
||||
};
|
||||
drawCrosshair(ctx, 10, 20, 2);
|
||||
assert.ok(calls.some(c => c[0] === 'fillRect' && c[1] === 8), 'vertical bar at x-2');
|
||||
assert.ok(calls.some(c => c[0] === 'fillRect' && c[2] === 18), 'horizontal bar at y-2');
|
||||
assert.ok(calls.some(c => c[0] === 'moveTo' && c[1] === 10), 'vertical line at x');
|
||||
assert.ok(calls.some(c => c[0] === 'moveTo' && c[2] === 20), 'horizontal line at y');
|
||||
assert.ok(calls.some(c => c[0] === 'setLineDash' && c[1] === '4,4'), 'dash pattern 2*width');
|
||||
assert.ok(calls.some(c => c[0] === 'stroke'), 'stroke called');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMousePos', function () {
|
||||
const realWindow = (global as any).window;
|
||||
|
||||
after(function () {
|
||||
if (realWindow === undefined) delete (global as any).window;
|
||||
else (global as any).window = realWindow;
|
||||
});
|
||||
|
||||
function stubWindow() {
|
||||
(global as any).window = {
|
||||
getComputedStyle: () => ({
|
||||
borderLeftWidth: '0px', borderRightWidth: '0px',
|
||||
borderTopWidth: '0px', borderBottomWidth: '0px',
|
||||
paddingLeft: '0px', paddingRight: '0px',
|
||||
paddingTop: '0px', paddingBottom: '0px',
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
it('should clamp out-of-bounds coordinates', function () {
|
||||
stubWindow();
|
||||
const canvas: any = {
|
||||
width: 100, height: 100,
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 })
|
||||
};
|
||||
const pos = getMousePos(canvas, { clientX: 10000, clientY: -100 });
|
||||
assert.deepStrictEqual(pos, { x: 100, y: 0 });
|
||||
});
|
||||
|
||||
it('should account for borders and padding', function () {
|
||||
(global as any).window = {
|
||||
getComputedStyle: () => ({
|
||||
borderLeftWidth: '2px', borderRightWidth: '2px',
|
||||
borderTopWidth: '2px', borderBottomWidth: '2px',
|
||||
paddingLeft: '2px', paddingRight: '2px',
|
||||
paddingTop: '2px', paddingBottom: '2px',
|
||||
})
|
||||
};
|
||||
const canvas: any = {
|
||||
width: 100, height: 100,
|
||||
getBoundingClientRect: () => ({ left: 10, top: 10, width: 108, height: 108 })
|
||||
};
|
||||
// content origin = rect.left + border(2) + padding(2) = 14
|
||||
let pos = getMousePos(canvas, { clientX: 14, clientY: 14 });
|
||||
assert.deepStrictEqual(pos, { x: 0, y: 0 });
|
||||
// content far edge
|
||||
pos = getMousePos(canvas, { clientX: 14 + 99, clientY: 14 + 99 });
|
||||
assert.ok(pos.x >= 99 && pos.x <= 100);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user