diff --git a/extension/notes/PLAN.md b/extension/notes/PLAN.md index 9e75d2b1..75f558c6 100644 --- a/extension/notes/PLAN.md +++ b/extension/notes/PLAN.md @@ -1383,7 +1383,8 @@ CodeMirror, no jQuery. **First-time players.** Writing the quickstart showed two things a new user can't guess: - *Focus.* Keys go to the emulator only while the panel has focus. When - it doesn't, dim the canvas and show "Click to play"; the webview's + it does, the controls hint fades in, as in the IDE; it fades out on + blur (a "Click to play" overlay was too much). The webview's `focus`/`blur` events drive it. Keys never go to the emulator while the editor has focus. - *Controls.* Show a one-line controls hint under the canvas ("Arrows: @@ -2190,9 +2191,13 @@ do them when a platform is the priority (VCS needs 3–4). - Each Run starts a new emulator worker: platforms keep global state (Javatari deletes its own `start()`), so `emuworker` refuses a second `start`. A rebuild with an identical ROM doesn't reload. + - A hidden panel stops the worker's frames (`setVisible`) without + changing its paused/running state, so showing it resumes only what + was running. `Builder` keeps each main file's last output, so Run + works when the build is unchanged (e.g. running twice). - Control hints moved out of `index.html` into `src/common/controls.ts`; the IDE renders them from there, and the emulator panel shows them in - a bar you can hide, with "Click to play" when it lacks focus. + a bar you can hide, styled like the IDE's and shown while it has focus. Platforms with no entry get hints generated from their key map (`describeControls` in `emu.ts`). - Changed from the plan: "Review" is a checklist quick pick, not a @@ -2233,6 +2238,47 @@ do them when a platform is the priority (VCS needs 3–4). 5. **Packaging.** Asset-root override, lazy platform loading, binaryen dynamic import, base + per-family packs, VSIX/CI. +### Plan: fuzzing + +Fuzz the parts that don't need VS Code: grammars, the build and emulator +workers, and the pure helpers. The repo already has `jsfuzz` harnesses +(`fuzzbasic`, `fuzzhdl` in the root `package.json`). + +Each target is a mocha test in `extension/test/` with a fixed seed and a +small iteration count, so CI runs it. `FUZZ_ITERS=n` runs longer locally and +writes failing inputs to `out/fuzz/` for replay. If random mutation stops +finding bugs, drive targets 2 and 3 with `jsfuzz`'s coverage guidance. + +In order: +1. **Grammars.** VS Code tokenizes every line as you type, so a regex that + backtracks badly freezes the editor. Feed mutated lines from preset asm + files, and random punctuation-heavy strings, through + `scripts/tmtokenize.ts`. Check: each line tokenizes in a few ms, and the + tokens cover the line with no gaps or overlaps. +2. **Build worker.** Mutate preset sources (delete lines, swap tokens, + truncate, insert junk bytes) and build them with `Builder` + (`buildcore.ts`). Check: every build ends with output or errors within a + timeout; the worker survives for the next build; every error names a real + file and a line inside it. Covers the compilers' wasm, error parsing, + listing and symbol parsing, and diagnostics. +3. **Emulator worker.** Load random or mutated ROMs on each platform, then + send random keys, pause/step/reset and visibility changes. Check: frames + keep coming, status stays valid, and the worker never dies silently. + Reaches the CPU emulators' illegal-opcode and out-of-range memory paths. +4. **Property tests** (e.g. `fast-check`) for pure functions: detection + over random file trees (`src/common/detect.ts`, `projectinfo.ts`), path + edge cases in `isInside` (`..`, trailing slashes, letter case), and + `patchHeaderForClang` (applying it twice gives the same result, and + headers with nothing to patch come through unchanged). +5. **Random command sequences** (later, nightly). Under + `@vscode/test-electron`, open presets, build, run, close the panel and + change settings in random order. Catches lifecycle bugs (a disposed + panel, the build scheduler running after deactivate), but it's slow and + flaky. + +Skip the webview messages: there are three types (`key`, `frameDone`, +`controlsVisible`), and a field check covers them. + ## 10. Risks and open questions - **cwd / asset root.** Worker shims assume repo-root cwd; must be made diff --git a/extension/src/buildcore.ts b/extension/src/buildcore.ts index 2cd8605c..8b9bfc2b 100644 --- a/extension/src/buildcore.ts +++ b/extension/src/buildcore.ts @@ -53,6 +53,8 @@ export class Builder { private queue: Promise = Promise.resolve(); private initialized = false; private preloaded = new Set(); + /** the last successful outcome per main file, for unchanged builds */ + private last = new Map(); constructor(readonly rootDir: string) { } @@ -77,6 +79,7 @@ export class Builder { getToolForFilename: getTool, }, deps); var paths = [req.mainPath].concat(deps.map(d => d.path)); + var key = `${req.platform}/${tool}/${req.mainPath}`; var result: WorkerResult; try { for (var t of preloads) { @@ -90,8 +93,10 @@ export class Builder { return { success: false, tool, paths, diagnostics: [{ path: req.mainPath, line: 0, msg: String(e && e.message || e) }] }; } if (!result || ('unchanged' in result && result.unchanged)) { - return { success: true, tool, paths, diagnostics: [], unchanged: true }; + // the worker skips unchanged builds, but Run still needs the output + return { ...this.last.get(key), success: true, tool, paths, diagnostics: [], unchanged: true }; } + this.last.delete(key); if ('errors' in result && result.errors && result.errors.length) { var toPath = (err: WorkerError) => (err.path && filename2path[err.path]) || err.path || req.mainPath; return { @@ -103,10 +108,12 @@ export class Builder { // listings stay raw: SourceFile objects don't survive postMessage, so // the receiver runs projectcore.processListings on them var r = result as any; - return { + var outcome: BuildOutcome = { success: true, tool, paths, diagnostics: [], output: r.output, listings: r.listings, symbolmap: r.symbolmap, segments: r.segments, }; + this.last.set(key, outcome); + return outcome; } return { success: false, tool, paths, diagnostics: [{ path: req.mainPath, line: 0, msg: 'Unknown build result' }] }; } diff --git a/extension/src/emulatorpanel.ts b/extension/src/emulatorpanel.ts index e7a7d760..8de0d71a 100644 --- a/extension/src/emulatorpanel.ts +++ b/extension/src/emulatorpanel.ts @@ -72,19 +72,19 @@ function getHtml(controlsVisible: boolean) { canvas:focus { box-shadow: 0 0 0 1px var(--vscode-focusBorder); } #status { position: absolute; left: 8px; top: 6px; font: 12px var(--vscode-font-family); color: #ccc; text-shadow: 0 0 3px #000; pointer-events: none; } - /* keys only reach the emulator while it has focus, so say so */ - #focus { position: absolute; inset: 0; bottom: var(--bar, 0px); display: none; align-items: center; justify-content: center; - font: 14px var(--vscode-font-family); color: #fff; background: rgba(0,0,0,0.45); cursor: pointer; } - body.unfocused #focus { display: flex; } - #focus span { padding: 6px 12px; border-radius: 4px; background: rgba(0,0,0,0.6); } - #bar { position: absolute; left: 0; right: 0; bottom: 0; min-height: 24px; display: none; align-items: center; justify-content: center; - flex-wrap: wrap; gap: 4px 14px; padding: 3px 28px; box-sizing: border-box; - font: 12px var(--vscode-font-family); color: var(--vscode-foreground); background: var(--vscode-editorWidget-background, #222); } - body.controls #bar { display: flex; } - .key { display: inline-block; padding: 0 4px; margin-right: 2px; border: 1px solid var(--vscode-widget-border, #666); - border-radius: 3px; font-family: var(--vscode-editor-font-family, monospace); white-space: pre; line-height: 1.3; } + /* the IDE's control hints (css/ui.css): key caps on black, shown while + the emulator has focus, since keys only reach it then */ + #bar { position: absolute; left: 0; right: 0; bottom: 0; display: none; align-items: center; justify-content: center; + flex-wrap: wrap; padding: 2px 28px; box-sizing: border-box; background: #000; + font: 11px var(--vscode-font-family); opacity: 0; transition: opacity 200ms; } + body.controls.hascontrols #bar { display: flex; } + body:not(.unfocused) #bar { opacity: 1; } + .def { color: #ccc; white-space: nowrap; line-height: 2em; } + .key { border: 2px solid rgba(0,0,0,0.2); border-radius: 0.6em; padding: 0.2em 0.5em; margin-left: 1em; + background-color: #eee; color: #666; white-space: pre; } + .key + .key { margin-left: 0.25em; } #hide { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); cursor: pointer; opacity: 0.7; border: 0; - background: none; color: inherit; font-size: 14px; } + background: none; color: #ccc; font-size: 14px; } #show { position: absolute; right: 6px; bottom: 4px; display: none; cursor: pointer; font: 11px var(--vscode-font-family); color: #ccc; background: rgba(0,0,0,0.5); border: 0; border-radius: 3px; padding: 2px 6px; } body.hascontrols:not(.controls) #show { display: block; } @@ -93,7 +93,6 @@ function getHtml(controlsVisible: boolean) {
-
Click to play