diff --git a/justfile b/justfile index 4323b85..2e93c58 100644 --- a/justfile +++ b/justfile @@ -1,3 +1,5 @@ +set default-list := true + lint: npx eslint src/**/* --config .eslint.config.mjs @@ -6,3 +8,6 @@ format: protocol: temp_dir=$(mktemp -d) && trap 'rm -rf "$temp_dir"' EXIT && curl --fail --silent --show-error --location https://raw.githubusercontent.com/TOPLLab/WARDuino/7805f0bc87e79c55c151788a279f2ecb13e0fa7a/src/Debug/debug.proto --output "$temp_dir/debug.proto" && protoc --proto_path="$temp_dir" --plugin=protoc-gen-ts_proto=node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=src/protocol/vendor --ts_proto_opt=env=node,forceLong=bigint,useOptionals=messages,outputEncodeMethods=true,outputJsonMethods=false "$temp_dir/debug.proto" + +test: + npm run test:all diff --git a/package-lock.json b/package-lock.json index d86c192..b83b0d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "name": "latch", "version": "0.6.0", "dependencies": { + "@bufbuild/protobuf": "^2.14.0", "@thi.ng/leb128": "^3.1.90", "ieee754": "^1.2.1", "ink": "^3.2.0", "json-with-bigint": "^3.5.8", "react": "^17.0.2", + "serialport": "^13.0.0", "source-map": "^0.7.6", "ts-node": "^10.9.2", "tslib": "^2.8.1", @@ -39,7 +41,6 @@ "globals": "^17.6.0", "ink-testing-library": "^2.1.0", "mqtt": "^5.15.1", - "serialport": "^13.0.0", "ts-proto": "^2.12.1", "typescript-eslint": "^8.60.1" } @@ -342,7 +343,6 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.14.0.tgz", "integrity": "sha512-C3UGsiCwSprE2NKIIFA3hCDlpXTMCAXRZuEVp88L1GY36Y41+rYL5fryE+nOFhp4p4JPQvdV8PQ4DWgHgeTE+w==", - "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@cspotcode/source-map-support": { @@ -736,7 +736,6 @@ "version": "10.2.2", "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", - "dev": true, "license": "MIT", "dependencies": { "@serialport/bindings-interface": "^1.2.1", @@ -750,7 +749,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz", "integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -771,7 +769,6 @@ "version": "12.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -784,7 +781,6 @@ "version": "12.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", - "dev": true, "license": "MIT", "dependencies": { "@serialport/parser-delimiter": "12.0.0" @@ -800,7 +796,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -818,7 +813,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", - "dev": true, "license": "MIT", "engines": { "node": "^12.22 || ^14.13 || >=16" @@ -828,7 +822,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz", "integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" @@ -841,7 +834,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz", "integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" @@ -854,7 +846,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz", "integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" @@ -867,7 +858,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz", "integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" @@ -880,7 +870,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz", "integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6.0" @@ -890,7 +879,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz", "integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==", - "dev": true, "license": "MIT", "dependencies": { "@serialport/parser-delimiter": "13.0.0" @@ -906,7 +894,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz", "integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" @@ -919,7 +906,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz", "integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" @@ -932,7 +918,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz", "integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" @@ -945,7 +930,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz", "integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" @@ -958,7 +942,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz", "integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==", - "dev": true, "license": "MIT", "dependencies": { "@serialport/bindings-interface": "1.2.2", @@ -975,7 +958,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2575,7 +2557,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4372,7 +4353,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/multiline-ts": { @@ -4396,7 +4376,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz", "integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==", - "dev": true, "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" @@ -4427,7 +4406,6 @@ "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "dev": true, "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -5005,7 +4983,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz", "integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==", - "dev": true, "license": "MIT", "dependencies": { "@serialport/binding-mock": "10.2.2", @@ -5034,7 +5011,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" diff --git a/package.json b/package.json index d4f5756..93644e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "latch", "description": "A testing framework for constrained environments", - "version": "0.6.0", + "version": "0.7.0", "repository": { "type": "git", "url": "https://github.com/TOPLLab/latch.git" @@ -48,11 +48,13 @@ "coverage:test:ava": "c8 --src src/ --all ava" }, "dependencies": { + "@bufbuild/protobuf": "^2.14.0", "@thi.ng/leb128": "^3.1.90", "ieee754": "^1.2.1", "ink": "^3.2.0", "json-with-bigint": "^3.5.8", "react": "^17.0.2", + "serialport": "^13.0.0", "source-map": "^0.7.6", "ts-node": "^10.9.2", "tslib": "^2.8.1", @@ -76,7 +78,6 @@ "globals": "^17.6.0", "ink-testing-library": "^2.1.0", "mqtt": "^5.15.1", - "serialport": "^13.0.0", "ts-proto": "^2.12.1", "typescript-eslint": "^8.60.1" }, diff --git a/src/messaging/Message.ts b/src/messaging/Message.ts index 00f3440..2b6651c 100644 --- a/src/messaging/Message.ts +++ b/src/messaging/Message.ts @@ -3,7 +3,7 @@ import {Breakpoint} from '../debug/Breakpoint'; import {WASM} from '../sourcemap/Wasm'; import {SourceMap} from '../sourcemap/SourceMap'; import {readFileSync} from "fs"; -import {remoteFunctionResultParser} from "./Parsers"; +import {operationResultParser, remoteFunctionResultParser} from "./Parsers"; import {CompileOutput, CompilerFactory} from '../manage/Compiler'; import {WABT} from '../util/env'; import WasmValue = WASM.Value; @@ -22,7 +22,9 @@ import { ModuleUpdate, NotificationType, OperationResult, + Range, RemoteFunctionCall, + ValueUpdate, Snapshot, Value as ProtocolValue } from '../protocol/vendor/debug'; @@ -75,6 +77,14 @@ export namespace Message { import Float = WASM.Float; import isFloat = WASM.isFloat; + function operation(command: Command): Request { + return { + type: command, + notification: NotificationType.NOTIFICATION_OPERATION_RESULT, + parser: payload => operationResultParser(command, payload), + }; + } + export const run: Request = { type: Command.COMMAND_RUN, notification: NotificationType.NOTIFICATION_CONTINUED, @@ -104,29 +114,23 @@ export namespace Message { notification: NotificationType.NOTIFICATION_STEPPED, parser: notificationParsers[NotificationType.NOTIFICATION_STEPPED] }; - export function addBreakpoint(payload: Breakpoint): Request { return { - type: Command.COMMAND_ADD_BREAKPOINT, - notification: NotificationType.NOTIFICATION_OPERATION_RESULT, + ...operation(Command.COMMAND_ADD_BREAKPOINT), payload: () => ProtocolBreakpoint.encode({ location: {moduleIndex: 0, programCounter: payload.id} - }).finish(), - parser: notificationParsers[NotificationType.NOTIFICATION_OPERATION_RESULT] + }).finish() }; } export function removeBreakpoint(payload: Breakpoint): Request { return { - type: Command.COMMAND_REMOVE_BREAKPOINT, - notification: NotificationType.NOTIFICATION_OPERATION_RESULT, + ...operation(Command.COMMAND_REMOVE_BREAKPOINT), payload: () => ProtocolBreakpoint.encode({ location: {moduleIndex: 0, programCounter: payload.id} - }).finish(), - parser: notificationParsers[NotificationType.NOTIFICATION_OPERATION_RESULT] + }).finish() }; } - export function inspect(fields: Inspect[]): Request { return { type: Command.COMMAND_INSPECT, @@ -135,7 +139,7 @@ export namespace Message { state: Buffer.from(fields.map(field => Number.parseInt(field, 16))) }).finish(), parser: notificationParsers[NotificationType.NOTIFICATION_SNAPSHOT] - } + }; } export const dump: Request = { @@ -150,22 +154,22 @@ export namespace Message { parser: notificationParsers[NotificationType.NOTIFICATION_LOCALS_DUMP] }; - export const reset: Request = { - type: Command.COMMAND_RESET, - notification: NotificationType.NOTIFICATION_OPERATION_RESULT, - parser: notificationParsers[NotificationType.NOTIFICATION_OPERATION_RESULT] - }; - - export const updateFunction: Request = { - type: Command.COMMAND_UPDATE_FUNCTION, - notification: NotificationType.NOTIFICATION_OPERATION_RESULT, - parser: notificationParsers[NotificationType.NOTIFICATION_OPERATION_RESULT] + export const reset: Request = operation(Command.COMMAND_RESET); + export function updateFunction(functionMessage: FunctionMessage): Request { + return { + ...operation(Command.COMMAND_UPDATE_FUNCTION), + payload: () => FunctionMessage.encode(functionMessage).finish() + }; } - export const updateLocal: Request = { - type: Command.COMMAND_UPDATE_LOCAL, - notification: NotificationType.NOTIFICATION_OPERATION_RESULT, - parser: notificationParsers[NotificationType.NOTIFICATION_OPERATION_RESULT] + export function updateLocal(index: number, value: ProtocolValue): Request { + if (!Number.isInteger(index) || index < 0 || value === undefined) { + throw Error("A local update requires a non-negative integer index and a value."); + } + return { + ...operation(Command.COMMAND_UPDATE_LOCAL), + payload: () => ValueUpdate.encode({index, value}).finish() + }; } export async function uploadFile(program: string): Promise> { @@ -175,11 +179,9 @@ export namespace Message { export function updateModule(wasm: string): Request { return { - type: Command.COMMAND_UPDATE_MODULE, - notification: NotificationType.NOTIFICATION_OPERATION_RESULT, - payload: () => ModuleUpdate.encode({wasm: readFileSync(wasm)}).finish(), - parser: notificationParsers[NotificationType.NOTIFICATION_OPERATION_RESULT] - } + ...operation(Command.COMMAND_UPDATE_MODULE), + payload: () => ModuleUpdate.encode({wasm: readFileSync(wasm)}).finish() + }; } export function pushEvent(topic: string, payload: string): Request { @@ -236,16 +238,24 @@ export namespace Message { parser: notificationParsers[NotificationType.NOTIFICATION_SNAPSHOT] } + /** The protocol default range (0, 0) requests the complete event queue. */ export const dumpAllEvents: Request = { type: Command.COMMAND_DUMP_EVENTS, notification: NotificationType.NOTIFICATION_EVENTS_DUMP, + payload: () => Range.encode({start: 0, end: 0}).finish(), parser: notificationParsers[NotificationType.NOTIFICATION_EVENTS_DUMP] } - export const dumpEvents: Request = { - type: Command.COMMAND_DUMP_EVENTS, - notification: NotificationType.NOTIFICATION_EVENTS_DUMP, - parser: notificationParsers[NotificationType.NOTIFICATION_EVENTS_DUMP] + export function dumpEvents(range: Range): Request { + if (!Number.isInteger(range.start) || range.start < 0 || !Number.isInteger(range.end) || range.end < 0) { + throw Error("An event range requires non-negative integer start and end positions."); + } + return { + type: Command.COMMAND_DUMP_EVENTS, + notification: NotificationType.NOTIFICATION_EVENTS_DUMP, + payload: () => Range.encode(range).finish(), + parser: notificationParsers[NotificationType.NOTIFICATION_EVENTS_DUMP] + }; } export const dumpCallbackmapping: Request = { @@ -254,9 +264,5 @@ export namespace Message { parser: notificationParsers[NotificationType.NOTIFICATION_CALLBACKS_DUMP] } - export const proxifyRequest: Request = { - type: Command.COMMAND_PROXIFY, - notification: NotificationType.NOTIFICATION_OPERATION_RESULT, - parser: notificationParsers[NotificationType.NOTIFICATION_OPERATION_RESULT] - }; + export const proxifyRequest: Request = operation(Command.COMMAND_PROXIFY); } diff --git a/src/messaging/Parsers.ts b/src/messaging/Parsers.ts index 78dfedc..0e14a4f 100644 --- a/src/messaging/Parsers.ts +++ b/src/messaging/Parsers.ts @@ -1,33 +1,20 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import {WASM} from '../sourcemap/Wasm'; -import {Ack, Exception} from './Message'; -import {Breakpoint} from '../debug/Breakpoint'; -import {WARDuino} from '../debug/WARDuino'; -import {JSONParse} from 'json-with-bigint'; -import State = WARDuino.State; +import type {Exception} from './Message'; import nothing = WASM.nothing; import Type = WASM.Type; import WasmInt = WASM.WasmInt; -import ieee754 from "ieee754"; -import {RemoteFunctionResult, Value as ProtocolValue} from "../protocol/vendor/debug"; +import {Command, OperationResult, RemoteFunctionResult, Value as ProtocolValue} from "../protocol/vendor/debug"; -export function identityParser(text: string) { - return stripEnd(text); -} - -export function stateParser(text: string): State { - return JSONParse(text); -} - -export function invokeParser(text: string): WASM.Value | Exception { - if (exception(text)) { - return {text: text}; +/** Decode and validate an operation response for one command. */ +export function operationResultParser(expected: Command, payload: Uint8Array, decoded?: OperationResult): OperationResult { + const result = decoded ?? OperationResult.decode(payload); + if (result.command !== expected) { + throw Error("Operation result was for command " + result.command + ", expected " + expected + "."); } - const stack: { value: any, type: any }[] = stateParser(text).stack!; - if (stack.length == 0) { - return nothing; + if (!result.success) { + throw Error("Operation " + (Command[expected] ?? expected) + " failed."); } - return stacking(stack)[stack.length - 1]; + return result; } /** Convert a protobuf remote-function response into the Latch WASM value contract. */ @@ -81,39 +68,6 @@ function floatFromBits(bits: number | bigint, bytes: 4 | 8): number { return buffer.readDoubleLE(); } -function exception(text: string): boolean { - return text.length > 1 && text.toLowerCase().includes('exception') && text.trim()[0] !== '{'; -} - -export function ackParser(text: string, ack: string): Ack { - if (text.toLowerCase().includes(ack.toLowerCase())) { - return {'text': identityParser(text)}; - } - throw Error(`No ack for ${ack}.`); -} - -export function breakpointParser(text: string): Breakpoint { - const ack: Ack = ackParser(text, 'BP'); - - const breakpointInfo = ack.text.match(/BP (0x.*)!/); - if (breakpointInfo!.length > 1) { - return new Breakpoint(parseInt(breakpointInfo![1]), 0); // TODO address to line mapping - } - - throw new Error('Could not messaging BREAKPOINT address in ack.'); -} - -export function breakpointHitParser(text: string): Breakpoint { - const ack: Ack = ackParser(text, 'AT '); - - const breakpointInfo = ack.text.match(/AT (0x.*)!/); - if (breakpointInfo!.length > 1) { - return new Breakpoint(parseInt(breakpointInfo![1]), 0); // TODO address to line mapping - } - - throw new Error('Could not messaging BREAKPOINT address in ack.'); -} - export function signed(value: bigint, bits = 32) { const x = value; const sign = 1n << BigInt(bits - 1); @@ -122,61 +76,3 @@ export function signed(value: bigint, bits = 32) { } -function extractType(object: { value: string, type: any }): Type { - return WASM.typing.get(object.type.toLowerCase()) ?? WASM.Special.unknown; -} - -function stacking(objects: { value: string, type: any }[]): WASM.Value[] { - const stacked: WASM.Value[] = []; - for (const object of objects) { - const type: WASM.Type = extractType(object); - let buff: Buffer; - switch (type) { - case WASM.Integer.u32: - case WASM.Integer.u64: - stacked.push({ - value: isNaN(Number(object.value)) ? WasmInt.nan() - : object.value === 'inf' ? WasmInt.infinity() - : object.value === '-inf' ? WasmInt.infinity(false) - : WasmInt.finite(BigInt(object.value)), - type: type - }); - break; - case WASM.Integer.i32: - stacked.push({value: WasmInt.finite(signed(BigInt(object.value), 32)), type: type}); - break; - case WASM.Integer.i64: - stacked.push({value: WasmInt.finite(signed(BigInt(object.value), 64)), type: type}); - break; - case WASM.Float.f32: - buff = floatBitsBuffer(object.value, 4); - stacked.push({value: ieee754.read(buff, 0, false, 23, buff.length), type: type}); - break; - case WASM.Float.f64: - buff = floatBitsBuffer(object.value, 8); - stacked.push({value: ieee754.read(buff, 0, false, 52, buff.length), type: type}); - break; - case WASM.Special.unknown: - break; - } - } - return stacked; -} - -function floatBitsBuffer(value: string | number | bigint, bytes: number): Buffer { - const raw = value.toString().trim(); - const hex = /^[0-9]+$/.test(raw) - ? BigInt(raw).toString(16) - : raw.replace(/^0x/i, ''); - const length = bytes * 2; - if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length > length) { - throw Error(`Invalid ${bytes * 8}-bit float bit pattern: ${value}`); - } - return Buffer.from(hex.padStart(length, '0'), 'hex'); -} - - -// Strips all trailing newlines -function stripEnd(text: string): string { - return text.replace(/\s+$/g, ''); -} diff --git a/src/testbeds/Emulator.ts b/src/testbeds/Emulator.ts index 95eb35b..ddc5a0d 100644 --- a/src/testbeds/Emulator.ts +++ b/src/testbeds/Emulator.ts @@ -6,6 +6,7 @@ import {Socket} from 'node:net'; import {Meta, TestbedEvents} from './Testbed'; import {EMULATOR} from "../util/env"; import {execFileAsync} from "../util/util"; +import {DebugFrameDecoder} from "../protocol/frame"; export class Emulator extends Platform { readonly name: string = 'Emulator'; @@ -50,6 +51,8 @@ export class DummyProxy extends Emulator { private supervisor?: Socket; + private readonly forwarding = new DebugFrameDecoder(); + constructor(connection: SubProcess, specification: ProxySpecification) { super(connection); diff --git a/src/testbeds/Platform.ts b/src/testbeds/Platform.ts index 28a4e0d..4a6f185 100644 --- a/src/testbeds/Platform.ts +++ b/src/testbeds/Platform.ts @@ -58,11 +58,19 @@ export abstract class Platform extends EventEmitter implements Testbed { this.connection.channel.removeAllListeners('data'); } - protected process(frame: DebugFrame): void { - const index = this.requests.findIndex(([request]) => this.matches(request, frame)); + protected process(frame: DebugFrame): boolean { + const operation = + frame.type === NotificationType.NOTIFICATION_OPERATION_RESULT + ? OperationResult.decode(frame.payload) + : undefined; + + const index = this.requests.findIndex(([request]) => + this.matches(request, frame, operation) + ); + this.emit(TestbedEvents.OnMessage, frame); - if (index === -1) return; + if (index === -1) return false; const [request, resolve, reject] = this.requests.splice(index, 1)[0]; try { @@ -70,12 +78,17 @@ export abstract class Platform extends EventEmitter implements Testbed { } catch (error) { reject(error); } + return true; } - private matches(request: Request, frame: DebugFrame): boolean { + private matches( + request: Request, + frame: DebugFrame, + operation?: OperationResult, + ): boolean { if (request.notification !== frame.type) return false; - if (frame.type !== NotificationType.NOTIFICATION_OPERATION_RESULT) return true; - return OperationResult.decode(frame.payload).command === request.type; + return frame.type !== NotificationType.NOTIFICATION_OPERATION_RESULT + || operation?.command === request.type; } protected failPending(error: unknown): void { diff --git a/tests/unit/interface.test.ts b/tests/unit/interface.test.ts index 551162d..b8f052b 100644 --- a/tests/unit/interface.test.ts +++ b/tests/unit/interface.test.ts @@ -184,3 +184,15 @@ test('[dummy] log file create', t => { test('[dummy] log file correct', t => { t.pass(); }); + + +test("[platform] rejects only a failed matching operation", async t => { + const channel = new TestChannel(); + const platform = new TestPlatform(channel); + const request = platform.sendRequest(new SourceMap.Mapping(), Message.reset); + channel.receive(encodeFrame({ + type: NotificationType.NOTIFICATION_OPERATION_RESULT, + payload: OperationResult.encode({command: Command.COMMAND_RESET, success: false}).finish() + })); + await t.throwsAsync(request, {message: /COMMAND_RESET failed/}); +}); diff --git a/tests/unit/messaging.test.ts b/tests/unit/messaging.test.ts index b12d9d6..93a9253 100644 --- a/tests/unit/messaging.test.ts +++ b/tests/unit/messaging.test.ts @@ -3,7 +3,7 @@ import {MessageQueue} from '../../src/messaging/MessageQueue'; import {Message} from '../../src/messaging/Message'; import {WASM} from '../../src/sourcemap/Wasm'; import {SourceMap} from '../../src/sourcemap/SourceMap'; -import {RemoteFunctionCall, RemoteFunctionResult} from "../../src/protocol/vendor/debug"; +import {Command, FunctionMessage, Range, RemoteFunctionCall, RemoteFunctionResult, ValueUpdate} from "../../src/protocol/vendor/debug"; const alphanumerical = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split(''); @@ -115,7 +115,7 @@ const fuzzy = (characters: string[]): (n: number) => string => { return result; }; } -import {Command, ContinueFor} from "../../src/protocol/vendor/debug"; +import {ContinueFor} from "../../src/protocol/vendor/debug"; import {DebugFrameDecoder, encodeFrame} from "../../src/protocol/frame"; test("[debug protocol] encodes WARDuino CONTINUE_FOR fixture", t => { @@ -132,3 +132,25 @@ test("[debug protocol] decodes fragmented WARDuino frames", t => { payload: Buffer.from([0x08, 0x05]) }]); }); + + +test("[Message updates] encode function and local payloads", t => { + const mapping = new SourceMap.Mapping(); + const functionRequest = Message.updateFunction({functionIndex: 3, instructions: Buffer.from([0xaa, 0xbb])}); + const localRequest = Message.updateLocal(2, {i32Bits: 5, index: 0}); + + t.is(functionRequest.type, Command.COMMAND_UPDATE_FUNCTION); + t.deepEqual(functionRequest.payload!(mapping), new Uint8Array([0x08, 0x03, 0x22, 0x02, 0xaa, 0xbb])); + t.deepEqual(FunctionMessage.decode(functionRequest.payload!(mapping)), {functionIndex: 3, instructions: Buffer.from([0xaa, 0xbb]), range: undefined, locals: undefined}); + t.is(localRequest.type, Command.COMMAND_UPDATE_LOCAL); + t.deepEqual(localRequest.payload!(mapping), new Uint8Array([0x08, 0x02, 0x12, 0x05, 0x0d, 0x05, 0x00, 0x00, 0x00])); + t.deepEqual(ValueUpdate.decode(localRequest.payload!(mapping)), {index: 2, value: {i32Bits: 5, index: 0, i64Bits: undefined, f32Bits: undefined, f64Bits: undefined, raw: undefined}}); + t.throws(() => Message.updateLocal(-1, {i32Bits: 1, index: 0}), {message: /non-negative integer/}); +}); + +test("[Message events] distinguish a range from the all-events default", t => { + const mapping = new SourceMap.Mapping(); + t.deepEqual(Message.dumpEvents({start: 2, end: 9}).payload!(mapping), new Uint8Array([0x08, 0x02, 0x10, 0x09])); + t.deepEqual(Range.decode(Message.dumpEvents({start: 2, end: 9}).payload!(mapping)), {start: 2, end: 9}); + t.deepEqual(Message.dumpAllEvents.payload!(mapping), new Uint8Array()); +}); diff --git a/tests/unit/parsing.test.ts b/tests/unit/parsing.test.ts index 9c63948..ee3ac55 100644 --- a/tests/unit/parsing.test.ts +++ b/tests/unit/parsing.test.ts @@ -1,9 +1,8 @@ import test from 'ava'; -import {invokeParser, remoteFunctionResultParser, signed} from "../../src/messaging/Parsers"; -import {Exception, Expected, Kind, Message, Step, WASM} from "../../src"; -import {RemoteFunctionResult} from "../../src/protocol/vendor/debug"; +import {operationResultParser, remoteFunctionResultParser, signed} from "../../src/messaging/Parsers"; +import {Expected, Kind, Message, Step, WASM} from "../../src"; +import {Command, OperationResult, RemoteFunctionResult} from "../../src/protocol/vendor/debug"; import {Verifier} from "../../src/framework/Verifier"; -import Type = WASM.Type; import WasmInt = WASM.WasmInt; import {Outcome} from "../../src/reporter/Outcome"; @@ -35,84 +34,6 @@ test('[signed] : 64-bit unsigned to signed', t => { t.is(signed(18446744073709551489n, 64), -127n); }); -/** - * Check for precision loss in state parser - */ -const equality = (a: bigint | number | undefined, b: bigint) => - // false if a is undefined or a float - (typeof a === 'bigint' && a === b) || // both bigint - (a !== undefined && Number.isInteger(a) && BigInt(a) === b); // compare integer number with bigint - -test('[invoke parser] : 64-bit signed conversion', t => { - const values = [[1n, 1n], [127n, 127n], [2147483648n, 2147483648n], [4294967294n, 4294967294n], [18446744073709551615n, -1n], [18446744073709551489n, -127n]]; - - for (const [value, expected] of values) { - const result: WASM.Value | Exception = invokeParser(`{\"stack\": [{\"idx\":0,\"type\":\"i64\",\"value\":${value}}]}\n`); - - if ('text' in result) { // check if exception - t.fail(`Expected parsed value, got exception: ${result.text}`); - return; - } - - t.is(result.type, WASM.Integer.i64); - t.is(typeof result.value, 'object'); - t.is((result.value as WasmInt).toBigInt(), expected); - } -}); - -test('[invoke parser] : 64-bit float', t => { - const values = [[9221120237041090560n, NaN]]; - - for (const [value, expected] of values) { - const result: WASM.Value | Exception = invokeParser(`{\"stack\": [{\"idx\":0,"type":"F64","value": 9221120237041090560}]}\n`); - - if ('text' in result) { // check if exception - t.fail(`Expected parsed value, got exception: ${result.text}`); - return; - } - - t.is(result.type, WASM.Float.f64); - t.is(typeof result.value, 'number'); - t.true(isNaN(result.value)); - } -}); - -test('[invoke parser] : f32 hex bit pattern with alpha digits', t => { - const result: WASM.Value | Exception = invokeParser(`{\"stack\": [{\"idx\":0,\"type\":\"F32\",\"value\":\"a6800001\"}]}\n`); - - if ('text' in result) { - t.fail(`Expected parsed value, got exception: ${result.text}`); - return; - } - - t.is(result.type, WASM.Float.f32); - t.is(result.value, -8.881785255792436e-16); -}); - -test('[invoke parser] : f32 decimal bit pattern whose hex has only digits', t => { - const result: WASM.Value | Exception = invokeParser(`{\"stack\": [{\"idx\":0,\"type\":\"F32\",\"value\":\"645922818\"}]}\n`); - - if ('text' in result) { - t.fail(`Expected parsed value, got exception: ${result.text}`); - return; - } - - t.is(result.type, WASM.Float.f32); - t.is(result.value, 8.88178631458362e-16); -}); - -test('[invoke parser] : f32 decimal bit pattern with alpha hex equivalent', t => { - const result: WASM.Value | Exception = invokeParser(`{\"stack\": [{\"idx\":0,\"type\":\"F32\",\"value\":\"2793406465\"}]}\n`); - - if ('text' in result) { - t.fail(`Expected parsed value, got exception: ${result.text}`); - return; - } - - t.is(result.type, WASM.Float.f32); - t.is(result.value, -8.881785255792436e-16); -}); - test("[protobuf invoke result] : decodes signed i64 boundaries", t => { for (const [bits, expected] of [[0n, 0n], [0xffffffffffffffffn, -1n], [0x8000000000000000n, -9223372036854775808n]]) { const result = remoteFunctionResultParser(RemoteFunctionResult.encode({success: true, results: [{i64Bits: bits, index: 0}], error: Buffer.alloc(0)}).finish()); @@ -161,3 +82,11 @@ test("[verifier] : numeric mismatch is reported as failure, not missing field", t.false(result.clarification.includes(`state does not contain 'value'`)); t.true(result.clarification.includes('Expected')); }); + + +test("[operation result] rejects failed and mismatched operations", t => { + const failed = OperationResult.encode({command: Command.COMMAND_RESET, success: false}).finish(); + t.throws(() => operationResultParser(Command.COMMAND_RESET, failed), {message: /COMMAND_RESET failed/}); + const mismatched = OperationResult.encode({command: Command.COMMAND_UPDATE_LOCAL, success: true}).finish(); + t.throws(() => operationResultParser(Command.COMMAND_RESET, mismatched), {message: /expected/}); +});