From 40050cb615898c832e7ac7612d998ea6cd3889a6 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 3 Mar 2026 17:54:55 +0100 Subject: [PATCH] started command-line test harness (npm run cli) --- package-lock.json | 3 + package.json | 4 + src/tools/8bws.ts | 256 +++++++++++++++++++++++++++++++++++++++++++ src/tools/testlib.ts | 195 ++++++++++++++++++++++++++++++++ 4 files changed, 458 insertions(+) create mode 100644 src/tools/8bws.ts create mode 100644 src/tools/testlib.ts diff --git a/package-lock.json b/package-lock.json index 17010e4f..158803a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,9 @@ "octokat": "^0.10.0", "split.js": "^1.6.2" }, + "bin": { + "8bws": "gen/cli/8bws.js" + }, "devDependencies": { "@lezer/generator": "^1.8.0", "@types/bootbox": "^5.1.3", diff --git a/package.json b/package.json index d789edb6..181d355f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "url": "git+https://github.com/sehugg/8bitworkshop.git" }, "license": "GPL-3.0", + "bin": { + "8bws": "./gen/cli/8bws.js" + }, "dependencies": { "@codemirror/commands": "^6.10.2", "@codemirror/lang-cpp": "^6.0.3", @@ -88,6 +91,7 @@ "fuzzbasic": "jsfuzz gen/common/basic/fuzz.js ~/basic/corpus/ --versifier false", "fuzzhdl": "jsfuzz -r binaryen gen/common/hdl/fuzz.js ~/verilator/corpus/ --versifier false", "machine": "node gen/tools/runmachine.js", + "cli": "node gen/tools/8bws.js", "mkdoc": "typedoc --out web/jsdoc src/common/" }, "keywords": [ diff --git a/src/tools/8bws.ts b/src/tools/8bws.ts new file mode 100644 index 00000000..751aff24 --- /dev/null +++ b/src/tools/8bws.ts @@ -0,0 +1,256 @@ +#!/usr/bin/env node + +// 8bws - 8bitworkshop CLI tool for compilation, ROM execution, and platform info + +import * as fs from 'fs'; +import { initialize, compile, compileSourceFile, preload, listTools, listPlatforms, PLATFORM_PARAMS, TOOLS, TOOL_PRELOADFS } from './testlib'; + +interface CLIResult { + success: boolean; + command: string; + data?: any; + error?: string; +} + +function outputJSON(result: CLIResult): void { + console.log(JSON.stringify(result, null, 2)); +} + +function usage(): void { + outputJSON({ + success: false, + command: 'help', + data: { + commands: { + 'compile': 'compile --tool --platform [--output ] ', + 'check': 'check --tool --platform ', + 'run': 'run --platform [--frames N] ', + 'list-tools': 'list-tools', + 'list-platforms': 'list-platforms', + } + }, + error: 'No command specified' + }); + process.exit(1); +} + +function parseArgs(argv: string[]): { command: string; args: { [key: string]: string }; positional: string[] } { + var command = argv[2]; + var args: { [key: string]: string } = {}; + var positional: string[] = []; + + for (var i = 3; i < argv.length; i++) { + if (argv[i].startsWith('--')) { + var key = argv[i].substring(2); + if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) { + args[key] = argv[++i]; + } else { + args[key] = 'true'; + } + } else { + positional.push(argv[i]); + } + } + + return { command, args, positional }; +} + +async function doCompile(args: { [key: string]: string }, positional: string[], checkOnly: boolean): Promise { + var tool = args['tool']; + var platform = args['platform']; + var outputFile = args['output']; + var sourceFile = positional[0]; + + if (!tool || !platform || !sourceFile) { + outputJSON({ + success: false, + command: checkOnly ? 'check' : 'compile', + error: 'Required: --tool --platform ' + }); + process.exit(1); + } + + if (!TOOLS[tool]) { + outputJSON({ + success: false, + command: checkOnly ? 'check' : 'compile', + error: `Unknown tool: ${tool}. Use list-tools to see available tools.` + }); + process.exit(1); + } + + // Preload the tool's filesystem if needed + var preloadKey = tool; + if (TOOL_PRELOADFS[tool + '-' + platform]) { + preloadKey = tool; + } + await preload(tool, platform); + + var result = await compileSourceFile(tool, platform, sourceFile); + + if (!result.success) { + outputJSON({ + success: false, + command: checkOnly ? 'check' : 'compile', + data: { errors: result.errors } + }); + process.exit(1); + } + + if (checkOnly) { + outputJSON({ + success: true, + command: 'check', + data: { + tool: tool, + platform: platform, + source: sourceFile, + outputSize: result.output ? (result.output.code ? result.output.code.length : result.output.length) : 0, + } + }); + return; + } + + // Write output if requested + if (outputFile && result.output) { + var outData = result.output.code || result.output; + if (outData instanceof Uint8Array) { + fs.writeFileSync(outputFile, outData); + } else if (typeof outData === 'object') { + fs.writeFileSync(outputFile, JSON.stringify(outData)); + } else { + fs.writeFileSync(outputFile, outData); + } + } + + var outputSize = 0; + if (result.output) { + outputSize = result.output.code ? result.output.code.length : result.output.length; + } + + outputJSON({ + success: true, + command: 'compile', + data: { + tool: tool, + platform: platform, + source: sourceFile, + outputSize: outputSize, + outputFile: outputFile || null, + hasListings: result.listings ? Object.keys(result.listings).length > 0 : false, + hasSymbolmap: !!result.symbolmap, + } + }); +} + +async function doRun(args: { [key: string]: string }, positional: string[]): Promise { + var platform = args['platform']; + var frames = parseInt(args['frames'] || '1'); + var romFile = positional[0]; + + if (!platform || !romFile) { + outputJSON({ + success: false, + command: 'run', + error: 'Required: --platform ' + }); + process.exit(1); + } + + // Dynamic import of MachineRunner + try { + var runmachine = require('./runmachine.js'); + var MachineRunner = runmachine.MachineRunner; + } catch (e) { + outputJSON({ + success: false, + command: 'run', + error: `Could not load MachineRunner: ${e.message}` + }); + process.exit(1); + } + + outputJSON({ + success: false, + command: 'run', + error: 'Run command not yet fully implemented (requires platform machine loading)' + }); +} + +function doListTools(): void { + var tools = listTools(); + outputJSON({ + success: true, + command: 'list-tools', + data: { + tools: tools, + count: tools.length + } + }); +} + +function doListPlatforms(): void { + var platforms = listPlatforms(); + var details: { [key: string]: any } = {}; + for (var p of platforms) { + details[p] = { + arch: PLATFORM_PARAMS[p].arch || 'unknown', + }; + } + outputJSON({ + success: true, + command: 'list-platforms', + data: { + platforms: details, + count: platforms.length + } + }); +} + +async function main() { + if (process.argv.length < 3) { + usage(); + } + + var { command, args, positional } = parseArgs(process.argv); + + try { + switch (command) { + case 'compile': + await initialize(); + await doCompile(args, positional, false); + break; + case 'check': + await initialize(); + await doCompile(args, positional, true); + break; + case 'run': + await doRun(args, positional); + break; + case 'list-tools': + await initialize(); + doListTools(); + break; + case 'list-platforms': + await initialize(); + doListPlatforms(); + break; + default: + outputJSON({ + success: false, + command: command, + error: `Unknown command: ${command}` + }); + process.exit(1); + } + } catch (e) { + outputJSON({ + success: false, + command: command, + error: e.message || String(e) + }); + process.exit(1); + } +} + +main(); diff --git a/src/tools/testlib.ts b/src/tools/testlib.ts new file mode 100644 index 00000000..c2aae846 --- /dev/null +++ b/src/tools/testlib.ts @@ -0,0 +1,195 @@ + +// testlib - Clean async API for compiling and testing 8bitworkshop projects +// Wraps the worker build system for use in tests and CLI tools + +import * as fs from 'fs'; +import type { WorkerResult, WorkerMessage, WorkerErrorResult, WorkerOutputResult } from "../common/workertypes"; +import { setupNodeEnvironment, handleMessage, store, TOOL_PRELOADFS } from "../worker/workerlib"; +import { PLATFORM_PARAMS } from "../worker/platforms"; +import { TOOLS } from "../worker/workertools"; + +export { store, TOOL_PRELOADFS, PLATFORM_PARAMS, TOOLS }; + +export interface CompileOptions { + tool: string; + platform: string; + code?: string; + path?: string; + files?: { path: string; data: string | Uint8Array }[]; + buildsteps?: { path: string; platform: string; tool: string }[]; + mainfile?: boolean; +} + +export interface CompileResult { + success: boolean; + output?: Uint8Array | any; + errors?: { line: number; msg: string; path?: string }[]; + listings?: any; + symbolmap?: any; + params?: any; + unchanged?: boolean; +} + +let initialized = false; + +/** + * Initialize the Node.js environment for compilation. + * Must be called once before any compile/preload calls. + */ +export async function initialize(): Promise { + if (initialized) return; + setupNodeEnvironment(); + // The worker tools are already registered through the import chain: + // workerlib -> workertools -> tools/* and builder -> TOOLS + // No need to load the esbuild bundle. + initialized = true; +} + +/** + * Preload a tool's filesystem (e.g. CC65 standard libraries). + */ +export async function preload(tool: string, platform?: string): Promise { + await initialize(); + var msg: WorkerMessage = { preload: tool } as any; + if (platform) (msg as any).platform = platform; + await handleMessage(msg); +} + +/** + * Compile source code with the specified tool and platform. + */ +export async function compile(options: CompileOptions): Promise { + await initialize(); + + // Reset the store for a clean build + await handleMessage({ reset: true } as any); + + let result: WorkerResult; + + if (options.files && options.buildsteps) { + // Multi-file build + var msg: any = { + updates: options.files.map(f => ({ path: f.path, data: f.data })), + buildsteps: options.buildsteps + }; + result = await handleMessage(msg); + } else { + // Single-file build + var msg: any = { + code: options.code, + platform: options.platform, + tool: options.tool, + path: options.path || ('src.' + options.tool), + mainfile: options.mainfile !== false + }; + result = await handleMessage(msg); + } + + return workerResultToCompileResult(result); +} + +/** + * Compile a file from the presets directory. + */ +export async function compileFile(tool: string, platform: string, presetPath: string): Promise { + await initialize(); + + var code = fs.readFileSync('presets/' + platform + '/' + presetPath, 'utf-8'); + return compile({ + tool: tool, + platform: platform, + code: code, + path: presetPath, + }); +} + +/** + * Compile an arbitrary source file path. + */ +export async function compileSourceFile(tool: string, platform: string, filePath: string): Promise { + await initialize(); + + var code = fs.readFileSync(filePath, 'utf-8'); + var basename = filePath.split('/').pop(); + return compile({ + tool: tool, + platform: platform, + code: code, + path: basename, + }); +} + +function workerResultToCompileResult(result: WorkerResult): CompileResult { + if (!result) { + return { success: true, unchanged: true }; + } + if ('unchanged' in result && result.unchanged) { + return { success: true, unchanged: true }; + } + if ('errors' in result && result.errors && result.errors.length > 0) { + return { + success: false, + errors: result.errors, + }; + } + if ('output' in result) { + return { + success: true, + output: result.output, + listings: (result as any).listings, + symbolmap: (result as any).symbolmap, + params: (result as any).params, + }; + } + return { success: false, errors: [{ line: 0, msg: 'Unknown result format' }] }; +} + +/** + * List available compilation tools. + */ +export function listTools(): string[] { + return Object.keys(TOOLS); +} + +/** + * List available target platforms. + */ +export function listPlatforms(): string[] { + return Object.keys(PLATFORM_PARAMS); +} + +/** + * Convert a binary buffer to a hex string for display. + */ +export function ab2str(buf: Buffer | ArrayBuffer): string { + return String.fromCharCode.apply(null, new Uint16Array(buf)); +} + +// Platform test harness utilities + +/** + * Create a mock localStorage for tests that need it. + */ +export function createMockLocalStorage(): Storage { + var items: { [key: string]: string } = {}; + return { + get length() { + return Object.keys(items).length; + }, + clear() { + items = {}; + }, + getItem(k: string) { + return items[k] || null; + }, + setItem(k: string, v: string) { + items[k] = v; + }, + removeItem(k: string) { + delete items[k]; + }, + key(i: number) { + return Object.keys(items)[i] || null; + } + }; +}