mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-29 08:05:37 +00:00
639 lines
27 KiB
TypeScript
639 lines
27 KiB
TypeScript
|
|
import * as assert from 'assert';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { Worker } from 'worker_threads';
|
|
import { Rpc } from '../src/rpc';
|
|
import { findRootDir } from '../src/projectinfo';
|
|
import { Builder, ProjectFileProvider } from '../src/buildcore';
|
|
import type { AudioChunk, EmuStatus, FrameEvent, ScriptResult, VcdEvent, ViewEvent } from '../src/emuworker';
|
|
import { WorkerDebugBackend } from '../src/debugbackend';
|
|
import { EmuDebugSession } from '../../src/tools/dapsession';
|
|
import { DapClient } from '../../test/unit/dapclient';
|
|
|
|
// Drives out/emuworker.js in a worker thread, the way the extension host does.
|
|
// Mocha runs from extension/, so BIOS fetches must resolve against ROOT.
|
|
const ROOT = findRootDir(__dirname);
|
|
|
|
function rom(rel: string) {
|
|
return new Uint8Array(fs.readFileSync(path.join(ROOT, 'test/roms', rel)));
|
|
}
|
|
|
|
describe('extension emuworker', function () {
|
|
this.timeout(60000);
|
|
var worker: Worker;
|
|
var rpc: Rpc;
|
|
var frames: FrameEvent[];
|
|
var statuses: EmuStatus[];
|
|
var audios: AudioChunk[];
|
|
var viewEvents: ViewEvent[];
|
|
var vcdEvents: VcdEvent[];
|
|
var reveals: string[];
|
|
|
|
beforeEach(function () {
|
|
worker = new Worker(path.join(__dirname, '..', 'emuworker.js'), { workerData: { rootDir: ROOT } });
|
|
rpc = new Rpc(worker);
|
|
frames = [];
|
|
statuses = [];
|
|
audios = [];
|
|
viewEvents = [];
|
|
vcdEvents = [];
|
|
reveals = [];
|
|
rpc.on('reveal', (id: string) => reveals.push(id));
|
|
rpc.on('vcd', v => vcdEvents.push(v));
|
|
rpc.on('view', v => viewEvents.push(v));
|
|
rpc.on('frame', f => frames.push(f));
|
|
rpc.on('status', s => statuses.push(s));
|
|
rpc.on('audio', c => audios.push(c));
|
|
});
|
|
|
|
afterEach(async function () {
|
|
await worker.terminate();
|
|
});
|
|
|
|
async function waitForFrames(n: number) {
|
|
var start = frames.length;
|
|
for (var i = 0; i < 200 && frames.length - start < n; i++) await new Promise(r => setTimeout(r, 20));
|
|
assert.ok(frames.length - start >= n, `only ${frames.length - start} frames`);
|
|
}
|
|
|
|
async function waitForAudio(n: number) {
|
|
var start = audios.length;
|
|
for (var i = 0; i < 200 && audios.length - start < n; i++) await new Promise(r => setTimeout(r, 20));
|
|
assert.ok(audios.length - start >= n, `only ${audios.length - start} audio chunks`);
|
|
}
|
|
|
|
it('streams NES frames at the screen size', async function () {
|
|
var s = await rpc.call<EmuStatus>('start', 'nes', rom('nes/shoot2.c.rom'));
|
|
assert.equal(s.state, 'running');
|
|
await waitForFrames(30);
|
|
var f = frames[frames.length - 1];
|
|
assert.equal(f.width, 256);
|
|
assert.equal(f.height, 224);
|
|
var px = new Uint32Array(f.pixels);
|
|
assert.equal(px.length, 256 * 224);
|
|
assert.ok(px.some(p => p !== px[0]), 'screen is blank');
|
|
});
|
|
|
|
it('sends the Machine view its debug info only while it is showing', async function () {
|
|
var s = await rpc.call<EmuStatus>('start', 'nes', rom('nes/shoot2.c.rom'));
|
|
assert.ok(s.debugInfo, 'nes has debug info');
|
|
assert.strictEqual(s.screen, true, 'platforms with a screen say so');
|
|
await waitForFrames(10);
|
|
assert.equal(viewEvents.length, 0, 'nothing is sent to a hidden view');
|
|
await rpc.call('setViews', ['machine']);
|
|
await waitForFrames(30);
|
|
assert.ok(viewEvents.length > 0);
|
|
var ev = viewEvents[viewEvents.length - 1];
|
|
assert.equal(ev.id, 'machine');
|
|
assert.ok(ev.sections && ev.sections.length > 0 && ev.sections[0].text.length > 0);
|
|
// a stop sends the state it stopped in, whatever the throttle
|
|
var n = viewEvents.length;
|
|
await rpc.call('pause');
|
|
assert.ok(viewEvents.length > n);
|
|
// hiding it stops the data
|
|
await rpc.call('setViews', []);
|
|
n = viewEvents.length;
|
|
await rpc.call('resume');
|
|
await waitForFrames(30);
|
|
assert.equal(viewEvents.length, n);
|
|
});
|
|
|
|
it('pauses, resumes, resets, and takes keys', async function () {
|
|
await rpc.call('start', 'nes', rom('nes/shoot2.c.rom'));
|
|
await waitForFrames(5);
|
|
var s = await rpc.call<EmuStatus>('pause');
|
|
assert.equal(s.state, 'paused');
|
|
var n = frames.length;
|
|
await new Promise(r => setTimeout(r, 100));
|
|
assert.equal(frames.length, n);
|
|
await rpc.call('key', 32, 32, 1); // space down
|
|
await rpc.call('key', 32, 32, 64); // space up
|
|
s = await rpc.call<EmuStatus>('resume');
|
|
assert.equal(s.state, 'running');
|
|
await waitForFrames(5);
|
|
await rpc.call('reset');
|
|
await waitForFrames(5);
|
|
});
|
|
|
|
it('stops while hidden without changing its paused state', async function () {
|
|
await rpc.call('start', 'nes', rom('nes/shoot2.c.rom'));
|
|
await waitForFrames(5);
|
|
await rpc.call('setVisible', false);
|
|
var n = frames.length;
|
|
await new Promise(r => setTimeout(r, 100));
|
|
assert.equal(frames.length, n);
|
|
assert.equal((await rpc.call<EmuStatus>('status')).state, 'running');
|
|
await rpc.call('setVisible', true);
|
|
await waitForFrames(5);
|
|
// a pause while hidden stays paused when shown
|
|
await rpc.call('setVisible', false);
|
|
await rpc.call('pause');
|
|
await rpc.call('setVisible', true);
|
|
n = frames.length;
|
|
await new Promise(r => setTimeout(r, 100));
|
|
assert.equal(frames.length, n);
|
|
});
|
|
|
|
// Platforms keep global state (Javatari deletes its own start()), so a
|
|
// worker runs one emulator; the extension starts a new worker for each run.
|
|
it('runs the VCS once per worker, and says so on a second start', async function () {
|
|
var s = await rpc.call<EmuStatus>('start', 'vcs', rom('vcs/brickgame.rom'));
|
|
assert.equal(s.state, 'running');
|
|
await waitForFrames(5);
|
|
await assert.rejects(rpc.call('start', 'vcs', rom('vcs/brickgame.rom')), /new worker/);
|
|
});
|
|
|
|
// vcs runs on the vcs.jt4 core, which can stop between instructions
|
|
it('runs the VCS on a core that steps and rewinds', async function () {
|
|
var s = await rpc.call<EmuStatus>('start', 'vcs', rom('vcs/brickgame.rom'));
|
|
assert.equal(s.platform, 'vcs.jt4');
|
|
var caps = await rpc.call<any>('debug', 'capabilities');
|
|
assert.ok(caps.step && caps.rewind, JSON.stringify(caps));
|
|
assert.notEqual(caps.granularity, 'frame');
|
|
});
|
|
|
|
it('loads a BIOS from the asset root', async function () {
|
|
await rpc.call('start', 'atari8-5200', rom('atari8-5200/hello.a.rom'));
|
|
await waitForFrames(10);
|
|
assert.ok(!statuses.some(s => s && s.state === 'halted'), JSON.stringify(statuses));
|
|
});
|
|
|
|
it('streams audio chunks at the resampled rate', async function () {
|
|
var s = await rpc.call<EmuStatus>('start', 'nes', rom('nes/shoot2.c.rom'));
|
|
assert.ok(s.audio, 'status reports an audio rate');
|
|
assert.equal(s.audio.sampleRate, 48000);
|
|
await waitForAudio(2);
|
|
var c = audios[audios.length - 1];
|
|
assert.equal(c.sampleRate, 48000);
|
|
assert.equal(new Float32Array(c.samples).length, 2048);
|
|
});
|
|
|
|
it('stops sending audio while muted', async function () {
|
|
await rpc.call('start', 'nes', rom('nes/shoot2.c.rom'));
|
|
await waitForAudio(1);
|
|
await rpc.call('setMuted', true);
|
|
var n = audios.length;
|
|
await new Promise(r => setTimeout(r, 200));
|
|
assert.equal(audios.length, n);
|
|
await rpc.call('setMuted', false);
|
|
await waitForAudio(1);
|
|
});
|
|
|
|
// $readmem reads project files at load time, so they travel with the ROM
|
|
it('runs a script on a paused machine for the language model tools', async function () {
|
|
var dir = path.join(ROOT, 'presets/nes');
|
|
var read = async (rel: string) => {
|
|
var p = path.join(dir, rel);
|
|
return fs.existsSync(p) ? new Uint8Array(fs.readFileSync(p)) : null;
|
|
};
|
|
var nes = await new Builder(ROOT).build({
|
|
// not hello.c: the build store is global, and another test builds that
|
|
platform: 'nes', mainPath: 'attributes.c', mainText: fs.readFileSync(path.join(dir, 'attributes.c'), 'utf-8'),
|
|
files: new ProjectFileProvider(read, ROOT, 'nes'),
|
|
});
|
|
await rpc.call('start', 'nes', nes.output, nes.files, { paused: true });
|
|
var build = { listings: nes.listings, symbolmap: nes.symbolmap, mainPath: 'attributes.c', paths: nes.paths };
|
|
var r = await rpc.call<ScriptResult>('script', 'run 30; mem main 4', build);
|
|
assert.equal(r.error, undefined);
|
|
assert.equal(r.frame, 30);
|
|
assert.match(r.output, /[0-9a-f]{4}/i);
|
|
assert.deepEqual([...r.png.slice(0, 4)], [0x89, 0x50, 0x4e, 0x47]);
|
|
// no frames streamed: the script drives the machine, not the pacing loop
|
|
assert.equal(frames.length, 0);
|
|
r = await rpc.call<ScriptResult>('script', 'bogus', build);
|
|
assert.match(r.error, /unknown command 'bogus'/);
|
|
});
|
|
|
|
describe('verilog $readmem', function () {
|
|
const MAIN = [
|
|
'module top(clk, reset, hsync, vsync, rgb);',
|
|
' input clk, reset;',
|
|
' output hsync, vsync;',
|
|
' output [3:0] rgb;',
|
|
' reg [7:0] rom[0:3];',
|
|
' initial $readmemh("rom_data.hex", rom);',
|
|
' assign hsync = 0;',
|
|
' assign vsync = 0;',
|
|
' assign rgb = rom[1][3:0];',
|
|
'endmodule',
|
|
'',
|
|
].join('\n');
|
|
var built: Awaited<ReturnType<Builder['build']>>;
|
|
|
|
before(async function () {
|
|
var enc = new TextEncoder();
|
|
var files: { [path: string]: string } = { 'main.v': MAIN, 'rom_data.hex': '00\n0f\n00\n00\n' };
|
|
var read = async (rel: string) => rel in files ? enc.encode(files[rel]) : null;
|
|
built = await new Builder(ROOT).build({
|
|
platform: 'verilog', mainPath: 'main.v', mainText: MAIN,
|
|
files: new ProjectFileProvider(read, ROOT, 'verilog'),
|
|
});
|
|
assert.deepEqual(built.diagnostics, []);
|
|
});
|
|
|
|
it('builds with the data file attached', function () {
|
|
assert.equal(built.files['rom_data.hex'], '00\n0f\n00\n00\n');
|
|
});
|
|
|
|
it('runs with the data file', async function () {
|
|
var s = await rpc.call<EmuStatus>('start', 'verilog', built.output, built.files);
|
|
assert.equal(s.state, 'running');
|
|
await waitForFrames(3);
|
|
assert.ok(!statuses.some(s => s && s.state === 'halted'), JSON.stringify(statuses));
|
|
});
|
|
|
|
it('reports a missing data file without killing the worker', async function () {
|
|
var exited = false;
|
|
worker.on('exit', () => { exited = true; });
|
|
await assert.rejects(rpc.call('start', 'verilog', built.output, {}), /no file "rom_data.hex"/);
|
|
await new Promise(r => setTimeout(r, 200));
|
|
assert.ok(!exited, 'worker exited');
|
|
await rpc.call('status');
|
|
});
|
|
});
|
|
|
|
describe('verilog waveform view', function () {
|
|
const MAIN = [
|
|
'module top(clk, reset, hsync, vsync, rgb, sw);',
|
|
' input clk, reset;',
|
|
' input [3:0] sw;',
|
|
' output hsync, vsync;',
|
|
' output [3:0] rgb;',
|
|
' reg [7:0] n;',
|
|
' always @(posedge clk) n <= reset ? 0 : n + 1;',
|
|
' assign hsync = n[7];',
|
|
' assign vsync = 0;',
|
|
' assign rgb = n[3:0];',
|
|
'endmodule',
|
|
'',
|
|
].join('\n');
|
|
|
|
it('sends the signal trace only while the view is showing, and takes writes', async function () {
|
|
var read = async (rel: string) => null;
|
|
var built = await new Builder(ROOT).build({
|
|
platform: 'verilog', mainPath: 'main.v', mainText: MAIN,
|
|
files: new ProjectFileProvider(read, ROOT, 'verilog'),
|
|
});
|
|
assert.deepEqual(built.diagnostics, []);
|
|
var s = await rpc.call<EmuStatus>('start', 'verilog', built.output, built.files);
|
|
assert.ok(s.waveform, 'verilog has a waveform');
|
|
await waitForFrames(5);
|
|
assert.equal(viewEvents.length, 0, 'nothing is sent to a hidden view');
|
|
await rpc.call('setViews', ['waveform']);
|
|
await waitForFrames(40); // the view refreshes a few times a second
|
|
var ev = viewEvents.filter(e => e.id === 'waveform').pop();
|
|
assert.ok(ev && ev.waveform, 'no waveform event');
|
|
var w = ev!.waveform!;
|
|
var labels = w.meta.map(m => m.label);
|
|
assert.ok(labels.indexOf('n') >= 0 && labels.indexOf('sw') >= 0, labels.join());
|
|
assert.equal(w.data.length % w.meta.length, 0);
|
|
// the counter counts up one a clock
|
|
var n = labels.indexOf('n');
|
|
var vals = Array.from({ length: 10 }, (_, i) => w.data[(i + 20) * w.meta.length + n]);
|
|
for (var i = 1; i < vals.length; i++) assert.equal(vals[i], (vals[i - 1] + 1) & 255, vals.join());
|
|
// a write to an input shows in the next trace
|
|
var sw = labels.indexOf('sw');
|
|
assert.ok(w.meta[sw].input);
|
|
await rpc.call('setSignal', sw, 5);
|
|
await waitForFrames(40);
|
|
w = viewEvents.filter(e => e.id === 'waveform').pop()!.waveform!;
|
|
assert.equal(w.data[10 * w.meta.length + sw], 5);
|
|
});
|
|
});
|
|
|
|
describe('verilog waveform view, while paused and with no video', function () {
|
|
async function build(main: string) {
|
|
var read = async (rel: string) => null;
|
|
var built = await new Builder(ROOT).build({
|
|
platform: 'verilog', mainPath: 'main.v', mainText: main,
|
|
files: new ProjectFileProvider(read, ROOT, 'verilog'),
|
|
});
|
|
assert.deepEqual(built.diagnostics, []);
|
|
return built;
|
|
}
|
|
const COUNTER = [
|
|
'module top(clk, reset, n);',
|
|
' input clk, reset;',
|
|
' output reg [7:0] n;',
|
|
' always @(posedge clk) n <= reset ? 0 : n + 1;',
|
|
'endmodule',
|
|
'',
|
|
].join('\n');
|
|
const WITH_VIDEO = [
|
|
'module top(clk, reset, hsync, vsync, rgb);',
|
|
' input clk, reset;',
|
|
' output hsync, vsync;',
|
|
' output [3:0] rgb;',
|
|
' reg [7:0] n;',
|
|
' always @(posedge clk) n <= reset ? 0 : n + 1;',
|
|
' assign hsync = n[7];',
|
|
' assign vsync = 0;',
|
|
' assign rgb = n[3:0];',
|
|
'endmodule',
|
|
'',
|
|
].join('\n');
|
|
|
|
async function lastWaveform() {
|
|
for (var i = 0; i < 100 && !viewEvents.some(e => e.waveform); i++) await new Promise(r => setTimeout(r, 20));
|
|
return viewEvents.filter(e => e.waveform).pop()!.waveform!;
|
|
}
|
|
|
|
it('asks the host to open the view for a design with no video, once', async function () {
|
|
var built = await build(COUNTER);
|
|
var s = await rpc.call<EmuStatus>('start', 'verilog', built.output, built.files);
|
|
assert.strictEqual(s.screen, false, 'only signals, so no emulator screen');
|
|
await waitForFrames(1).catch(() => { }); // no video: there may be no frames
|
|
for (var i = 0; i < 100 && !reveals.length; i++) await new Promise(r => setTimeout(r, 20));
|
|
assert.deepEqual(reveals, ['waveform']);
|
|
// it traces once the view is showing
|
|
await rpc.call('setViews', ['waveform']);
|
|
var w: any;
|
|
for (i = 0; i < 100; i++) {
|
|
await new Promise(r => setTimeout(r, 20));
|
|
w = viewEvents.filter(e => e.waveform).pop()?.waveform;
|
|
if (w && w.data.some((v: number) => v > 3)) break;
|
|
}
|
|
assert.ok(w && w.data.some((v: number) => v > 3), 'no trace');
|
|
assert.deepEqual(reveals, ['waveform'], 'asked once');
|
|
});
|
|
|
|
it('keeps the screen for a design with sound or controls but no video', async function () {
|
|
var tone = await build([
|
|
'module top(clk, reset, spkr);',
|
|
' input clk, reset;',
|
|
' output spkr;',
|
|
' reg [15:0] c;',
|
|
' always @(posedge clk) c <= reset ? 0 : c + 1;',
|
|
' assign spkr = c[8];',
|
|
'endmodule',
|
|
'',
|
|
].join('\n'));
|
|
var s = await rpc.call<EmuStatus>('start', 'verilog', tone.output, tone.files);
|
|
assert.strictEqual(s.screen, true, 'sound needs the panel');
|
|
assert.ok(s.audio, 'and it makes sound');
|
|
await waitForAudio(3);
|
|
var keys = await build([
|
|
'module top(clk, reset, switches_p1, led);',
|
|
' input clk, reset;',
|
|
' input [7:0] switches_p1;',
|
|
' output led;',
|
|
' assign led = switches_p1[0];',
|
|
'endmodule',
|
|
'',
|
|
].join('\n'));
|
|
await worker.terminate();
|
|
var other = new Worker(path.join(__dirname, '..', 'emuworker.js'), { workerData: { rootDir: ROOT } });
|
|
var rpc2 = new Rpc(other);
|
|
s = await rpc2.call<EmuStatus>('start', 'verilog', keys.output, keys.files);
|
|
assert.strictEqual(s.screen, true, 'controls need the panel');
|
|
await other.terminate();
|
|
});
|
|
|
|
it('shows the last frame of a paused design when the view opens', async function () {
|
|
var built = await build(WITH_VIDEO);
|
|
var s = await rpc.call<EmuStatus>('start', 'verilog', built.output, built.files);
|
|
assert.strictEqual(s.screen, true);
|
|
await waitForFrames(10);
|
|
await rpc.call('pause');
|
|
viewEvents.length = 0;
|
|
await rpc.call('setViews', ['waveform']);
|
|
var w = await lastWaveform();
|
|
var n = w.meta.map(m => m.label).indexOf('n');
|
|
assert.ok(Array.from({ length: 50 }, (_, i) => w.data[i * w.meta.length + n]).some(v => v > 0), 'trace is empty');
|
|
});
|
|
});
|
|
|
|
describe('verilog VCD recording', function () {
|
|
const MAIN = [
|
|
'module top(clk, reset, hsync, vsync, rgb);',
|
|
' input clk, reset;',
|
|
' output hsync, vsync;',
|
|
' output [3:0] rgb;',
|
|
' reg [7:0] n;',
|
|
' always @(posedge clk) n <= reset ? 0 : n + 1;',
|
|
' assign hsync = n[7];',
|
|
' assign vsync = 0;',
|
|
' assign rgb = n[3:0];',
|
|
'endmodule',
|
|
'',
|
|
].join('\n');
|
|
var built: Awaited<ReturnType<Builder['build']>>;
|
|
var dir: string;
|
|
|
|
before(async function () {
|
|
var read = async (rel: string) => null;
|
|
built = await new Builder(ROOT).build({
|
|
platform: 'verilog', mainPath: 'main.v', mainText: MAIN,
|
|
files: new ProjectFileProvider(read, ROOT, 'verilog'),
|
|
});
|
|
assert.deepEqual(built.diagnostics, []);
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), '8bws-vcd-'));
|
|
});
|
|
|
|
async function waitForVcd(recording: boolean) {
|
|
for (var i = 0; i < 400 && !vcdEvents.some(e => e.recording === recording); i++) await new Promise(r => setTimeout(r, 20));
|
|
var ev = vcdEvents.find(e => e.recording === recording);
|
|
assert.ok(ev, 'no vcd event ' + JSON.stringify(vcdEvents));
|
|
return ev!;
|
|
}
|
|
|
|
it('records a number of frames to a file and stops by itself', async function () {
|
|
var s = await rpc.call<EmuStatus>('start', 'verilog', built.output, built.files);
|
|
assert.ok(s.vcd, 'verilog design has signals to record');
|
|
var file = path.join(dir, 'a.vcd');
|
|
await rpc.call('startVcd', file, 2);
|
|
var done = await waitForVcd(false);
|
|
assert.equal(done.file, file);
|
|
assert.ok(done.clocks! > 0 && done.bytes! > 0);
|
|
var text = fs.readFileSync(file, 'utf-8');
|
|
assert.equal(text.length, done.bytes);
|
|
assert.ok(text.startsWith('$version'));
|
|
assert.ok(/\$var wire 8 \S+ n \[7:0\] \$end/.test(text), text.slice(0, 400));
|
|
assert.ok(text.endsWith('#' + done.clocks + '\n'));
|
|
// the counter changes every clock
|
|
assert.ok(text.split('\n').filter(l => /^b\d+ /.test(l)).length > 1000);
|
|
});
|
|
|
|
it('stops when asked, and a hidden-length recording keeps going until then', async function () {
|
|
await rpc.call('start', 'verilog', built.output, built.files);
|
|
var file = path.join(dir, 'b.vcd');
|
|
await rpc.call('startVcd', file);
|
|
await new Promise(r => setTimeout(r, 300));
|
|
assert.ok(!vcdEvents.some(e => !e.recording), 'no length, so it runs on');
|
|
var ev = await rpc.call<VcdEvent>('stopVcd');
|
|
assert.equal(ev.file, file);
|
|
assert.ok(ev.clocks! > 0);
|
|
assert.equal(await rpc.call('stopVcd'), null);
|
|
});
|
|
|
|
it('ends itself at the size limit', async function () {
|
|
await rpc.call('start', 'verilog', built.output, built.files);
|
|
var file = path.join(dir, 'full.vcd');
|
|
await rpc.call('startVcd', file, 0, 100000);
|
|
var done = await waitForVcd(false);
|
|
assert.ok(done.full, JSON.stringify(done));
|
|
assert.ok(done.bytes! >= 100000 && done.bytes! < 300000, 'bytes ' + done.bytes);
|
|
assert.equal(fs.statSync(file).size, done.bytes);
|
|
assert.equal(await rpc.call('stopVcd'), null, 'already finished');
|
|
});
|
|
|
|
it('refuses platforms with no signals', async function () {
|
|
var s = await rpc.call<EmuStatus>('start', 'nes', rom('nes/shoot2.c.rom'));
|
|
assert.ok(!s.vcd);
|
|
await assert.rejects(rpc.call('startVcd', path.join(dir, 'c.vcd')), /no signals/);
|
|
});
|
|
});
|
|
|
|
describe('debugging', function () {
|
|
var built: Awaited<ReturnType<Builder['build']>>;
|
|
var stops: any[];
|
|
|
|
before(async function () {
|
|
var mainText = fs.readFileSync(path.join(ROOT, 'presets/mw8080bw/game2.c'), 'utf-8');
|
|
var read = async (rel: string) => rel === 'game2.c' ? new TextEncoder().encode(mainText) : null;
|
|
built = await new Builder(ROOT).build({
|
|
platform: 'mw8080bw', mainPath: 'game2.c', mainText,
|
|
files: new ProjectFileProvider(read, ROOT, 'mw8080bw'),
|
|
});
|
|
assert.deepEqual(built.diagnostics, []);
|
|
});
|
|
|
|
beforeEach(function () {
|
|
stops = [];
|
|
rpc.on('stopped', e => stops.push(e));
|
|
});
|
|
|
|
/** The next stop not yet seen; it may have come before the call's reply. */
|
|
async function nextStop() {
|
|
for (var i = 0; i < 300 && !stops.length; i++) await new Promise(r => setTimeout(r, 20));
|
|
assert.ok(stops.length, 'never stopped');
|
|
return stops.shift();
|
|
}
|
|
|
|
const debug = (method: string, ...args: any[]) => rpc.call('debug', method, ...args);
|
|
|
|
it('starts stopped, then stops at a source breakpoint, steps, and steps back', async function () {
|
|
var s = await rpc.call<EmuStatus>('start', 'mw8080bw', built.output, built.files, { paused: true });
|
|
assert.equal(s.state, 'paused');
|
|
await debug('setBuild', { listings: built.listings, symbolmap: built.symbolmap, mainPath: 'game2.c', paths: built.paths });
|
|
var [bp] = await debug('setBreakpoints', [{ id: 1, type: 'source', file: 'game2.c', line: 166, enabled: true }]);
|
|
assert.ok(bp.verified, bp.message);
|
|
await debug('continue');
|
|
assert.equal((await nextStop()).reason, 'breakpoint');
|
|
assert.equal((await debug('location')).source.line, 166);
|
|
await debug('step', 'over', 'line');
|
|
assert.equal((await nextStop()).reason, 'step');
|
|
assert.equal((await debug('location')).source.line, 167);
|
|
await debug('stepBack', 'line');
|
|
assert.equal((await nextStop()).reason, 'step');
|
|
assert.equal((await debug('location')).source.line, 166);
|
|
assert.equal(statuses[statuses.length - 1].state, 'paused');
|
|
});
|
|
|
|
it('steps through C source on the NES, which has no Machine of its own', async function () {
|
|
// jsnes stops between instructions through the trap its frame() takes
|
|
var dir = path.join(ROOT, 'presets/nes');
|
|
var mainText = fs.readFileSync(path.join(dir, 'hello.c'), 'utf-8');
|
|
var read = async (rel: string) => {
|
|
var p = path.join(dir, rel);
|
|
return fs.existsSync(p) ? new Uint8Array(fs.readFileSync(p)) : null;
|
|
};
|
|
var nes = await new Builder(ROOT).build({
|
|
platform: 'nes', mainPath: 'hello.c', mainText,
|
|
files: new ProjectFileProvider(read, ROOT, 'nes'),
|
|
});
|
|
assert.deepEqual(nes.diagnostics, []);
|
|
await rpc.call('start', 'nes', nes.output, nes.files, { paused: true });
|
|
await debug('setBuild', { listings: nes.listings, symbolmap: nes.symbolmap, mainPath: 'hello.c', paths: nes.paths });
|
|
var [bp] = await debug('setBreakpoints', [{ id: 1, type: 'source', file: 'hello.c', line: 19, enabled: true }]);
|
|
assert.ok(bp.verified, bp.message);
|
|
await debug('continue');
|
|
assert.equal((await nextStop()).reason, 'breakpoint');
|
|
assert.equal((await debug('location')).source.line, 19);
|
|
await debug('step', 'over', 'line');
|
|
assert.equal((await nextStop()).reason, 'step');
|
|
assert.equal((await debug('location')).source.line, 20);
|
|
await debug('stepBack', 'line');
|
|
assert.equal((await nextStop()).reason, 'step');
|
|
assert.equal((await debug('location')).source.line, 19);
|
|
await debug('setBreakpoints', []);
|
|
});
|
|
|
|
it('browses the NES machine state and symbols', async function () {
|
|
var dir = path.join(ROOT, 'presets/nes');
|
|
var read = async (rel: string) => {
|
|
var p = path.join(dir, rel);
|
|
return fs.existsSync(p) ? new Uint8Array(fs.readFileSync(p)) : null;
|
|
};
|
|
var nes = await new Builder(ROOT).build({
|
|
platform: 'nes', mainPath: 'scroll.c', mainText: fs.readFileSync(path.join(dir, 'scroll.c'), 'utf-8'),
|
|
files: new ProjectFileProvider(read, ROOT, 'nes'),
|
|
});
|
|
await rpc.call('start', 'nes', nes.output, nes.files, { paused: true });
|
|
await debug('setBuild', { listings: nes.listings, symbolmap: nes.symbolmap, mainPath: 'scroll.c', paths: nes.paths });
|
|
var caps = await debug('capabilities');
|
|
assert.equal(caps.tree, true);
|
|
assert.equal(caps.write, true);
|
|
assert.deepEqual(await debug('debugTree', []), [{ name: 'state', value: '', expandable: true }]);
|
|
var cpu = await debug('debugTree', ['state', 'c']);
|
|
assert.ok(cpu.find((e: any) => e.name === 'PC'));
|
|
var syms = await debug('symbols');
|
|
assert.ok(syms.find((s: any) => s.name === '_main'));
|
|
// zero page, through the machine's write
|
|
assert.equal(await debug('writeMemory', 0x10, [0x5a]), 1);
|
|
assert.deepEqual(await debug('readMemory', 0x10, 1), [0x5a]);
|
|
|
|
// a 6502 call stack, from JSR return addresses: neslib's vram_write
|
|
// (no source) called from put_str, called from main
|
|
await debug('setBreakpoints', [{ id: 2, type: 'address', target: 'vram_write', enabled: true }]);
|
|
await debug('continue');
|
|
assert.equal((await nextStop()).reason, 'breakpoint');
|
|
var stack = await debug('callStack');
|
|
assert.deepEqual(stack.map((f: any) => [f.symbol?.name, f.source?.line ?? null, !!f.unsure]),
|
|
[['_vram_write', null, false], ['_put_str', 21, false], ['_main', 52, false]]);
|
|
await debug('setBreakpoints', []);
|
|
});
|
|
|
|
it('drives the debug adapter through the worker, as the extension does', async function () {
|
|
var backend = new WorkerDebugBackend((method, ...args) => rpc.call('debug', method, ...args), async () => {
|
|
await rpc.call('start', 'mw8080bw', built.output, built.files, { paused: true });
|
|
await rpc.call('debug', 'setBuild', { listings: built.listings, symbolmap: built.symbolmap, mainPath: 'game2.c', paths: built.paths });
|
|
return { root: '/proj' };
|
|
}, async () => { });
|
|
rpc.on('stopped', e => backend.handleStop(e));
|
|
var c = new DapClient(new EmuDebugSession(backend));
|
|
await c.request('initialize', { adapterID: '8bitworkshop', pathFormat: 'path', linesStartAt1: true, columnsStartAt1: true });
|
|
await c.request('launch', { mainFile: 'game2.c' });
|
|
await c.event('initialized');
|
|
var { breakpoints } = await c.request('setBreakpoints', { source: { path: '/proj/game2.c' }, breakpoints: [{ line: 166 }] });
|
|
assert.ok(breakpoints[0].verified);
|
|
await c.request('configurationDone');
|
|
assert.equal((await c.event('stopped')).reason, 'breakpoint');
|
|
var frame = await c.where();
|
|
assert.equal(frame.source.path, '/proj/game2.c');
|
|
assert.equal(frame.line, 166);
|
|
await c.request('next', { threadId: 1 });
|
|
await c.event('stopped');
|
|
assert.equal((await c.where()).line, 167);
|
|
await c.request('stepBack', { threadId: 1 });
|
|
await c.event('stopped');
|
|
assert.equal((await c.where()).line, 166);
|
|
assert.match((await c.request('evaluate', { expression: 'now', context: 'repl' })).result, /past/);
|
|
});
|
|
|
|
it('pauses with a stop, and seeks to a recorded frame', async function () {
|
|
await rpc.call('start', 'mw8080bw', built.output, built.files);
|
|
await waitForFrames(10);
|
|
await rpc.call('pause');
|
|
assert.equal((await nextStop()).reason, 'pause');
|
|
var s = await rpc.call<EmuStatus>('seekFrame', 3);
|
|
assert.equal((await nextStop()).reason, 'goto');
|
|
assert.equal(s.timeline.now.frame, 3);
|
|
assert.ok(s.timeline.past);
|
|
assert.ok(s.timeline.last >= 10);
|
|
});
|
|
});
|
|
});
|