From 02ad152581186f1d55b8e562e83578a3ee2fcd7b Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Fri, 3 Nov 2023 15:40:11 -0400 Subject: [PATCH 1/2] Improve startup logic & code refactor --- .vscode/launch.json | 17 +- package-lock.json | 8 +- package.json | 102 +- sampleWorkspace/.vscode/launch.json | 15 +- src/activateMockDebug.ts | 92 +- src/configuration.ts | 29 +- ...txnGroupWalkerRuntime.ts => avmRuntime.ts} | 28 +- src/debugAdapter/basicServer.ts | 8 +- src/debugAdapter/debugAdapter.ts | 26 +- src/debugAdapter/debugRequestHandlers.ts | 1223 +++++++++-------- src/debugAdapter/traceReplayEngine.ts | 71 +- src/debugAdapter/utils.ts | 101 +- src/descriptorFactory.ts | 71 + src/extension.ts | 124 +- src/fileAccessor.ts | 30 + src/utils.ts | 165 --- tests/adapter.test.ts | 232 ++-- tests/client.ts | 98 +- tests/testing.ts | 33 +- 19 files changed, 1142 insertions(+), 1331 deletions(-) rename src/debugAdapter/{txnGroupWalkerRuntime.ts => avmRuntime.ts} (94%) create mode 100644 src/descriptorFactory.ts create mode 100644 src/fileAccessor.ts delete mode 100644 src/utils.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 628505b..0585be0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,15 +23,20 @@ "preLaunchTask": "npm: compile" }, { - "name": "Tests", + "name": "Debug Mocha Tests", "type": "node", "request": "launch", "cwd": "${workspaceFolder}", - "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "args": ["-u", "tdd", "--timeout", "999999", "--colors", "./out/tests/"], - "outFiles": ["${workspaceFolder}/out/**/*.js"], - "internalConsoleOptions": "openOnSessionStart", - "preLaunchTask": "npm: compile" + "program": "${workspaceFolder}/node_modules/ts-mocha/bin/ts-mocha", + "args": [ + "-p", + "tsconfig.json", + "tests/*test.ts", + "--timeout", + "30s", + "--diff", + "false" + ] } ], "compounds": [ diff --git a/package-lock.json b/package-lock.json index 47f81a3..3b3b8f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,7 @@ "license": "MIT", "dependencies": { "algosdk": "github:jasonpaulos/js-algorand-sdk#teal-source-map-improvements", - "json-bigint": "^1.0.0", - "lodash": "^4.17.21" + "json-bigint": "^1.0.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -3189,11 +3188,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", diff --git a/package.json b/package.json index 404d6c2..00168f8 100644 --- a/package.json +++ b/package.json @@ -108,50 +108,7 @@ "configuration": "./teal-language-configuration.json" } ], - "menus": { - "editor/title/run": [ - { - "command": "extension.teal-debug.runEditorContents", - "when": "resourceLangId == 'markdown' || resourceLangId == 'teal'", - "group": "navigation@1" - }, - { - "command": "extension.teal-debug.debugEditorContents", - "when": "resourceLangId == 'markdown' || resourceLangId == 'teal'", - "group": "navigation@2" - } - ], - "commandPalette": [ - { - "command": "extension.teal-debug.debugEditorContents", - "when": "resourceLangId == 'markdown' || resourceLangId == 'teal'" - }, - { - "command": "extension.teal-debug.runEditorContents", - "when": "resourceLangId == 'markdown' || resourceLangId == 'teal'" - } - ] - }, - "commands": [ - { - "command": "extension.teal-debug.debugEditorContents", - "title": "Debug File", - "category": "TEAL Debug", - "enablement": "!inDebugMode", - "icon": "$(debug-alt)" - }, - { - "command": "extension.teal-debug.runEditorContents", - "title": "Run File", - "category": "TEAL Debug", - "enablement": "!inDebugMode", - "icon": "$(play)" - } - ], "breakpoints": [ - { - "language": "markdown" - }, { "language": "teal" } @@ -160,7 +117,6 @@ { "type": "teal", "languages": [ - "markdown", "teal" ], "label": "TEAL Debug", @@ -169,48 +125,20 @@ "configurationAttributes": { "launch": { "properties": { - "debuggerPath": { - "type": "string", - "description": "Path to Algorand txn-group debugger that take in both simulate trace file and txn-group app source description file." - }, - "simulationTraceFile": { + "simulateTraceFile": { "type": "string", "description": "Transaction group simulation response with execution trace.", - "default": "" - }, - "appSourceDescriptionFile": { - "type": "string", - "description": "Description file for TEAL sources of apps appearing in transaction group.", - "default": "" + "default": "${workspaceFolder}/path/to/simulateTraceFile.json" }, - "program": { + "programSourcesDescriptionFile": { "type": "string", - "description": "Absolute path to a text file.", - "default": "${workspaceFolder}/${command:AskForProgramName}" + "description": "Description file for sources of programs appearing in transaction group.", + "default": "${workspaceFolder}/path/to/programSourcesDescriptionFile.json" }, "stopOnEntry": { "type": "boolean", "description": "Automatically stop after launch.", "default": true - }, - "trace": { - "type": "boolean", - "description": "Enable logging of the Debug Adapter Protocol.", - "default": true - }, - "compileError": { - "type": "string", - "description": "Simulates a compile error in 'launch' request.", - "enum": [ - "default", - "show", - "hide" - ], - "enumDescriptions": [ - "default: show fake compile error to user", - "show fake compile error to user", - "do not show fake compile error to user" - ] } } } @@ -219,23 +147,22 @@ { "type": "teal", "request": "launch", - "appSourceDescriptionFile": "launch.json", - "simulationTraceFile": "launch.json", - "debuggerPath": "yo-a-path-for-deal", - "name": "Ask for file name", - "program": "${workspaceFolder}/${command:AskForProgramName}", + "name": "Debug AVM Transactions", + "simulateTraceFile": "${workspaceFolder}/path/to/simulateTraceFile.json", + "programSourcesDescriptionFile": "${workspaceFolder}/path/to/programSourcesDescriptionFile.json", "stopOnEntry": true } ], "configurationSnippets": [ { - "label": "TEAL Debug: Launch", - "description": "A new configuration for 'debugging' a user selected markdown file.", + "label": "AVM Debug", + "description": "A new configuration for replaying and debugging a Algorand transactions.", "body": { "type": "teal", "request": "launch", - "name": "Ask for file name", - "program": "^\"\\${workspaceFolder}/\\${command:AskForProgramName}\"", + "name": "Debug AVM Transactions", + "simulateTraceFile": "^\"\\${workspaceFolder}/path/to/simulateTraceFile.json\"", + "programSourcesDescriptionFile": "^\"\\${workspaceFolder}/path/to/programSourcesDescriptionFile.json\"", "stopOnEntry": true } } @@ -245,7 +172,6 @@ }, "dependencies": { "algosdk": "github:jasonpaulos/js-algorand-sdk#teal-source-map-improvements", - "json-bigint": "^1.0.0", - "lodash": "^4.17.21" + "json-bigint": "^1.0.0" } } diff --git a/sampleWorkspace/.vscode/launch.json b/sampleWorkspace/.vscode/launch.json index f8aeabf..9d7b51b 100644 --- a/sampleWorkspace/.vscode/launch.json +++ b/sampleWorkspace/.vscode/launch.json @@ -7,19 +7,12 @@ { "type": "teal", "request": "launch", - "name": "Debug Transaction Group", - "debuggerPath": "stuff-to-deal", + "name": "Debug Slot Machine", - // "program": "${workspaceFolder}/stepping-test/simulate-response.json", - // "simulationTraceFile": "${workspaceFolder}/stepping-test/simulate-response.json", - // "appSourceDescriptionFile": "${workspaceFolder}/stepping-test/sources.json", + "simulateTraceFile": "${workspaceFolder}/slot-machine/simulate-response.json", + "programSourcesDescriptionFile": "${workspaceFolder}/slot-machine/sources.json", - "program": "${workspaceFolder}/slot-machine/simulate-response.json", - "simulationTraceFile": "${workspaceFolder}/slot-machine/simulate-response.json", - "appSourceDescriptionFile": "${workspaceFolder}/slot-machine/sources.json", - - "stopOnEntry": true, - "trace": false + "stopOnEntry": true } ] } diff --git a/src/activateMockDebug.ts b/src/activateMockDebug.ts index 89e336d..6f578d4 100644 --- a/src/activateMockDebug.ts +++ b/src/activateMockDebug.ts @@ -1,54 +1,13 @@ 'use strict'; import * as vscode from 'vscode'; -import { FileAccessor } from './debugAdapter/utils'; -import { TEALDebugAdapterDescriptorFactory } from './extension'; +import { TEALDebugAdapterDescriptorFactory } from './descriptorFactory'; import { TealDebugConfigProvider } from './configuration'; export function activateTealDebug( context: vscode.ExtensionContext, factory: TEALDebugAdapterDescriptorFactory, - config: vscode.DebugConfiguration, ) { - context.subscriptions.push( - vscode.commands.registerCommand( - 'extension.teal-debug.runEditorContents', - (resource: vscode.Uri) => { - let targetResource = resource; - if (!targetResource && vscode.window.activeTextEditor) { - targetResource = vscode.window.activeTextEditor.document.uri; - } - if (targetResource) { - vscode.debug.startDebugging( - undefined, - { - ...config, - name: 'Run File', - program: targetResource.fsPath, - }, - { noDebug: true }, - ); - } - }, - ), - vscode.commands.registerCommand( - 'extension.teal-debug.debugEditorContents', - (resource: vscode.Uri) => { - let targetResource = resource; - if (!targetResource && vscode.window.activeTextEditor) { - targetResource = vscode.window.activeTextEditor.document.uri; - } - if (targetResource) { - vscode.debug.startDebugging(undefined, { - ...config, - name: 'Debug File', - program: targetResource.fsPath, - }); - } - }, - ), - ); - // register a configuration provider for 'teal' debug type const provider = new TealDebugConfigProvider(); context.subscriptions.push( @@ -61,53 +20,4 @@ export function activateTealDebug( // https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/#events // see events, by the end of subscription, call `dispose()` to release resource. context.subscriptions.push(factory); - - // override VS Code's default implementation of the debug hover - // here we match only Mock "variables", that are words starting with an '$' - context.subscriptions.push( - vscode.languages.registerEvaluatableExpressionProvider('markdown', { - provideEvaluatableExpression( - document: vscode.TextDocument, - position: vscode.Position, - ): vscode.ProviderResult { - const VARIABLE_REGEXP = /\$[a-z][a-z0-9]*/gi; - const line = document.lineAt(position.line).text; - - let m: RegExpExecArray | null; - while ((m = VARIABLE_REGEXP.exec(line))) { - const varRange = new vscode.Range( - position.line, - m.index, - position.line, - m.index + m[0].length, - ); - - if (varRange.contains(position)) { - return new vscode.EvaluatableExpression(varRange); - } - } - return undefined; - }, - }), - ); -} - -export const workspaceFileAccessor: FileAccessor = { - isWindows: typeof process !== 'undefined' && process.platform === 'win32', - async readFile(path: string): Promise { - const uri = pathToUri(path); - return await vscode.workspace.fs.readFile(uri); - }, - async writeFile(path: string, contents: Uint8Array) { - const uri = pathToUri(path); - await vscode.workspace.fs.writeFile(uri, contents); - }, -}; - -function pathToUri(path: string) { - try { - return vscode.Uri.file(path); - } catch (e) { - return vscode.Uri.parse(path, true); - } } diff --git a/src/configuration.ts b/src/configuration.ts index 4388fb2..9b7a6f7 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -20,28 +20,19 @@ export class TealDebugConfigProvider config: DebugConfiguration, _token?: CancellationToken, ): ProviderResult { - // NOTE: log the overloaded config to window - vscode.window.showInformationMessage(JSON.stringify(config)); - // Check necessary part, we do need these 2 files for debug - if (!config.simulationTraceFile) { - return vscode.window - .showInformationMessage( - 'missing critical part: simulationTraceFile in launch.json', - ) - .then((_) => { - return undefined; - }); + if (!config.simulateTraceFile) { + vscode.window.showErrorMessage( + 'Missing property "simulateTraceFile" in debug config', + ); + return null; } - if (!config.appSourceDescriptionFile) { - return vscode.window - .showInformationMessage( - 'missing critical part: appSourceDescriptionFile in launch.json', - ) - .then((_) => { - return undefined; - }); + if (!config.programSourcesDescriptionFile) { + vscode.window.showErrorMessage( + 'Missing property "programSourcesDescriptionFile" in debug config', + ); + return null; } return config; diff --git a/src/debugAdapter/txnGroupWalkerRuntime.ts b/src/debugAdapter/avmRuntime.ts similarity index 94% rename from src/debugAdapter/txnGroupWalkerRuntime.ts rename to src/debugAdapter/avmRuntime.ts index 970f5d3..c1f486b 100644 --- a/src/debugAdapter/txnGroupWalkerRuntime.ts +++ b/src/debugAdapter/avmRuntime.ts @@ -9,7 +9,7 @@ import { import { FileAccessor, TEALDebuggingAssets, - TxnGroupSourceDescriptor, + ProgramSourceDescriptor, } from './utils'; export interface IRuntimeBreakpoint { @@ -33,22 +33,28 @@ interface IRuntimeStack { frames: TraceReplayStackFrame[]; } -export class TxnGroupWalkerRuntime extends EventEmitter { +export class AvmRuntime extends EventEmitter { // maps from sourceFile to array of IRuntimeBreakpoint - private breakPoints = new Map(); + private readonly breakPoints = new Map(); - private engine: TraceReplayEngine; + private readonly engine: TraceReplayEngine = new TraceReplayEngine(); // since we want to send breakpoint events, we will assign an id to every event // so that the frontend can match events with breakpoints. private breakpointId = 1; - constructor( - private fileAccessor: FileAccessor, - debugAssets: TEALDebuggingAssets, - ) { + constructor(private readonly fileAccessor: FileAccessor) { super(); - this.engine = new TraceReplayEngine(debugAssets); + } + + public onLaunch(debugAssets: TEALDebuggingAssets): Promise { + return this.engine.loadResources(debugAssets); + } + + public reset() { + this.breakPoints.clear(); + this.breakpointId = 1; + this.engine.reset(); } private nextTickWithErrorReporting(fn: () => Promise | void) { @@ -374,11 +380,11 @@ export class TxnGroupWalkerRuntime extends EventEmitter { private findSourceDescriptorsForPath( filePath: string, - ): Array<{ descriptor: TxnGroupSourceDescriptor; sourceIndex: number }> { + ): Array<{ descriptor: ProgramSourceDescriptor; sourceIndex: number }> { filePath = this.normalizePathAndCasing(filePath); const sourceDescriptors: Array<{ - descriptor: TxnGroupSourceDescriptor; + descriptor: ProgramSourceDescriptor; sourceIndex: number; }> = []; diff --git a/src/debugAdapter/basicServer.ts b/src/debugAdapter/basicServer.ts index 7835f53..016bd40 100644 --- a/src/debugAdapter/basicServer.ts +++ b/src/debugAdapter/basicServer.ts @@ -1,13 +1,13 @@ import * as Net from 'net'; -import { TxnGroupDebugSession } from './debugRequestHandlers'; -import { FileAccessor, TEALDebuggingAssets } from './utils'; +import { AvmDebugSession } from './debugRequestHandlers'; +import { FileAccessor } from './utils'; export class BasicServer { private server: Net.Server; - constructor(fileAccessor: FileAccessor, debugAssets: TEALDebuggingAssets) { + constructor(fileAccessor: FileAccessor) { this.server = Net.createServer((socket) => { - const session = new TxnGroupDebugSession(fileAccessor, debugAssets); + const session = new AvmDebugSession(fileAccessor); session.setRunAsServer(true); session.start(socket as NodeJS.ReadableStream, socket); socket.on('error', (err) => { diff --git a/src/debugAdapter/debugAdapter.ts b/src/debugAdapter/debugAdapter.ts index 18f7b0f..bd1c5e6 100644 --- a/src/debugAdapter/debugAdapter.ts +++ b/src/debugAdapter/debugAdapter.ts @@ -1,9 +1,7 @@ -import { TxnGroupDebugSession } from './debugRequestHandlers'; - import { promises as fs } from 'fs'; -import * as path from 'path'; import * as Net from 'net'; -import { FileAccessor, TEALDebuggingAssets } from './utils'; +import { FileAccessor } from './utils'; +import { AvmDebugSession } from './debugRequestHandlers'; /* * debugAdapter.js is the entrypoint of the debug adapter when it runs as a separate process. @@ -36,9 +34,6 @@ async function run() { // first parse command line arguments to see whether the debug adapter should run as a server let port = 0; - const simulateResponsePath = process.env.ALGORAND_SIMULATION_RESPONSE_PATH; - const txnGroupSourcesDescriptionPath = - process.env.ALGORAND_TXN_GROUP_SOURCES_DESCRIPTION_PATH; const args = process.argv.slice(2); args.forEach(function (val, index, array) { @@ -48,19 +43,6 @@ async function run() { } }); - if (typeof simulateResponsePath === 'undefined') { - throw new Error('missing ALGORAND_SIMULATION_RESPONSE_PATH'); - } - if (typeof txnGroupSourcesDescriptionPath === 'undefined') { - throw new Error('missing ALGORAND_TXN_GROUP_SOURCES_DESCRIPTION_PATH'); - } - - const assets = await TEALDebuggingAssets.loadFromFiles( - fsAccessor, - path.resolve(simulateResponsePath), - path.resolve(txnGroupSourcesDescriptionPath), - ); - if (port > 0) { // start a server that creates a new session for every connection request console.error(`waiting for debug protocol on port ${port}`); @@ -72,7 +54,7 @@ async function run() { socket.on('end', () => { console.error('>> client connection closed\n'); }); - const session = new TxnGroupDebugSession(fsAccessor, assets); + const session = new AvmDebugSession(fsAccessor); session.setRunAsServer(true); session.start(socket, socket); }).listen(port); @@ -81,7 +63,7 @@ async function run() { }); } else { // start a single session that communicates via stdin/stdout - const session = new TxnGroupDebugSession(fsAccessor, assets); + const session = new AvmDebugSession(fsAccessor); process.on('SIGTERM', () => { session.shutdown(); }); diff --git a/src/debugAdapter/debugRequestHandlers.ts b/src/debugAdapter/debugRequestHandlers.ts index ec14dbf..756170b 100644 --- a/src/debugAdapter/debugRequestHandlers.ts +++ b/src/debugAdapter/debugRequestHandlers.ts @@ -1,7 +1,5 @@ import { - Logger, - logger, - LoggingDebugSession, + DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, @@ -16,10 +14,7 @@ import { } from '@vscode/debugadapter'; import { DebugProtocol } from '@vscode/debugprotocol'; import { basename } from 'path-browserify'; -import { - TxnGroupWalkerRuntime, - IRuntimeBreakpoint, -} from './txnGroupWalkerRuntime'; +import { AvmRuntime, IRuntimeBreakpoint } from './avmRuntime'; import { ProgramStackFrame } from './traceReplayEngine'; import { Subject } from 'await-notify'; import * as algosdk from 'algosdk'; @@ -30,6 +25,8 @@ import { limitArray, } from './utils'; +const GENERIC_ERROR_ID = 9999; + export enum RuntimeEvents { stopOnEntry = 'stopOnEntry', stopOnStep = 'stopOnStep', @@ -45,28 +42,25 @@ export enum RuntimeEvents { * The schema for these attributes lives in the package.json of the teal-debug extension. * The interface should always match this schema. */ -interface ILaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { - /** An absolute path to the "program" to debug. */ - program: string; +export interface ILaunchRequestArguments + extends DebugProtocol.LaunchRequestArguments { + /** An absolute path to the simulate response to debug. */ + simulateTraceFile: string; + /** An absolute path to the file which maps programs to source maps. */ + programSourcesDescriptionFile: string; /** Automatically stop target after launch. If not specified, target does not stop. */ stopOnEntry?: boolean; - /** enable logging the Debug Adapter Protocol */ - trace?: boolean; - /** run without debugging */ - noDebug?: boolean; - /** if specified, results in a simulated compile error in launch. */ - compileError?: 'default' | 'show' | 'hide'; } // eslint-disable-next-line @typescript-eslint/no-empty-interface interface IAttachRequestArguments extends ILaunchRequestArguments {} -export class TxnGroupDebugSession extends LoggingDebugSession { +export class AvmDebugSession extends DebugSession { // we don't support multiple threads, so we can use a hardcoded ID for the default thread private static threadID = 1; // txn group walker runtime for walking txn group. - private _runtime: TxnGroupWalkerRuntime; + private _runtime: AvmRuntime; private _variableHandles = new Handles< | ProgramStateScope @@ -83,37 +77,28 @@ export class TxnGroupDebugSession extends LoggingDebugSession { private _configurationDone = new Subject(); - private _debugAssets: TEALDebuggingAssets; - /** * Creates a new debug adapter that is used for one debug session. * We configure the default implementation of a debug adapter here. */ - public constructor( - fileAccessor: FileAccessor, - debugAssets: TEALDebuggingAssets, - ) { - super('mock-debug.txt'); - - this._debugAssets = debugAssets; + public constructor(private readonly fileAccessor: FileAccessor) { + super(); // this debugger uses zero-based lines and columns this.setDebuggerLinesStartAt1(false); this.setDebuggerColumnsStartAt1(false); - this._runtime = new TxnGroupWalkerRuntime(fileAccessor, this._debugAssets); + this._runtime = new AvmRuntime(fileAccessor); // setup event handlers this._runtime.on(RuntimeEvents.stopOnEntry, () => { - this.sendEvent(new StoppedEvent('entry', TxnGroupDebugSession.threadID)); + this.sendEvent(new StoppedEvent('entry', AvmDebugSession.threadID)); }); this._runtime.on(RuntimeEvents.stopOnStep, () => { - this.sendEvent(new StoppedEvent('step', TxnGroupDebugSession.threadID)); + this.sendEvent(new StoppedEvent('step', AvmDebugSession.threadID)); }); this._runtime.on(RuntimeEvents.stopOnBreakpoint, () => { - this.sendEvent( - new StoppedEvent('breakpoint', TxnGroupDebugSession.threadID), - ); + this.sendEvent(new StoppedEvent('breakpoint', AvmDebugSession.threadID)); }); this._runtime.on( RuntimeEvents.breakpointValidated, @@ -207,8 +192,8 @@ export class TxnGroupDebugSession extends LoggingDebugSession { // response.body.supportsReadMemoryRequest = true; // response.body.supportsWriteMemoryRequest = true; - response.body.supportSuspendDebuggee = true; - response.body.supportTerminateDebuggee = true; + // response.body.supportSuspendDebuggee = true; + // response.body.supportTerminateDebuggee = true; // response.body.supportsFunctionBreakpoints = true; response.body.supportsDelayedStackTraceLoading = true; @@ -239,9 +224,12 @@ export class TxnGroupDebugSession extends LoggingDebugSession { args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request, ): void { - console.log( - `disconnectRequest suspend: ${args.suspendDebuggee}, terminate: ${args.terminateDebuggee}`, - ); + try { + this._runtime.reset(); + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected async attachRequest( @@ -255,66 +243,73 @@ export class TxnGroupDebugSession extends LoggingDebugSession { response: DebugProtocol.LaunchResponse, args: ILaunchRequestArguments, ) { - // make sure to 'Stop' the buffered logging if 'trace' is not set - logger.setup( - args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Stop, - false, - ); + try { + const debugAssets = await TEALDebuggingAssets.loadFromFiles( + this.fileAccessor, + args.simulateTraceFile, + args.programSourcesDescriptionFile, + ); - // TODO: use args.program + await this._runtime.onLaunch(debugAssets); - // wait 1 second until configuration has finished (and configurationDoneRequest has been called) - await this._configurationDone.wait(1000); + // wait 1 second until configuration has finished (and configurationDoneRequest has been called) + await this._configurationDone.wait(1000); - // start the program in the runtime - this._runtime.start(!!args.stopOnEntry, !args.noDebug); + // start the program in the runtime + this._runtime.start(!!args.stopOnEntry, !args.noDebug); - this.sendResponse(response); + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected setBreakPointsRequest( response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments, ): void { - const { path } = args.source; - if (typeof path !== 'undefined') { - const clientBreakpoints = args.breakpoints || []; - - // clear all breakpoints for this file - this._runtime.clearBreakpoints(path); - - // set and verify breakpoint locations - const actualBreakpoints = clientBreakpoints.map((clientBp) => { - const line = this.convertClientLineToDebugger(clientBp.line); - const column = - typeof clientBp.column === 'number' - ? this.convertClientColumnToDebugger(clientBp.column) - : undefined; - const runtimeBreakpoint = this._runtime.setBreakPoint( - path, - line, - column, - ); - const bp = new Breakpoint( - runtimeBreakpoint.verified, - this.convertDebuggerLineToClient(runtimeBreakpoint.location.line), - typeof runtimeBreakpoint.location.column !== 'undefined' - ? this.convertDebuggerColumnToClient( - runtimeBreakpoint.location.column, - ) - : undefined, - ) as DebugProtocol.Breakpoint; - bp.id = runtimeBreakpoint.id; - return bp; - }); + try { + const { path } = args.source; + if (typeof path !== 'undefined') { + const clientBreakpoints = args.breakpoints || []; + + // clear all breakpoints for this file + this._runtime.clearBreakpoints(path); + + // set and verify breakpoint locations + const actualBreakpoints = clientBreakpoints.map((clientBp) => { + const line = this.convertClientLineToDebugger(clientBp.line); + const column = + typeof clientBp.column === 'number' + ? this.convertClientColumnToDebugger(clientBp.column) + : undefined; + const runtimeBreakpoint = this._runtime.setBreakPoint( + path, + line, + column, + ); + const bp = new Breakpoint( + runtimeBreakpoint.verified, + this.convertDebuggerLineToClient(runtimeBreakpoint.location.line), + typeof runtimeBreakpoint.location.column !== 'undefined' + ? this.convertDebuggerColumnToClient( + runtimeBreakpoint.location.column, + ) + : undefined, + ) as DebugProtocol.Breakpoint; + bp.id = runtimeBreakpoint.id; + return bp; + }); - // send back the actual breakpoint positions - response.body = { - breakpoints: actualBreakpoints, - }; + // send back the actual breakpoint positions + response.body = { + breakpoints: actualBreakpoints, + }; + } + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); } - - this.sendResponse(response); } protected breakpointLocationsRequest( @@ -322,54 +317,58 @@ export class TxnGroupDebugSession extends LoggingDebugSession { args: DebugProtocol.BreakpointLocationsArguments, request?: DebugProtocol.Request, ): void { - const { path } = args.source; - if (typeof path !== 'undefined') { - const startLine = this.convertClientLineToDebugger(args.line); - const endLine = - typeof args.endLine === 'number' - ? this.convertClientLineToDebugger(args.endLine) - : startLine; - const startColumn = - typeof args.column === 'number' - ? this.convertClientColumnToDebugger(args.column) - : 0; - const endColumn = - typeof args.endColumn === 'number' - ? this.convertClientColumnToDebugger(args.endColumn) - : Number.MAX_SAFE_INTEGER; - - const locations = this._runtime - .breakpointLocations(path) - .filter( - ({ line, column }) => - line >= startLine && - line <= endLine && - (typeof column !== 'undefined' - ? column >= startColumn && column <= endColumn - : true), - ); + try { + const { path } = args.source; + if (typeof path !== 'undefined') { + const startLine = this.convertClientLineToDebugger(args.line); + const endLine = + typeof args.endLine === 'number' + ? this.convertClientLineToDebugger(args.endLine) + : startLine; + const startColumn = + typeof args.column === 'number' + ? this.convertClientColumnToDebugger(args.column) + : 0; + const endColumn = + typeof args.endColumn === 'number' + ? this.convertClientColumnToDebugger(args.endColumn) + : Number.MAX_SAFE_INTEGER; + + const locations = this._runtime + .breakpointLocations(path) + .filter( + ({ line, column }) => + line >= startLine && + line <= endLine && + (typeof column !== 'undefined' + ? column >= startColumn && column <= endColumn + : true), + ); - const responseBreakpoints: DebugProtocol.BreakpointLocation[] = []; - for (const location of locations) { - responseBreakpoints.push({ - line: this.convertDebuggerLineToClient(location.line), - column: - typeof location.column !== 'undefined' - ? this.convertDebuggerColumnToClient(location.column) - : undefined, - }); + const responseBreakpoints: DebugProtocol.BreakpointLocation[] = []; + for (const location of locations) { + responseBreakpoints.push({ + line: this.convertDebuggerLineToClient(location.line), + column: + typeof location.column !== 'undefined' + ? this.convertDebuggerColumnToClient(location.column) + : undefined, + }); + } + response.body = { + breakpoints: responseBreakpoints, + }; } - response.body = { - breakpoints: responseBreakpoints, - }; + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); } - this.sendResponse(response); } protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { // runtime supports no threads so just return a default thread. response.body = { - threads: [new Thread(TxnGroupDebugSession.threadID, 'thread 1')], + threads: [new Thread(AvmDebugSession.threadID, 'thread 1')], }; this.sendResponse(response); } @@ -378,104 +377,112 @@ export class TxnGroupDebugSession extends LoggingDebugSession { response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments, ): void { - const startFrame = - typeof args.startFrame === 'number' ? args.startFrame : 0; - const maxLevels = typeof args.levels === 'number' ? args.levels : 1000; - - // The runtime has a stack where the latest call is the last element. We need to return the - // reverse of that. - const adjustedEndFrame = this._runtime.stackLength() - startFrame; - const adjustedStartFrame = Math.max(0, adjustedEndFrame - maxLevels); - - const stk = this._runtime.stack(adjustedStartFrame, adjustedEndFrame); - - const stackFramesForResponse = stk.frames.map((frame, index) => { - const id = adjustedStartFrame + index; - - const sourceFile = frame.sourceFile(); - let source: Source | undefined = undefined; - if (typeof sourceFile.path !== 'undefined') { - source = this.createSource(sourceFile.path); - } else if (typeof sourceFile.content !== 'undefined') { - source = this.createSourceWithContent( - sourceFile.name, - sourceFile.content, - sourceFile.contentMimeType, - ); - } + try { + const startFrame = + typeof args.startFrame === 'number' ? args.startFrame : 0; + const maxLevels = typeof args.levels === 'number' ? args.levels : 1000; + + // The runtime has a stack where the latest call is the last element. We need to return the + // reverse of that. + const adjustedEndFrame = this._runtime.stackLength() - startFrame; + const adjustedStartFrame = Math.max(0, adjustedEndFrame - maxLevels); + + const stk = this._runtime.stack(adjustedStartFrame, adjustedEndFrame); + + const stackFramesForResponse = stk.frames.map((frame, index) => { + const id = adjustedStartFrame + index; + + const sourceFile = frame.sourceFile(); + let source: Source | undefined = undefined; + if (typeof sourceFile.path !== 'undefined') { + source = this.createSource(sourceFile.path); + } else if (typeof sourceFile.content !== 'undefined') { + source = this.createSourceWithContent( + sourceFile.name, + sourceFile.content, + sourceFile.contentMimeType, + ); + } - const sourceLocation = frame.sourceLocation(); - const line = this.convertDebuggerLineToClient(sourceLocation.line); - const column = - typeof sourceLocation.column !== 'undefined' - ? this.convertDebuggerColumnToClient(sourceLocation.column) - : undefined; - - const protocolFrame = new StackFrame( - id, - frame.name(), - source, - line, - column, - ); - protocolFrame.endLine = - typeof sourceLocation.endLine !== 'undefined' - ? this.convertDebuggerLineToClient(sourceLocation.endLine) - : undefined; - protocolFrame.endColumn = - typeof sourceLocation.endColumn !== 'undefined' - ? this.convertDebuggerColumnToClient(sourceLocation.endColumn) - : undefined; - return protocolFrame; - }); - stackFramesForResponse.reverse(); + const sourceLocation = frame.sourceLocation(); + const line = this.convertDebuggerLineToClient(sourceLocation.line); + const column = + typeof sourceLocation.column !== 'undefined' + ? this.convertDebuggerColumnToClient(sourceLocation.column) + : undefined; - response.body = { - totalFrames: stk.count, - stackFrames: stackFramesForResponse, - // 4 options for 'totalFrames': - //omit totalFrames property: // VS Code has to probe/guess. Should result in a max. of two requests - // totalFrames: stk.count // stk.count is the correct size, should result in a max. of two requests - //totalFrames: 1000000 // not the correct size, should result in a max. of two requests - //totalFrames: endFrame + 20 // dynamically increases the size with every requested chunk, results in paging - }; - this.sendResponse(response); + const protocolFrame = new StackFrame( + id, + frame.name(), + source, + line, + column, + ); + protocolFrame.endLine = + typeof sourceLocation.endLine !== 'undefined' + ? this.convertDebuggerLineToClient(sourceLocation.endLine) + : undefined; + protocolFrame.endColumn = + typeof sourceLocation.endColumn !== 'undefined' + ? this.convertDebuggerColumnToClient(sourceLocation.endColumn) + : undefined; + return protocolFrame; + }); + stackFramesForResponse.reverse(); + + response.body = { + totalFrames: stk.count, + stackFrames: stackFramesForResponse, + // 4 options for 'totalFrames': + //omit totalFrames property: // VS Code has to probe/guess. Should result in a max. of two requests + // totalFrames: stk.count // stk.count is the correct size, should result in a max. of two requests + //totalFrames: 1000000 // not the correct size, should result in a max. of two requests + //totalFrames: endFrame + 20 // dynamically increases the size with every requested chunk, results in paging + }; + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected scopesRequest( response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments, ): void { - const frame = this._runtime.getStackFrame(args.frameId); - - const scopes: DebugProtocol.Scope[] = []; - if (typeof frame !== 'undefined') { - if (frame instanceof ProgramStackFrame) { - const programScope = new ProgramStateScope(args.frameId); - let scopeName = 'Program State'; - const appID = frame.currentAppID(); - if (typeof appID !== 'undefined') { - scopeName += `: App ${appID}`; + try { + const frame = this._runtime.getStackFrame(args.frameId); + + const scopes: DebugProtocol.Scope[] = []; + if (typeof frame !== 'undefined') { + if (frame instanceof ProgramStackFrame) { + const programScope = new ProgramStateScope(args.frameId); + let scopeName = 'Program State'; + const appID = frame.currentAppID(); + if (typeof appID !== 'undefined') { + scopeName += `: App ${appID}`; + } + scopes.push( + new Scope( + scopeName, + this._variableHandles.create(programScope), + false, + ), + ); } scopes.push( new Scope( - scopeName, - this._variableHandles.create(programScope), + 'On-chain State', + this._variableHandles.create('chain'), false, ), ); } - scopes.push( - new Scope( - 'On-chain State', - this._variableHandles.create('chain'), - false, - ), - ); - } - response.body = { scopes }; - this.sendResponse(response); + response.body = { scopes }; + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected async variablesRequest( @@ -483,404 +490,414 @@ export class TxnGroupDebugSession extends LoggingDebugSession { args: DebugProtocol.VariablesArguments, request?: DebugProtocol.Request, ): Promise { - let variables: DebugProtocol.Variable[] = []; - - const v = this._variableHandles.get(args.variablesReference); + try { + let variables: DebugProtocol.Variable[] = []; - if (v instanceof ProgramStateScope) { - const frame = this._runtime.getStackFrame(v.frameIndex); - if (!frame || !(frame instanceof ProgramStackFrame)) { - throw new Error(`Unexpected frame: ${typeof frame}`); - } - const programState = frame.state; + const v = this._variableHandles.get(args.variablesReference); - if (typeof v.specificState === 'undefined') { + if (v instanceof ProgramStateScope) { + const frame = this._runtime.getStackFrame(v.frameIndex); + if (!frame || !(frame instanceof ProgramStackFrame)) { + throw new Error(`Unexpected frame: ${typeof frame}`); + } + const programState = frame.state; + + if (typeof v.specificState === 'undefined') { + variables = [ + { + name: 'pc', + value: programState.pc.toString(), + type: 'uint64', + variablesReference: 0, + evaluateName: 'pc', + }, + { + name: 'stack', + value: programState.stack.length === 0 ? '[]' : '[...]', + type: 'array', + variablesReference: this._variableHandles.create( + new ProgramStateScope(v.frameIndex, 'stack'), + ), + indexedVariables: programState.stack.length, + presentationHint: { + kind: 'data', + }, + }, + { + name: 'scratch', + value: '[...]', + type: 'array', + variablesReference: this._variableHandles.create( + new ProgramStateScope(v.frameIndex, 'scratch'), + ), + indexedVariables: 256, + presentationHint: { + kind: 'data', + }, + }, + ]; + } else if (v.specificState === 'stack') { + if (args.filter !== 'named') { + variables = programState.stack.map((value, index) => + this.convertAvmValue(v, value, index), + ); + } + } else if (v.specificState === 'scratch') { + const expandedScratch: algosdk.modelsv2.AvmValue[] = []; + for (let i = 0; i < 256; i++) { + expandedScratch.push( + programState.scratch.get(i) || + new algosdk.modelsv2.AvmValue({ type: 2 }), + ); + } + if (args.filter !== 'named') { + variables = expandedScratch.map((value, index) => + this.convertAvmValue(v, value, index), + ); + } + } + } else if (v === 'chain') { + const appIDs = this._runtime.getAppStateReferences(); variables = [ { - name: 'pc', - value: programState.pc.toString(), - type: 'uint64', - variablesReference: 0, - evaluateName: 'pc', + name: 'app', + value: '', + type: 'object', + variablesReference: this._variableHandles.create('app'), + namedVariables: appIDs.length, }, + ]; + } else if (v === 'app') { + const appIDs = this._runtime.getAppStateReferences(); + variables = appIDs.map((appID) => ({ + name: appID.toString(), + value: '', + type: 'object', + variablesReference: this._variableHandles.create( + new AppStateScope(appID), + ), + namedVariables: 3, + })); + } else if (v instanceof AppStateScope) { + variables = [ { - name: 'stack', - value: programState.stack.length === 0 ? '[]' : '[...]', - type: 'array', + name: 'global', + value: '', + type: 'object', variablesReference: this._variableHandles.create( - new ProgramStateScope(v.frameIndex, 'stack'), + new AppSpecificStateScope({ scope: 'global', appID: v.appID }), ), - indexedVariables: programState.stack.length, - presentationHint: { - kind: 'data', - }, + namedVariables: 1, // TODO }, { - name: 'scratch', - value: '[...]', - type: 'array', + name: 'local', + value: '', + type: 'object', variablesReference: this._variableHandles.create( - new ProgramStateScope(v.frameIndex, 'scratch'), + new AppSpecificStateScope({ scope: 'local', appID: v.appID }), ), - indexedVariables: 256, - presentationHint: { - kind: 'data', - }, + namedVariables: 1, // TODO }, - ]; - } else if (v.specificState === 'stack') { - if (args.filter !== 'named') { - variables = programState.stack.map((value, index) => - this.convertAvmValue(v, value, index), - ); - } - } else if (v.specificState === 'scratch') { - const expandedScratch: algosdk.modelsv2.AvmValue[] = []; - for (let i = 0; i < 256; i++) { - expandedScratch.push( - programState.scratch.get(i) || - new algosdk.modelsv2.AvmValue({ type: 2 }), - ); - } - if (args.filter !== 'named') { - variables = expandedScratch.map((value, index) => - this.convertAvmValue(v, value, index), - ); - } - } - } else if (v === 'chain') { - const appIDs = this._runtime.getAppStateReferences(); - variables = [ - { - name: 'app', - value: '', - type: 'object', - variablesReference: this._variableHandles.create('app'), - namedVariables: appIDs.length, - }, - ]; - } else if (v === 'app') { - const appIDs = this._runtime.getAppStateReferences(); - variables = appIDs.map((appID) => ({ - name: appID.toString(), - value: '', - type: 'object', - variablesReference: this._variableHandles.create( - new AppStateScope(appID), - ), - namedVariables: 3, - })); - } else if (v instanceof AppStateScope) { - variables = [ - { - name: 'global', - value: '', - type: 'object', - variablesReference: this._variableHandles.create( - new AppSpecificStateScope({ scope: 'global', appID: v.appID }), - ), - namedVariables: 1, // TODO - }, - { - name: 'local', - value: '', - type: 'object', - variablesReference: this._variableHandles.create( - new AppSpecificStateScope({ scope: 'local', appID: v.appID }), - ), - namedVariables: 1, // TODO - }, - { - name: 'box', - value: '', - type: 'object', - variablesReference: this._variableHandles.create( - new AppSpecificStateScope({ scope: 'box', appID: v.appID }), - ), - namedVariables: 1, // TODO - }, - ]; - } else if (v instanceof AppSpecificStateScope) { - const state = this._runtime.getAppState(v.appID); - if (v.scope === 'global') { - variables = state - .globalStateArray() - .map((kv) => this.convertAvmKeyValue(v, kv)); - } else if (v.scope === 'local') { - if (typeof v.account === 'undefined') { - const accounts = this._runtime.getAppLocalStateAccounts(v.appID); - variables = accounts.map((account) => ({ - name: account, - value: 'local state', + { + name: 'box', + value: '', type: 'object', variablesReference: this._variableHandles.create( - new AppSpecificStateScope({ - scope: 'local', - appID: v.appID, - account, - }), + new AppSpecificStateScope({ scope: 'box', appID: v.appID }), ), namedVariables: 1, // TODO - evaluateName: evaluateNameForScope(v, account), - })); - } else { + }, + ]; + } else if (v instanceof AppSpecificStateScope) { + const state = this._runtime.getAppState(v.appID); + if (v.scope === 'global') { variables = state - .localStateArray(v.account) + .globalStateArray() .map((kv) => this.convertAvmKeyValue(v, kv)); - } - } else if (v.scope === 'box') { - variables = state - .boxStateArray() - .map((kv) => this.convertAvmKeyValue(v, kv)); - } - } else if (v instanceof AvmValueReference) { - if (v.scope instanceof ProgramStateScope) { - const frame = this._runtime.getStackFrame(v.scope.frameIndex); - if (!frame || !(frame instanceof ProgramStackFrame)) { - throw new Error(`Unexpected frame: ${typeof frame}`); - } - let toExpand: algosdk.modelsv2.AvmValue; - if (v.scope.specificState === 'stack') { - toExpand = frame.state.stack[v.key as number]; - } else if (v.scope.specificState === 'scratch') { - toExpand = - frame.state.scratch.get(v.key as number) || - new algosdk.modelsv2.AvmValue({ type: 2 }); - } else { - throw new Error(`Unexpected AvmValueReference scope: ${v.scope}`); - } - variables = this.expandAvmValue(toExpand, args.filter); - } else if ( - v.scope instanceof AppSpecificStateScope && - typeof v.key === 'string' && - v.key.startsWith('0x') - ) { - let toExpand: algosdk.modelsv2.AvmKeyValue; - const state = this._runtime.getAppState(v.scope.appID); - const keyHex = v.key.slice(2); - if (v.scope.scope === 'global') { - const value = state.globalState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - toExpand = new algosdk.modelsv2.AvmKeyValue({ - key: keyBytes, - value, - }); + } else if (v.scope === 'local') { + if (typeof v.account === 'undefined') { + const accounts = this._runtime.getAppLocalStateAccounts(v.appID); + variables = accounts.map((account) => ({ + name: account, + value: 'local state', + type: 'object', + variablesReference: this._variableHandles.create( + new AppSpecificStateScope({ + scope: 'local', + appID: v.appID, + account, + }), + ), + namedVariables: 1, // TODO + evaluateName: evaluateNameForScope(v, account), + })); } else { - throw new Error(`key "${v.key}" not found in global state`); + variables = state + .localStateArray(v.account) + .map((kv) => this.convertAvmKeyValue(v, kv)); } - } else if (v.scope.scope === 'local') { - if (typeof v.scope.account === 'undefined') { - throw new Error("this shouldn't happen: " + JSON.stringify(v)); + } else if (v.scope === 'box') { + variables = state + .boxStateArray() + .map((kv) => this.convertAvmKeyValue(v, kv)); + } + } else if (v instanceof AvmValueReference) { + if (v.scope instanceof ProgramStateScope) { + const frame = this._runtime.getStackFrame(v.scope.frameIndex); + if (!frame || !(frame instanceof ProgramStackFrame)) { + throw new Error(`Unexpected frame: ${typeof frame}`); + } + let toExpand: algosdk.modelsv2.AvmValue; + if (v.scope.specificState === 'stack') { + toExpand = frame.state.stack[v.key as number]; + } else if (v.scope.specificState === 'scratch') { + toExpand = + frame.state.scratch.get(v.key as number) || + new algosdk.modelsv2.AvmValue({ type: 2 }); } else { - const accountState = state.localState.get(v.scope.account); - if (!accountState) { - throw new Error( - `account "${v.scope.account}" not found in local state`, - ); + throw new Error(`Unexpected AvmValueReference scope: ${v.scope}`); + } + variables = this.expandAvmValue(toExpand, args.filter); + } else if ( + v.scope instanceof AppSpecificStateScope && + typeof v.key === 'string' && + v.key.startsWith('0x') + ) { + let toExpand: algosdk.modelsv2.AvmKeyValue; + const state = this._runtime.getAppState(v.scope.appID); + const keyHex = v.key.slice(2); + if (v.scope.scope === 'global') { + const value = state.globalState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + toExpand = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + } else { + throw new Error(`key "${v.key}" not found in global state`); } - const value = accountState.getHex(keyHex); - if (!value) { - throw new Error( - `key "${v.key}" not found in local state for account "${v.scope.account}"`, - ); + } else if (v.scope.scope === 'local') { + if (typeof v.scope.account === 'undefined') { + throw new Error("this shouldn't happen: " + JSON.stringify(v)); + } else { + const accountState = state.localState.get(v.scope.account); + if (!accountState) { + throw new Error( + `account "${v.scope.account}" not found in local state`, + ); + } + const value = accountState.getHex(keyHex); + if (!value) { + throw new Error( + `key "${v.key}" not found in local state for account "${v.scope.account}"`, + ); + } + toExpand = new algosdk.modelsv2.AvmKeyValue({ + key: Buffer.from(keyHex, 'hex'), + value, + }); + } + } else if (v.scope.scope === 'box') { + const value = state.boxState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + toExpand = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + } else { + throw new Error(`key "${v.key}" not found in box state`); } - toExpand = new algosdk.modelsv2.AvmKeyValue({ - key: Buffer.from(keyHex, 'hex'), - value, - }); - } - } else if (v.scope.scope === 'box') { - const value = state.boxState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - toExpand = new algosdk.modelsv2.AvmKeyValue({ - key: keyBytes, - value, - }); } else { - throw new Error(`key "${v.key}" not found in box state`); + throw new Error( + `Unexpected AppSpecificStateScope scope: ${v.scope}`, + ); } - } else { - throw new Error(`Unexpected AppSpecificStateScope scope: ${v.scope}`); + variables = this.expandAvmKeyValue(v.scope, toExpand, args.filter); } - variables = this.expandAvmKeyValue(v.scope, toExpand, args.filter); } - } - variables = limitArray(variables, args.start, args.count); + variables = limitArray(variables, args.start, args.count); - response.body = { - variables, - }; - this.sendResponse(response); + response.body = { + variables, + }; + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected async evaluateRequest( response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments, ): Promise { - let reply: string | undefined; - let rv: DebugProtocol.Variable | undefined = undefined; + try { + let reply: string | undefined; + let rv: DebugProtocol.Variable | undefined = undefined; - // Note, can use args.context to perform different actions based on where the expression is evaluated + // Note, can use args.context to perform different actions based on where the expression is evaluated - let result: [AvmValueScope, number | string] | undefined = undefined; - try { - result = evaluateNameToScope(args.expression); - } catch (e) { - reply = (e as Error).message; - } + let result: [AvmValueScope, number | string] | undefined = undefined; + try { + result = evaluateNameToScope(args.expression); + } catch (e) { + reply = (e as Error).message; + } - if (result) { - const [scope, key] = result; - if (scope instanceof ProgramStateScope) { - if (typeof args.frameId === 'undefined') { - reply = 'frameId required for program state'; - } else { - const scopeWithFrame = new ProgramStateScope( - args.frameId, - scope.specificState, - ); - const frame = this._runtime.getStackFrame(args.frameId); - if (!frame || !(frame instanceof ProgramStackFrame)) { - reply = `Unexpected frame: ${typeof frame}`; + if (result) { + const [scope, key] = result; + if (scope instanceof ProgramStateScope) { + if (typeof args.frameId === 'undefined') { + reply = 'frameId required for program state'; } else { - if (scope.specificState === 'pc') { - rv = { - name: 'pc', - value: frame.state.pc.toString(), - type: 'uint64', - variablesReference: 0, - evaluateName: 'pc', - }; - } else if (scope.specificState === 'stack') { - let index = key as number; - const stackValues = frame.state.stack; - if (index < 0) { - const adjustedIndex = index + stackValues.length; - if (adjustedIndex < 0) { + const scopeWithFrame = new ProgramStateScope( + args.frameId, + scope.specificState, + ); + const frame = this._runtime.getStackFrame(args.frameId); + if (!frame || !(frame instanceof ProgramStackFrame)) { + reply = `Unexpected frame: ${typeof frame}`; + } else { + if (scope.specificState === 'pc') { + rv = { + name: 'pc', + value: frame.state.pc.toString(), + type: 'uint64', + variablesReference: 0, + evaluateName: 'pc', + }; + } else if (scope.specificState === 'stack') { + let index = key as number; + const stackValues = frame.state.stack; + if (index < 0) { + const adjustedIndex = index + stackValues.length; + if (adjustedIndex < 0) { + reply = `stack[${index}] out of range`; + } else { + index = adjustedIndex; + } + } + if (0 <= index && index < stackValues.length) { + rv = this.convertAvmValue( + scopeWithFrame, + stackValues[index], + index, + ); + } else if (index < 0 && stackValues.length + index >= 0) { + rv = this.convertAvmValue( + scopeWithFrame, + stackValues[stackValues.length + index], + index, + ); + } else { reply = `stack[${index}] out of range`; + } + } else if (scope.specificState === 'scratch') { + const index = key as number; + if (0 <= index && index < 256) { + rv = this.convertAvmValue( + scopeWithFrame, + frame.state.scratch.get(index) || + new algosdk.modelsv2.AvmValue({ type: 2 }), + index, + ); } else { - index = adjustedIndex; + reply = `scratch[${index}] out of range`; } } - if (0 <= index && index < stackValues.length) { - rv = this.convertAvmValue( - scopeWithFrame, - stackValues[index], - index, - ); - } else if (index < 0 && stackValues.length + index >= 0) { - rv = this.convertAvmValue( - scopeWithFrame, - stackValues[stackValues.length + index], - index, - ); - } else { - reply = `stack[${index}] out of range`; - } - } else if (scope.specificState === 'scratch') { - const index = key as number; - if (0 <= index && index < 256) { - rv = this.convertAvmValue( - scopeWithFrame, - frame.state.scratch.get(index) || - new algosdk.modelsv2.AvmValue({ type: 2 }), - index, - ); - } else { - reply = `scratch[${index}] out of range`; - } } } - } - } else if (typeof key === 'string') { - const state = this._runtime.getAppState(scope.appID); - if (scope.property) { - reply = `cannot evaluate property "${scope.property}"`; - } else if (scope.scope === 'global' && key.startsWith('0x')) { - const keyHex = key.slice(2); - const value = state.globalState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - const kv = new algosdk.modelsv2.AvmKeyValue({ - key: keyBytes, - value, - }); - rv = this.convertAvmKeyValue(scope, kv); - } else { - reply = `key "${key}" not found in global state`; - } - } else if (scope.scope === 'local') { - if (typeof scope.account === 'undefined') { - rv = { - name: key, - value: 'local state', - type: 'object', - variablesReference: this._variableHandles.create( - new AppSpecificStateScope({ - scope: 'local', - appID: scope.appID, - account: key, - }), - ), - namedVariables: 1, // TODO - evaluateName: evaluateNameForScope(scope, key), - }; - } else { - const accountState = state.localState.get(scope.account); - if (!accountState) { - reply = `account "${scope.account}" not found in local state`; - } else if (key.startsWith('0x')) { - const keyHex = key.slice(2); - const value = accountState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - const kv = new algosdk.modelsv2.AvmKeyValue({ - key: keyBytes, - value, - }); - rv = this.convertAvmKeyValue(scope, kv); + } else if (typeof key === 'string') { + const state = this._runtime.getAppState(scope.appID); + if (scope.property) { + reply = `cannot evaluate property "${scope.property}"`; + } else if (scope.scope === 'global' && key.startsWith('0x')) { + const keyHex = key.slice(2); + const value = state.globalState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + const kv = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + rv = this.convertAvmKeyValue(scope, kv); + } else { + reply = `key "${key}" not found in global state`; + } + } else if (scope.scope === 'local') { + if (typeof scope.account === 'undefined') { + rv = { + name: key, + value: 'local state', + type: 'object', + variablesReference: this._variableHandles.create( + new AppSpecificStateScope({ + scope: 'local', + appID: scope.appID, + account: key, + }), + ), + namedVariables: 1, // TODO + evaluateName: evaluateNameForScope(scope, key), + }; + } else { + const accountState = state.localState.get(scope.account); + if (!accountState) { + reply = `account "${scope.account}" not found in local state`; + } else if (key.startsWith('0x')) { + const keyHex = key.slice(2); + const value = accountState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + const kv = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + rv = this.convertAvmKeyValue(scope, kv); + } else { + reply = `key "${key}" not found in local state for account "${scope.account}"`; + } } else { - reply = `key "${key}" not found in local state for account "${scope.account}"`; + reply = `cannot evaluate property "${key}"`; } + } + } else if (scope.scope === 'box' && key.startsWith('0x')) { + const keyHex = key.slice(2); + const value = state.boxState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + const kv = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + rv = this.convertAvmKeyValue(scope, kv); } else { - reply = `cannot evaluate property "${key}"`; + reply = `key "${key}" not found in box state`; } } - } else if (scope.scope === 'box' && key.startsWith('0x')) { - const keyHex = key.slice(2); - const value = state.boxState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - const kv = new algosdk.modelsv2.AvmKeyValue({ - key: keyBytes, - value, - }); - rv = this.convertAvmKeyValue(scope, kv); - } else { - reply = `key "${key}" not found in box state`; - } } } - } - if (rv) { - response.body = { - result: rv.value, - type: rv.type, - variablesReference: rv.variablesReference, - presentationHint: rv.presentationHint, - }; - } else { - response.body = { - result: reply || `unknown expression: "${args.expression}"`, - variablesReference: 0, - }; - } + if (rv) { + response.body = { + result: rv.value, + type: rv.type, + variablesReference: rv.variablesReference, + presentationHint: rv.presentationHint, + }; + } else { + response.body = { + result: reply || `unknown expression: "${args.expression}"`, + variablesReference: 0, + }; + } - this.sendResponse(response); + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected sourceRequest( @@ -888,85 +905,117 @@ export class TxnGroupDebugSession extends LoggingDebugSession { args: DebugProtocol.SourceArguments, request?: DebugProtocol.Request, ): void { - const sourceInfo = this._sourceHandles.get(args.sourceReference); - if (typeof sourceInfo !== 'undefined') { - response.body = { - content: sourceInfo.content, - mimeType: sourceInfo.mimeType, - }; - } else { - response.body = { - content: `source not available`, - }; + try { + const sourceInfo = this._sourceHandles.get(args.sourceReference); + if (typeof sourceInfo !== 'undefined') { + response.body = { + content: sourceInfo.content, + mimeType: sourceInfo.mimeType, + }; + } else { + response.body = { + content: `source not available`, + }; + } + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); } - this.sendResponse(response); } protected continueRequest( response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments, ): void { - this.executionResumed(); - this._runtime.continue(false); - this.sendResponse(response); + try { + this.executionResumed(); + this._runtime.continue(false); + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected reverseContinueRequest( response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments, ): void { - this.executionResumed(); - this._runtime.continue(true); - this.sendResponse(response); + try { + this.executionResumed(); + this._runtime.continue(true); + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected nextRequest( response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments, ): void { - this.executionResumed(); - this._runtime.step(false); - this.sendResponse(response); + try { + this.executionResumed(); + this._runtime.step(false); + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected stepBackRequest( response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments, ): void { - this.executionResumed(); - this._runtime.step(true); - this.sendResponse(response); + try { + this.executionResumed(); + this._runtime.step(true); + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected stepInTargetsRequest( response: DebugProtocol.StepInTargetsResponse, args: DebugProtocol.StepInTargetsArguments, ) { - const targets = this._runtime.getStepInTargets(args.frameId); - response.body = { - targets: targets.map((t) => { - return { id: t.id, label: t.label }; - }), - }; - this.sendResponse(response); + try { + const targets = this._runtime.getStepInTargets(args.frameId); + response.body = { + targets: targets.map((t) => { + return { id: t.id, label: t.label }; + }), + }; + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected stepInRequest( response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments, ): void { - this.executionResumed(); - this._runtime.stepIn(args.targetId); - this.sendResponse(response); + try { + this.executionResumed(); + this._runtime.stepIn(args.targetId); + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } protected stepOutRequest( response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments, ): void { - this.executionResumed(); - this._runtime.stepOut(); - this.sendResponse(response); + try { + this.executionResumed(); + this._runtime.stepOut(); + this.sendResponse(response); + } catch (e) { + this.sendErrorResponse(response, GENERIC_ERROR_ID, (e as Error).message); + } } private executionResumed(): void { diff --git a/src/debugAdapter/traceReplayEngine.ts b/src/debugAdapter/traceReplayEngine.ts index 81bdb9d..a6b0565 100644 --- a/src/debugAdapter/traceReplayEngine.ts +++ b/src/debugAdapter/traceReplayEngine.ts @@ -3,25 +3,33 @@ import { AppState } from './appState'; import { ByteArrayMap, TEALDebuggingAssets, - TxnGroupSourceDescriptor, + ProgramSourceDescriptor, + ProgramSourceDescriptorRegistry, } from './utils'; export class TraceReplayEngine { - public debugAssets: TEALDebuggingAssets; + public simulateResponse: algosdk.modelsv2.SimulateResponse | undefined; + public programHashToSource: ByteArrayMap< - TxnGroupSourceDescriptor | undefined + ProgramSourceDescriptor | undefined > = new ByteArrayMap(); - public framePaths: number[][] = []; public initialAppState = new Map(); public currentAppState = new Map(); public stack: TraceReplayStackFrame[] = []; - constructor(debugAssets: TEALDebuggingAssets) { - this.debugAssets = debugAssets; + public reset() { + this.simulateResponse = undefined; + this.programHashToSource.clear(); + this.initialAppState.clear(); + this.currentAppState.clear(); + this.stack = []; + } - const { simulateResponse } = this.debugAssets; + public async loadResources(debugAssets: TEALDebuggingAssets) { + const { simulateResponse, programSourceDescriptorRegistry } = debugAssets; + this.simulateResponse = simulateResponse; for (const initialAppState of simulateResponse.initialStates ?.appInitialStates || []) { @@ -38,19 +46,23 @@ export class TraceReplayEngine { ) { const group = simulateResponse.txnGroups[groupIndex]; - this.framePaths.push([groupIndex]); - for (let txnIndex = 0; txnIndex < group.txnResults.length; txnIndex++) { - this.setupTxnTrace(groupIndex, txnIndex); + this.setupTxnTrace( + simulateResponse, + programSourceDescriptorRegistry, + groupIndex, + txnIndex, + ); } } this.resetCurrentAppState(); - this.setStartingStack(); + this.setStartingStack(simulateResponse); } - private setStartingStack() { - const { simulateResponse } = this.debugAssets; + private setStartingStack( + simulateResponse: algosdk.modelsv2.SimulateResponse, + ) { this.stack = [new TopLevelTransactionGroupsFrame(this, simulateResponse)]; if (simulateResponse.txnGroups.length === 1) { // If only a single group, get rid of the top-level frame @@ -68,29 +80,35 @@ export class TraceReplayEngine { ); } - private setupTxnTrace(groupIndex: number, txnIndex: number) { + private setupTxnTrace( + simulateResponse: algosdk.modelsv2.SimulateResponse, + programSourceDescriptorRegistry: ProgramSourceDescriptorRegistry, + groupIndex: number, + txnIndex: number, + ) { const txnPath = [groupIndex, txnIndex]; - this.framePaths.push(txnPath); - const txn = - this.debugAssets.simulateResponse.txnGroups[groupIndex].txnResults[ - txnIndex - ]; + const txn = simulateResponse.txnGroups[groupIndex].txnResults[txnIndex]; const trace = txn.execTrace; if (!trace) { // Probably not an app call txn return; } if (trace.logicSigTrace) { - this.fetchProgramSourceInfo(trace.logicSigHash!); + this.fetchProgramSourceInfo( + programSourceDescriptorRegistry, + trace.logicSigHash!, + ); } visitAppTrace( txnPath, txn.txnResult, trace, (path, programHash, txnInfo, opcodes) => { - this.framePaths.push(path); - this.fetchProgramSourceInfo(programHash); + this.fetchProgramSourceInfo( + programSourceDescriptorRegistry, + programHash, + ); let appID = txnInfo.applicationIndex || txnInfo.txn.txn.apid; if (typeof appID === 'undefined') { @@ -119,12 +137,15 @@ export class TraceReplayEngine { ); } - private fetchProgramSourceInfo(programHash: Uint8Array) { + private fetchProgramSourceInfo( + programSourceDescriptorRegistry: ProgramSourceDescriptorRegistry, + programHash: Uint8Array, + ) { if (this.programHashToSource.has(programHash)) { return; } const sourceDescriptor = - this.debugAssets.txnGroupDescriptorList.findByHash(programHash); + programSourceDescriptorRegistry.findByHash(programHash); this.programHashToSource.set(programHash, sourceDescriptor); } @@ -150,7 +171,7 @@ export class TraceReplayEngine { length = this.stack.length; this.currentFrame().backward(this.stack); if (this.stack.length === 0) { - this.setStartingStack(); + this.setStartingStack(this.simulateResponse!); return false; } } while (this.stack.length < length); diff --git a/src/debugAdapter/utils.ts b/src/debugAdapter/utils.ts index c9bd616..9d13bed 100644 --- a/src/debugAdapter/utils.ts +++ b/src/debugAdapter/utils.ts @@ -127,10 +127,10 @@ function filePathRelativeTo(base: string, filePath: string): string { return path.join(path.dirname(base), filePath); } -export class TxnGroupSourceDescriptor { +export class ProgramSourceDescriptor { public readonly sourcemapFileLocation: string; public readonly sourcemap: algosdk.SourceMap; - public readonly hash: string; + public readonly hash: Uint8Array; constructor({ sourcemapFileLocation, @@ -139,7 +139,7 @@ export class TxnGroupSourceDescriptor { }: { sourcemapFileLocation: string; sourcemap: algosdk.SourceMap; - hash: string; + hash: Uint8Array; }) { this.sourcemapFileLocation = sourcemapFileLocation; this.sourcemap = sourcemap; @@ -163,7 +163,7 @@ export class TxnGroupSourceDescriptor { fileAccessor: FileAccessor, originFile: string, data: Record, - ): Promise { + ): Promise { const sourcemapFileLocation = filePathRelativeTo( originFile, data['sourcemap-location'], @@ -175,108 +175,79 @@ export class TxnGroupSourceDescriptor { JSON.parse(rawSourcemap.toString('utf-8')), ); - return new TxnGroupSourceDescriptor({ + return new ProgramSourceDescriptor({ sourcemapFileLocation, sourcemap, - hash: data['hash'], + hash: new Uint8Array(Buffer.from(data['hash'], 'base64')), }); } } -export class TxnGroupSourceDescriptorList { - private _txnGroupSources: Array; +export class ProgramSourceDescriptorRegistry { + private registry: ByteArrayMap; constructor({ txnGroupSources, }: { - txnGroupSources: Array; + txnGroupSources: ProgramSourceDescriptor[]; }) { - this._txnGroupSources = txnGroupSources; - } - - public get txnGroupSources(): Array { - return this._txnGroupSources; + this.registry = new ByteArrayMap( + txnGroupSources.map((source) => [source.hash, source]), + ); } - public findByHash( - hash: string | Uint8Array, - ): TxnGroupSourceDescriptor | undefined { - if (typeof hash !== 'string') { - hash = Buffer.from(hash).toString('base64'); - } - for (let i = 0; i < this._txnGroupSources.length; i++) { - if ( - this._txnGroupSources[i].hash && - this._txnGroupSources[i].hash === hash - ) { - return this._txnGroupSources[i]; - } - } - return undefined; + public findByHash(hash: Uint8Array): ProgramSourceDescriptor | undefined { + return this.registry.get(hash); } static async loadFromFile( fileAccessor: FileAccessor, - txnGroupSourcesDescriptionPath: string, - ): Promise { - const rawGroupSourcesDescription = Buffer.from( - await fileAccessor.readFile(txnGroupSourcesDescriptionPath), + programSourcesDescriptionFilePath: string, + ): Promise { + const rawSourcesDescription = Buffer.from( + await fileAccessor.readFile(programSourcesDescriptionFilePath), ); - const jsonGroupSourcesDescription = JSON.parse( - rawGroupSourcesDescription.toString('utf-8'), + const jsonSourcesDescription = JSON.parse( + rawSourcesDescription.toString('utf-8'), ) as Record; - const txnGroupSources = ( - jsonGroupSourcesDescription['txn-group-sources'] as any[] + const programSources = ( + jsonSourcesDescription['txn-group-sources'] as Array> ).map((source) => - TxnGroupSourceDescriptor.fromJSONObj( + ProgramSourceDescriptor.fromJSONObj( fileAccessor, - txnGroupSourcesDescriptionPath, + programSourcesDescriptionFilePath, source, ), ); - return new TxnGroupSourceDescriptorList({ - txnGroupSources: await Promise.all(txnGroupSources), + return new ProgramSourceDescriptorRegistry({ + txnGroupSources: await Promise.all(programSources), }); } } export class TEALDebuggingAssets { - private _simulateResponse: algosdk.modelsv2.SimulateResponse; - private _txnGroupDescriptorList: TxnGroupSourceDescriptorList; - constructor( - simulateResponse: algosdk.modelsv2.SimulateResponse, - txnGroupDescriptorList: TxnGroupSourceDescriptorList, - ) { - this._simulateResponse = simulateResponse; - this._txnGroupDescriptorList = txnGroupDescriptorList; - } - - public get simulateResponse(): algosdk.modelsv2.SimulateResponse { - return this._simulateResponse; - } - - public get txnGroupDescriptorList(): TxnGroupSourceDescriptorList { - return this._txnGroupDescriptorList; - } + public readonly simulateResponse: algosdk.modelsv2.SimulateResponse, + public readonly programSourceDescriptorRegistry: ProgramSourceDescriptorRegistry, + ) {} static async loadFromFiles( fileAccessor: FileAccessor, - simulateResponsePath: string, - txnGroupSourcesDescriptionPath: string, + simulateTraceFilePath: string, + programSourcesDescriptionFilePath: string, ): Promise { - const rawSimulateResponse = Buffer.from( - await fileAccessor.readFile(simulateResponsePath), + const rawSimulateTrace = Buffer.from( + await fileAccessor.readFile(simulateTraceFilePath), ); const simulateResponse = algosdk.modelsv2.SimulateResponse.from_obj_for_encoding( - parseJsonWithBigints(rawSimulateResponse.toString('utf-8')), + parseJsonWithBigints(rawSimulateTrace.toString('utf-8')), ); const txnGroupDescriptorList = - await TxnGroupSourceDescriptorList.loadFromFile( + await ProgramSourceDescriptorRegistry.loadFromFile( fileAccessor, - txnGroupSourcesDescriptionPath, + programSourcesDescriptionFilePath, ); return new TEALDebuggingAssets(simulateResponse, txnGroupDescriptorList); diff --git a/src/descriptorFactory.ts b/src/descriptorFactory.ts new file mode 100644 index 0000000..1a5950b --- /dev/null +++ b/src/descriptorFactory.ts @@ -0,0 +1,71 @@ +'use strict'; + +import * as Net from 'net'; +import * as vscode from 'vscode'; +import { ProviderResult } from 'vscode'; +import { AvmDebugSession } from './debugAdapter/debugRequestHandlers'; +import { workspaceFileAccessor } from './fileAccessor'; + +export interface TEALDebugAdapterDescriptorFactory + extends vscode.DebugAdapterDescriptorFactory { + dispose(); +} + +export class TEALDebugAdapterExecutableFactory + implements TEALDebugAdapterDescriptorFactory +{ + createDebugAdapterDescriptor( + _session: vscode.DebugSession, + executable: vscode.DebugAdapterExecutable | undefined, + ): ProviderResult { + // param "executable" contains the executable optionally specified in the package.json (if any) + + // use the executable specified in the package.json if it exists or determine it based on some other information (e.g. the session) + + // TODO: IMPLEMENT HERE + if (!executable) { + const command = 'absolute path to my DA executable'; + const args = ['some args', 'another arg']; + const options = { + cwd: 'working directory for executable', + env: { envVariable: 'some value' }, + }; + executable = new vscode.DebugAdapterExecutable(command, args, options); + } + + // make VS Code launch the DA executable + return executable; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + dispose() {} +} + +export class TEALDebugAdapterServerDescriptorFactory + implements TEALDebugAdapterDescriptorFactory +{ + private server?: Net.Server; + + async createDebugAdapterDescriptor( + _session: vscode.DebugSession, + _executable: vscode.DebugAdapterExecutable | undefined, + ): Promise { + if (!this.server) { + this.server = Net.createServer((socket) => { + const session = new AvmDebugSession(workspaceFileAccessor); + session.setRunAsServer(true); + session.start(socket as NodeJS.ReadableStream, socket); + }).listen(0); + } + + return new vscode.DebugAdapterServer( + (this.server.address() as Net.AddressInfo).port, + ); + } + + dispose() { + if (this.server) { + this.server.close(); + } + } +} diff --git a/src/extension.ts b/src/extension.ts index ba9cd05..b427e60 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,138 +1,26 @@ 'use strict'; -import * as Net from 'net'; import * as vscode from 'vscode'; -import { ProviderResult } from 'vscode'; -import { TxnGroupDebugSession } from './debugAdapter/debugRequestHandlers'; -import { activateTealDebug, workspaceFileAccessor } from './activateMockDebug'; +import { activateTealDebug } from './activateMockDebug'; import { - TEALDebuggingAssetsDescriptor, - loadTEALDAConfiguration, -} from './utils'; -import { TEALDebuggingAssets } from './debugAdapter/utils'; + TEALDebugAdapterServerDescriptorFactory, + TEALDebugAdapterExecutableFactory, +} from './descriptorFactory'; const runMode: 'external' | 'server' = 'server'; export function activate(context: vscode.ExtensionContext) { - // Load config for debug from launch.json here - // Error abort if failed to load - const config: vscode.DebugConfiguration | undefined = - loadTEALDAConfiguration(); - if (typeof config === 'undefined') { - // TODO: check if this is the best practice of aborting? - console.assert(0); - return; - } - - const debugAssetDescriptor = new TEALDebuggingAssetsDescriptor(config); - const debugAssets = TEALDebuggingAssets.loadFromFiles( - workspaceFileAccessor, - debugAssetDescriptor.simulateResponseFullPath.fsPath, - debugAssetDescriptor.txnGroupSourceDescriptionFullPath.fsPath, - ); - switch (runMode) { case 'server': - activateTealDebug( - context, - new TEALDebugAdapterServerDescriptorFactory(debugAssets), - config, - ); + activateTealDebug(context, new TEALDebugAdapterServerDescriptorFactory()); break; case 'external': default: - activateTealDebug( - context, - new TEALDebugAdapterExecutableFactory(debugAssetDescriptor), - config, - ); + activateTealDebug(context, new TEALDebugAdapterExecutableFactory()); break; } } // eslint-disable-next-line @typescript-eslint/no-empty-function export function deactivate() {} - -export interface TEALDebugAdapterDescriptorFactory - extends vscode.DebugAdapterDescriptorFactory { - dispose(); -} - -class TEALDebugAdapterExecutableFactory - implements TEALDebugAdapterDescriptorFactory -{ - private _debugAssetDescriptor: TEALDebuggingAssetsDescriptor; - - constructor(debugAssetDescriptor: TEALDebuggingAssetsDescriptor) { - this._debugAssetDescriptor = debugAssetDescriptor; - } - - createDebugAdapterDescriptor( - _session: vscode.DebugSession, - executable: vscode.DebugAdapterExecutable | undefined, - ): ProviderResult { - // param "executable" contains the executable optionally specified in the package.json (if any) - - // use the executable specified in the package.json if it exists or determine it based on some other information (e.g. the session) - - // TODO: IMPLEMENT HERE - if (!executable) { - const command = 'absolute path to my DA executable'; - const args = ['some args', 'another arg']; - const options = { - cwd: 'working directory for executable', - env: { envVariable: 'some value' }, - }; - executable = new vscode.DebugAdapterExecutable(command, args, options); - } - - // Just to make the compiler stop complaining about an unused var - this._debugAssetDescriptor; - - // make VS Code launch the DA executable - return executable; - } - - // eslint-disable-next-line @typescript-eslint/no-empty-function - dispose() {} -} - -class TEALDebugAdapterServerDescriptorFactory - implements TEALDebugAdapterDescriptorFactory -{ - private server?: Net.Server; - - private _debugAssets: Promise; - - constructor(debugAssets: Promise) { - this._debugAssets = debugAssets; - } - - async createDebugAdapterDescriptor( - _session: vscode.DebugSession, - _executable: vscode.DebugAdapterExecutable | undefined, - ): Promise { - if (!this.server) { - const debugAssets = await this._debugAssets; - this.server = Net.createServer((socket) => { - const session = new TxnGroupDebugSession( - workspaceFileAccessor, - debugAssets, - ); - session.setRunAsServer(true); - session.start(socket as NodeJS.ReadableStream, socket); - }).listen(0); - } - - return new vscode.DebugAdapterServer( - (this.server.address() as Net.AddressInfo).port, - ); - } - - dispose() { - if (this.server) { - this.server.close(); - } - } -} diff --git a/src/fileAccessor.ts b/src/fileAccessor.ts new file mode 100644 index 0000000..57c33e1 --- /dev/null +++ b/src/fileAccessor.ts @@ -0,0 +1,30 @@ +'use strict'; + +import * as vscode from 'vscode'; +import { FileAccessor } from './debugAdapter/utils'; + +export const workspaceFileAccessor: FileAccessor = { + isWindows: typeof process !== 'undefined' && process.platform === 'win32', + readFile(path: string): Promise { + const uri = pathToUri(path); + return thenableToPromise(vscode.workspace.fs.readFile(uri)); + }, + writeFile(path: string, contents: Uint8Array): Promise { + const uri = pathToUri(path); + return thenableToPromise(vscode.workspace.fs.writeFile(uri, contents)); + }, +}; + +function pathToUri(path: string) { + try { + return vscode.Uri.file(path); + } catch (e) { + return vscode.Uri.parse(path, true); + } +} + +function thenableToPromise(t: Thenable): Promise { + return new Promise((resolve, reject) => { + t.then(resolve, reject); + }); +} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 72b903d..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,165 +0,0 @@ -'use strict'; - -import * as console from 'console'; -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as _ from 'lodash'; - -function vscodeVariables(string, recursive = false) { - console.assert(vscode.workspace.workspaceFolders); - - const workspaces: vscode.WorkspaceFolder[] = ( - vscode.workspace.workspaceFolders - ); - - const workspace = workspaces.length ? workspaces[0] : null; - const activeFile = vscode.window.activeTextEditor?.document; - const absoluteFilePath: string = activeFile?.uri.fsPath; - string = string.replace(/\${workspaceFolder}/g, workspace?.uri.fsPath); - string = string.replace(/\${workspaceFolderBasename}/g, workspace?.name); - string = string.replace(/\${file}/g, absoluteFilePath); - let activeWorkspace = workspace; - let relativeFilePath = absoluteFilePath; - for (const workspace of workspaces) { - if ( - absoluteFilePath.replace(workspace.uri.fsPath, '') !== absoluteFilePath - ) { - activeWorkspace = workspace; - relativeFilePath = absoluteFilePath - .replace(workspace.uri.fsPath, '') - .substring(path.sep.length); - break; - } - } - const parsedPath = path.parse(absoluteFilePath); - string = string.replace( - /\${fileWorkspaceFolder}/g, - activeWorkspace?.uri.fsPath, - ); - string = string.replace(/\${relativeFile}/g, relativeFilePath); - string = string.replace( - /\${relativeFileDirname}/g, - relativeFilePath.substring(0, relativeFilePath.lastIndexOf(path.sep)), - ); - string = string.replace(/\${fileBasename}/g, parsedPath.base); - string = string.replace(/\${fileBasenameNoExtension}/g, parsedPath.name); - string = string.replace(/\${fileExtname}/g, parsedPath.ext); - string = string.replace( - /\${fileDirname}/g, - parsedPath.dir.substring(parsedPath.dir.lastIndexOf(path.sep) + 1), - ); - string = string.replace(/\${cwd}/g, parsedPath.dir); - string = string.replace(/\${pathSeparator}/g, path.sep); - // string = string.replace(/\${lineNumber}/g, vscode.window.activeTextEditor.selection.start.line + 1); - // string = string.replace(/\${selectedText}/g, vscode.window.activeTextEditor.document.getText(new vscode.Range(vscode.window.activeTextEditor.selection.start, vscode.window.activeTextEditor.selection.end))); - string = string.replace(/\${env:(.*?)}/g, function (variable) { - return process.env[variable.match(/\${env:(.*?)}/)[1]] || ''; - }); - string = string.replace(/\${config:(.*?)}/g, function (variable) { - return vscode.workspace - .getConfiguration() - .get(variable.match(/\${config:(.*?)}/)[1], ''); - }); - - if ( - recursive && - string.match( - /\${(workspaceFolder|workspaceFolderBasename|fileWorkspaceFolder|relativeFile|fileBasename|fileBasenameNoExtension|fileExtname|fileDirname|cwd|pathSeparator|lineNumber|selectedText|env:(.*?)|config:(.*?))}/, - ) - ) { - string = vscodeVariables(string, recursive); - } - return string; -} - -/** - * loadTEALDAConfiguration reads from launch.json for configuration, - * then checks for the first debug config unit containing - * { - * 'type': typeString, - * 'request': requestString - * }. - */ -export function loadTEALDAConfiguration({ - typeString = 'teal', - requestString = 'launch', -}: { typeString?: string; requestString?: string } = {}): - | vscode.DebugConfiguration - | undefined { - const launchJson = vscode.workspace.getConfiguration('launch'); - - const configs: vscode.DebugConfiguration[] | undefined = - launchJson.get('configurations'); - if (!configs) { - vscode.window.showErrorMessage( - 'TEAL Debug Plugin Error: load configurations from launch.json error', - ); - return undefined; - } - - console.assert(configs.length && configs.length > 0); - - for (let i = 0; i < configs.length; i++) { - if ( - _.isMatch(configs[i], { - type: typeString, - request: requestString, - }) - ) { - return configs[i]; - } - } - - vscode.window.showErrorMessage( - 'TEAL Debug Plugin Error: launch.json configurations array did not contain relevant TEAL Debug configuration', - ); - return undefined; -} - -export function absPathAgainstWorkspace(pathStr: string): vscode.Uri { - pathStr = vscodeVariables(pathStr); - - if (!path.isAbsolute(pathStr)) { - console.assert(vscode.workspace.workspaceFolders); - const workspaceFolders = ( - vscode.workspace.workspaceFolders - ); - - console.assert(workspaceFolders.length > 0); - const workspaceFolderUri = workspaceFolders[0].uri; - - return vscode.Uri.joinPath(workspaceFolderUri, pathStr); - } - return vscode.Uri.file(pathStr); -} - -export class TEALDebuggingAssetsDescriptor { - private _simulateRespFilesysFullPath: vscode.Uri; - private _txnGroupSourcesDescriptionFullPath: vscode.Uri; - - constructor(config: vscode.DebugConfiguration) { - console.assert(config.simulationTraceFile); - this._simulateRespFilesysFullPath = absPathAgainstWorkspace( - config.simulationTraceFile, - ); - console.assert(config.appSourceDescriptionFile); - this._txnGroupSourcesDescriptionFullPath = absPathAgainstWorkspace( - config.appSourceDescriptionFile, - ); - - vscode.window.showInformationMessage( - this._simulateRespFilesysFullPath.fsPath, - ); - vscode.window.showInformationMessage( - this._txnGroupSourcesDescriptionFullPath.fsPath, - ); - } - - public get simulateResponseFullPath(): vscode.Uri { - return this._simulateRespFilesysFullPath; - } - - public get txnGroupSourceDescriptionFullPath(): vscode.Uri { - return this._txnGroupSourcesDescriptionFullPath; - } -} diff --git a/tests/adapter.test.ts b/tests/adapter.test.ts index f49a14f..b65eea6 100644 --- a/tests/adapter.test.ts +++ b/tests/adapter.test.ts @@ -8,18 +8,17 @@ import { TestFixture, assertVariables, advanceTo, DATA_ROOT } from './testing'; describe('Debug Adapter Tests', () => { const fixture = new TestFixture(); + before(async () => await fixture.init()); + afterEach(async () => { await fixture.reset(); }); - describe('general', () => { - beforeEach(async () => { - await fixture.init( - path.join(DATA_ROOT, 'app-state-changes/local-simulate-response.json'), - path.join(DATA_ROOT, 'app-state-changes/sources.json'), - ); - }); + after(async () => { + await fixture.stop(); + }); + describe('general', () => { describe('basic', () => { it('should produce error for unknown request', async () => { let success: boolean; @@ -63,28 +62,38 @@ describe('Debug Adapter Tests', () => { describe('launch', () => { it('should run program to the end', async () => { - const PROGRAM = path.join( - DATA_ROOT, - 'app-state-changes/local-simulate-response.json', - ); - await Promise.all([ fixture.client.configurationSequence(), - fixture.client.launch({ program: PROGRAM }), + fixture.client.launch({ + simulateTraceFile: path.join( + DATA_ROOT, + 'app-state-changes/local-simulate-response.json', + ), + programSourcesDescriptionFile: path.join( + DATA_ROOT, + 'app-state-changes/sources.json', + ), + }), fixture.client.waitForEvent('terminated'), ]); }); it('should stop on entry', async () => { - const PROGRAM = path.join( - DATA_ROOT, - 'app-state-changes/local-simulate-response.json', - ); const ENTRY_LINE = 2; await Promise.all([ fixture.client.configurationSequence(), - fixture.client.launch({ program: PROGRAM, stopOnEntry: true }), + fixture.client.launch({ + simulateTraceFile: path.join( + DATA_ROOT, + 'app-state-changes/local-simulate-response.json', + ), + programSourcesDescriptionFile: path.join( + DATA_ROOT, + 'app-state-changes/sources.json', + ), + stopOnEntry: true, + }), fixture.client.assertStoppedLocation('entry', { line: ENTRY_LINE }), ]); }); @@ -99,7 +108,16 @@ describe('Debug Adapter Tests', () => { const BREAKPOINT_LINE = 2; await fixture.client.hitBreakpoint( - { program: PROGRAM }, + { + simulateTraceFile: path.join( + DATA_ROOT, + 'app-state-changes/local-simulate-response.json', + ), + programSourcesDescriptionFile: path.join( + DATA_ROOT, + 'app-state-changes/sources.json', + ), + }, { path: PROGRAM, line: BREAKPOINT_LINE }, ); }); @@ -337,19 +355,23 @@ describe('Debug Adapter Tests', () => { describe('Step in', () => { it('should pause at the correct locations', async () => { - const simulateTracePath = path.join( + const simulateTraceFile = path.join( DATA_ROOT, 'stepping-test/simulate-response.json', ); - await fixture.init( - simulateTracePath, - path.join(DATA_ROOT, 'stepping-test/sources.json'), + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'stepping-test/sources.json', ); const { client } = fixture; await Promise.all([ client.configurationSequence(), - client.launch({ program: simulateTracePath, stopOnEntry: true }), + client.launch({ + simulateTraceFile, + programSourcesDescriptionFile, + stopOnEntry: true, + }), client.assertStoppedLocation('entry', {}), ]); @@ -538,19 +560,24 @@ describe('Debug Adapter Tests', () => { describe('Step over', () => { it('should pause at the correct locations in a transaction group', async () => { - const simulateTracePath = path.join( + const simulateTraceFile = path.join( DATA_ROOT, 'stepping-test/simulate-response.json', ); - await fixture.init( - simulateTracePath, - path.join(DATA_ROOT, 'stepping-test/sources.json'), + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'stepping-test/sources.json', ); + const { client } = fixture; await Promise.all([ client.configurationSequence(), - client.launch({ program: simulateTracePath, stopOnEntry: true }), + client.launch({ + simulateTraceFile, + programSourcesDescriptionFile, + stopOnEntry: true, + }), client.assertStoppedLocation('entry', {}), ]); @@ -584,13 +611,13 @@ describe('Debug Adapter Tests', () => { }); it('should pause at the correct locations in app execution', async () => { - const simulateTracePath = path.join( + const simulateTraceFile = path.join( DATA_ROOT, 'slot-machine/simulate-response.json', ); - await fixture.init( - simulateTracePath, - path.join(DATA_ROOT, 'slot-machine/sources.json'), + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'slot-machine/sources.json', ); const { client } = fixture; @@ -600,7 +627,7 @@ describe('Debug Adapter Tests', () => { ); await client.hitBreakpoint( - { program: simulateTracePath }, + { simulateTraceFile, programSourcesDescriptionFile }, { path: programPath, line: 2 }, ); @@ -648,13 +675,13 @@ describe('Debug Adapter Tests', () => { describe('Step out', () => { it('should pause at the correct locations', async () => { - const simulateTracePath = path.join( + const simulateTraceFile = path.join( DATA_ROOT, 'slot-machine/simulate-response.json', ); - await fixture.init( - simulateTracePath, - path.join(DATA_ROOT, 'slot-machine/sources.json'), + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'slot-machine/sources.json', ); const { client } = fixture; @@ -672,7 +699,7 @@ describe('Debug Adapter Tests', () => { ); await client.hitBreakpoint( - { program: simulateTracePath }, + { simulateTraceFile, programSourcesDescriptionFile }, { path: fakeRandomPath, line: 13 }, ); @@ -836,19 +863,23 @@ describe('Debug Adapter Tests', () => { describe('Step back', () => { it('should pause at the correct locations in a transaction group', async () => { - const simulateTracePath = path.join( + const simulateTraceFile = path.join( DATA_ROOT, 'stepping-test/simulate-response.json', ); - await fixture.init( - simulateTracePath, - path.join(DATA_ROOT, 'stepping-test/sources.json'), + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'stepping-test/sources.json', ); const { client } = fixture; await Promise.all([ client.configurationSequence(), - client.launch({ program: simulateTracePath, stopOnEntry: true }), + client.launch({ + simulateTraceFile, + programSourcesDescriptionFile, + stopOnEntry: true, + }), client.assertStoppedLocation('entry', {}), ]); @@ -901,13 +932,13 @@ describe('Debug Adapter Tests', () => { }); it('should pause at the correct locations in app execution', async () => { - const simulateTracePath = path.join( + const simulateTraceFile = path.join( DATA_ROOT, 'slot-machine/simulate-response.json', ); - await fixture.init( - simulateTracePath, - path.join(DATA_ROOT, 'slot-machine/sources.json'), + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'slot-machine/sources.json', ); const { client } = fixture; @@ -917,7 +948,7 @@ describe('Debug Adapter Tests', () => { const startLocation = expectedLocations[0]; await client.hitBreakpoint( - { program: simulateTracePath }, + { simulateTraceFile, programSourcesDescriptionFile }, { path: startLocation.program!, line: startLocation.line, @@ -969,20 +1000,20 @@ describe('Debug Adapter Tests', () => { describe('Stack and scratch changes', () => { context('LogicSig', () => { it('should return variables correctly', async () => { - const simulateResponse = path.join( + const simulateTraceFile = path.join( DATA_ROOT, 'stepping-test/simulate-response.json', ); - await fixture.init( - simulateResponse, - path.join(DATA_ROOT, 'stepping-test/sources.json'), + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'stepping-test/sources.json', ); const { client } = fixture; const PROGRAM = path.join(DATA_ROOT, 'stepping-test/lsig.teal'); await client.hitBreakpoint( - { program: simulateResponse }, + { simulateTraceFile, programSourcesDescriptionFile }, { path: PROGRAM, line: 3 }, ); @@ -1011,19 +1042,23 @@ describe('Debug Adapter Tests', () => { }); context('App', () => { it('should return variables correctly', async () => { - await fixture.init( - path.join(DATA_ROOT, 'stack-scratch/simulate-response.json'), - path.join(DATA_ROOT, 'stack-scratch/sources.json'), + const simulateTraceFile = path.join( + DATA_ROOT, + 'stack-scratch/simulate-response.json', + ); + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'stack-scratch/sources.json', ); - const { client } = fixture; + const PROGRAM = path.join( DATA_ROOT, 'stack-scratch/stack-scratch.teal', ); await client.hitBreakpoint( - { program: PROGRAM }, + { simulateTraceFile, programSourcesDescriptionFile }, { path: PROGRAM, line: 3 }, ); @@ -1122,9 +1157,13 @@ describe('Debug Adapter Tests', () => { describe('Global state changes', () => { it('should return variables correctly', async () => { - await fixture.init( - path.join(DATA_ROOT, 'app-state-changes/global-simulate-response.json'), - path.join(DATA_ROOT, 'app-state-changes/sources.json'), + const simulateTraceFile = path.join( + DATA_ROOT, + 'app-state-changes/global-simulate-response.json', + ); + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'app-state-changes/sources.json', ); const { client } = fixture; @@ -1134,7 +1173,7 @@ describe('Debug Adapter Tests', () => { ); await client.hitBreakpoint( - { program: PROGRAM }, + { simulateTraceFile, programSourcesDescriptionFile }, { path: PROGRAM, line: 3 }, ); @@ -1215,9 +1254,13 @@ describe('Debug Adapter Tests', () => { describe('Local state changes', () => { it('should return variables correctly', async () => { - await fixture.init( - path.join(DATA_ROOT, 'app-state-changes/local-simulate-response.json'), - path.join(DATA_ROOT, 'app-state-changes/sources.json'), + const simulateTraceFile = path.join( + DATA_ROOT, + 'app-state-changes/local-simulate-response.json', + ); + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'app-state-changes/sources.json', ); const { client } = fixture; @@ -1227,7 +1270,7 @@ describe('Debug Adapter Tests', () => { ); await client.hitBreakpoint( - { program: PROGRAM }, + { simulateTraceFile, programSourcesDescriptionFile }, { path: PROGRAM, line: 3 }, ); @@ -1344,9 +1387,13 @@ describe('Debug Adapter Tests', () => { describe('Box state changes', () => { it('should return variables correctly', async () => { - await fixture.init( - path.join(DATA_ROOT, 'app-state-changes/box-simulate-response.json'), - path.join(DATA_ROOT, 'app-state-changes/sources.json'), + const simulateTraceFile = path.join( + DATA_ROOT, + 'app-state-changes/box-simulate-response.json', + ); + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'app-state-changes/sources.json', ); const { client } = fixture; @@ -1356,7 +1403,7 @@ describe('Debug Adapter Tests', () => { ); await client.hitBreakpoint( - { program: PROGRAM }, + { simulateTraceFile, programSourcesDescriptionFile }, { path: PROGRAM, line: 3 }, ); @@ -1469,13 +1516,24 @@ describe('Debug Adapter Tests', () => { ]; it('should return correct breakpoint locations', async () => { - await fixture.init( - path.join(DATA_ROOT, 'sourcemap-test/simulate-response.json'), - path.join(DATA_ROOT, 'sourcemap-test/sources.json'), - ); - const { client } = fixture; + await Promise.all([ + client.configurationSequence(), + client.launch({ + simulateTraceFile: path.join( + DATA_ROOT, + 'sourcemap-test/simulate-response.json', + ), + programSourcesDescriptionFile: path.join( + DATA_ROOT, + 'sourcemap-test/sources.json', + ), + stopOnEntry: true, + }), + client.assertStoppedLocation('entry', {}), + ]); + for (const source of testSources) { const response = await client.breakpointLocationsRequest({ source: { @@ -1503,20 +1561,19 @@ describe('Debug Adapter Tests', () => { }); it('should correctly set and stop at valid breakpoints', async () => { - await fixture.init( - path.join(DATA_ROOT, 'sourcemap-test/simulate-response.json'), - path.join(DATA_ROOT, 'sourcemap-test/sources.json'), - ); - const { client } = fixture; await Promise.all([ client.configurationSequence(), client.launch({ - program: path.join( + simulateTraceFile: path.join( DATA_ROOT, 'sourcemap-test/simulate-response.json', ), + programSourcesDescriptionFile: path.join( + DATA_ROOT, + 'sourcemap-test/sources.json', + ), stopOnEntry: true, }), client.assertStoppedLocation('entry', {}), @@ -1591,20 +1648,19 @@ describe('Debug Adapter Tests', () => { }); it('should correctly handle invalid breakpoints and not stop at them', async () => { - await fixture.init( - path.join(DATA_ROOT, 'sourcemap-test/simulate-response.json'), - path.join(DATA_ROOT, 'sourcemap-test/sources.json'), - ); - const { client } = fixture; await Promise.all([ client.configurationSequence(), client.launch({ - program: path.join( + simulateTraceFile: path.join( DATA_ROOT, 'sourcemap-test/simulate-response.json', ), + programSourcesDescriptionFile: path.join( + DATA_ROOT, + 'sourcemap-test/sources.json', + ), stopOnEntry: true, }), client.assertStoppedLocation('entry', {}), diff --git a/tests/client.ts b/tests/client.ts index cb39856..899d5d4 100644 --- a/tests/client.ts +++ b/tests/client.ts @@ -2,6 +2,11 @@ import * as assert from 'assert'; import { SpawnOptions } from 'child_process'; import { DebugClient as DebugClientBase } from '@vscode/debugadapter-testsupport'; import { DebugProtocol } from '@vscode/debugprotocol'; +import { ILaunchRequestArguments } from '../src/debugAdapter/debugRequestHandlers'; +import { + ILocation, + IPartialLocation, +} from '@vscode/debugadapter-testsupport/lib/debugClient'; export class DebugClient extends DebugClientBase { private lastStoppedEvent: DebugProtocol.StoppedEvent | undefined; @@ -29,6 +34,20 @@ export class DebugClient extends DebugClientBase { }); } + launch( + launchArgs: ILaunchRequestArguments, + ): Promise { + return super.launch(launchArgs); + } + + disconnectRequest( + args?: DebugProtocol.DisconnectArguments | undefined, + ): Promise { + // Clear lastStoppedEvent + this.lastStoppedEvent = undefined; + return super.disconnectRequest(args); + } + continueRequest( args: DebugProtocol.ContinueArguments, ): Promise { @@ -87,10 +106,8 @@ export class DebugClient extends DebugClientBase { if (typeof this.lastStoppedEvent !== 'undefined') { return Promise.resolve(this.lastStoppedEvent); } - const event = (await this.waitForEvent( - 'stopped', - )) as DebugProtocol.StoppedEvent; - return event; + const event = await this.waitForEvent('stopped'); + return event as DebugProtocol.StoppedEvent; } async assertStoppedLocation( @@ -141,4 +158,77 @@ export class DebugClient extends DebugClientBase { args, ) as Promise; } + + async hitBreakpoint( + launchArgs: ILaunchRequestArguments, + location: ILocation, + expectedStopLocation?: IPartialLocation | undefined, + expectedBPLocation?: IPartialLocation | undefined, + ): Promise { + if (launchArgs.stopOnEntry) { + throw new Error("Can't hit breakpoint when stopOnEntry is true"); + } + // Can't call into super.hitBreakpoint because there is a race between setting the breakpoint + // and sending the launch request. Any breakpoints set before launch will be marked 'unverified', + // which will cause super.hitBreakpoint to fail. + await Promise.all([ + this.configurationSequence(), + this.launch({ + ...launchArgs, + stopOnEntry: true, + }), + this.assertStoppedLocation('entry', {}), + ]); + + const setBreakpointsResponse = await this.setBreakpointsRequest({ + breakpoints: [{ line: location.line, column: location.column }], + source: { path: location.path }, + }); + + const bp = setBreakpointsResponse.body.breakpoints[0]; + const verified = + typeof location.verified === 'boolean' ? location.verified : true; + assert.strictEqual( + bp.verified, + verified, + 'breakpoint verification mismatch: verified', + ); + const actualLocation = { + column: bp.column, + line: bp.line, + path: bp.source && bp.source.path, + }; + // assertPartialLocationsEqual(actualLocation, expectedBPLocation || location); + const expectedLocation = expectedBPLocation || location; + if (actualLocation.path) { + this.assertPath( + actualLocation.path, + expectedLocation.path!, + 'breakpoint verification mismatch: path', + ); + } + if (typeof actualLocation.line === 'number') { + assert.strictEqual( + actualLocation.line, + expectedLocation.line, + 'breakpoint verification mismatch: line', + ); + } + if ( + typeof expectedLocation.column === 'number' && + typeof actualLocation.column === 'number' + ) { + assert.strictEqual( + actualLocation.column, + expectedLocation.column, + 'breakpoint verification mismatch: column', + ); + } + + await this.continueRequest({ threadId: 1 }); + await this.assertStoppedLocation( + 'breakpoint', + expectedStopLocation || location, + ); + } } diff --git a/tests/testing.ts b/tests/testing.ts index 0cf47d4..c7dc773 100644 --- a/tests/testing.ts +++ b/tests/testing.ts @@ -44,34 +44,27 @@ export class TestFixture { return this._server; } - public async init( - simulateResponsePath: string, - txnGroupSourcesDescriptionPath: string, - ) { - const debugAssets: TEALDebuggingAssets = - await TEALDebuggingAssets.loadFromFiles( - testFileAccessor, - simulateResponsePath, - txnGroupSourcesDescriptionPath, - ); - this._server = new BasicServer(testFileAccessor, debugAssets); + public async init() { + this._server = new BasicServer(testFileAccessor); this._client = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal'); await this._client.start(this._server.port()); - // this._client = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal', { - // env: { - // ...process.env, - // /* eslint-disable @typescript-eslint/naming-convention */ - // ALGORAND_SIMULATION_RESPONSE_PATH: simulateResponsePath, - // ALGORAND_TXN_GROUP_SOURCES_DESCRIPTION_PATH: txnGroupSourcesDescriptionPath, - // /* eslint-enable @typescript-eslint/naming-convention */ - // } - // }, true); + // this._client = new DebugClient( + // 'node', + // DEBUG_CLIENT_PATH, + // 'teal', + // undefined, + // true, + // ); // await this._client.start(); } public async reset() { + await this.client.disconnectRequest(); + } + + public async stop() { await this.client.stop(); this.server.dispose(); this._client = undefined; From 9d8c210a6548b053ad1f0a1e6ac17d2b60080863 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Fri, 3 Nov 2023 16:50:49 -0400 Subject: [PATCH 2/2] Test for various errors during launch --- src/debugAdapter/utils.ts | 87 +++++++++++++++++++++++------- tests/adapter.test.ts | 108 ++++++++++++++++++++++++++++++++++++++ tests/testing.ts | 12 ++--- 3 files changed, 181 insertions(+), 26 deletions(-) diff --git a/src/debugAdapter/utils.ts b/src/debugAdapter/utils.ts index 9d13bed..d4e50f3 100644 --- a/src/debugAdapter/utils.ts +++ b/src/debugAdapter/utils.ts @@ -127,6 +127,15 @@ function filePathRelativeTo(base: string, filePath: string): string { return path.join(path.dirname(base), filePath); } +interface ProgramSourceEntryFile { + 'txn-group-sources': ProgramSourceEntry[]; +} + +interface ProgramSourceEntry { + hash: string; + 'sourcemap-location': string; +} + export class ProgramSourceDescriptor { public readonly sourcemapFileLocation: string; public readonly sourcemap: algosdk.SourceMap; @@ -162,14 +171,17 @@ export class ProgramSourceDescriptor { static async fromJSONObj( fileAccessor: FileAccessor, originFile: string, - data: Record, + data: ProgramSourceEntry, ): Promise { const sourcemapFileLocation = filePathRelativeTo( originFile, data['sourcemap-location'], ); const rawSourcemap = Buffer.from( - await fileAccessor.readFile(sourcemapFileLocation), + await prefixPotentialError( + fileAccessor.readFile(sourcemapFileLocation), + 'Could not read source map file', + ), ); const sourcemap = new algosdk.SourceMap( JSON.parse(rawSourcemap.toString('utf-8')), @@ -178,7 +190,7 @@ export class ProgramSourceDescriptor { return new ProgramSourceDescriptor({ sourcemapFileLocation, sourcemap, - hash: new Uint8Array(Buffer.from(data['hash'], 'base64')), + hash: new Uint8Array(Buffer.from(data.hash, 'base64')), }); } } @@ -205,20 +217,40 @@ export class ProgramSourceDescriptorRegistry { programSourcesDescriptionFilePath: string, ): Promise { const rawSourcesDescription = Buffer.from( - await fileAccessor.readFile(programSourcesDescriptionFilePath), - ); - const jsonSourcesDescription = JSON.parse( - rawSourcesDescription.toString('utf-8'), - ) as Record; - const programSources = ( - jsonSourcesDescription['txn-group-sources'] as Array> - ).map((source) => - ProgramSourceDescriptor.fromJSONObj( - fileAccessor, - programSourcesDescriptionFilePath, - source, + await prefixPotentialError( + fileAccessor.readFile(programSourcesDescriptionFilePath), + 'Could not read program sources description file', ), ); + let jsonSourcesDescription: ProgramSourceEntryFile; + try { + jsonSourcesDescription = JSON.parse( + rawSourcesDescription.toString('utf-8'), + ) as ProgramSourceEntryFile; + if ( + !Array.isArray(jsonSourcesDescription['txn-group-sources']) || + !jsonSourcesDescription['txn-group-sources'].every( + (entry) => + typeof entry.hash === 'string' && + typeof entry['sourcemap-location'] === 'string', + ) + ) { + throw new Error('Invalid program sources description file'); + } + } catch (e) { + const err = e as Error; + throw new Error( + `Could not parse program sources description file from '${programSourcesDescriptionFilePath}': ${err.message}`, + ); + } + const programSources = jsonSourcesDescription['txn-group-sources'].map( + (source) => + ProgramSourceDescriptor.fromJSONObj( + fileAccessor, + programSourcesDescriptionFilePath, + source, + ), + ); return new ProgramSourceDescriptorRegistry({ txnGroupSources: await Promise.all(programSources), }); @@ -237,12 +269,23 @@ export class TEALDebuggingAssets { programSourcesDescriptionFilePath: string, ): Promise { const rawSimulateTrace = Buffer.from( - await fileAccessor.readFile(simulateTraceFilePath), + await prefixPotentialError( + fileAccessor.readFile(simulateTraceFilePath), + 'Could not read simulate trace file', + ), ); - const simulateResponse = - algosdk.modelsv2.SimulateResponse.from_obj_for_encoding( - parseJsonWithBigints(rawSimulateTrace.toString('utf-8')), + let simulateResponse: algosdk.modelsv2.SimulateResponse; + try { + simulateResponse = + algosdk.modelsv2.SimulateResponse.from_obj_for_encoding( + parseJsonWithBigints(rawSimulateTrace.toString('utf-8')), + ); + } catch (e) { + const err = e as Error; + throw new Error( + `Could not parse simulate trace file from '${simulateTraceFilePath}': ${err.message}`, ); + } const txnGroupDescriptorList = await ProgramSourceDescriptorRegistry.loadFromFile( @@ -253,3 +296,9 @@ export class TEALDebuggingAssets { return new TEALDebuggingAssets(simulateResponse, txnGroupDescriptorList); } } + +function prefixPotentialError(task: Promise, prefix: string): Promise { + return task.catch((error) => { + throw new Error(`${prefix}: ${error.message}`); + }); +} diff --git a/tests/adapter.test.ts b/tests/adapter.test.ts index b65eea6..6415e87 100644 --- a/tests/adapter.test.ts +++ b/tests/adapter.test.ts @@ -61,6 +61,114 @@ describe('Debug Adapter Tests', () => { }); describe('launch', () => { + it('should return error when simulate trace file does not exist', async () => { + let caughtError: Error | undefined; + try { + await fixture.client.launch({ + simulateTraceFile: path.join( + DATA_ROOT, + 'does-not-exist/simulate-response.json', + ), + programSourcesDescriptionFile: path.join( + DATA_ROOT, + 'app-state-changes/sources.json', + ), + }); + } catch (e) { + caughtError = e as Error; + } + if (!caughtError) { + assert.fail('Expected error'); + } + assert.ok( + caughtError.message.includes('Could not read simulate trace file'), + caughtError.message, + ); + }); + + it('should return error when program sources description files does not exist', async () => { + let caughtError: Error | undefined; + try { + await fixture.client.launch({ + simulateTraceFile: path.join( + DATA_ROOT, + 'app-state-changes/local-simulate-response.json', + ), + programSourcesDescriptionFile: path.join( + DATA_ROOT, + 'does-not-exist/sources.json', + ), + }); + } catch (e) { + caughtError = e as Error; + } + if (!caughtError) { + assert.fail('Expected error'); + } + assert.ok( + caughtError.message.includes( + 'Could not read program sources description file', + ), + caughtError.message, + ); + }); + + it('should return error when simulate trace file is invalid', async () => { + const simulateTraceFile = path.join( + DATA_ROOT, + 'slot-machine/sources.json', // not a valid simulate trace file + ); + let caughtError: Error | undefined; + try { + await fixture.client.launch({ + simulateTraceFile, + programSourcesDescriptionFile: path.join( + DATA_ROOT, + 'app-state-changes/sources.json', + ), + }); + } catch (e) { + caughtError = e as Error; + } + if (!caughtError) { + assert.fail('Expected error'); + } + assert.ok( + caughtError.message.includes( + `Could not parse simulate trace file from '${simulateTraceFile}'`, + ), + caughtError.message, + ); + }); + + it('should return error when program sources description files is invalid', async () => { + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'slot-machine/simulate-response.json', // not a valid program sources description file + ); + let caughtError: Error | undefined; + try { + await fixture.client.launch({ + simulateTraceFile: path.join( + DATA_ROOT, + 'app-state-changes/local-simulate-response.json', + ), + programSourcesDescriptionFile, + }); + } catch (e) { + caughtError = e as Error; + } + if (!caughtError) { + assert.fail('Expected error'); + } + assert.ok( + caughtError.message.includes( + `Could not parse program sources description file from '${programSourcesDescriptionFile}': Invalid program sources description file`, + ), + caughtError.message, + ); + }); + it('should run program to the end', async () => { await Promise.all([ fixture.client.configurationSequence(), diff --git a/tests/testing.ts b/tests/testing.ts index c7dc773..514896b 100644 --- a/tests/testing.ts +++ b/tests/testing.ts @@ -3,16 +3,12 @@ import * as path from 'path'; import * as fs from 'fs/promises'; import { DebugClient } from './client'; import { BasicServer } from '../src/debugAdapter/basicServer'; -import { - FileAccessor, - ByteArrayMap, - TEALDebuggingAssets, -} from '../src/debugAdapter/utils'; +import { FileAccessor, ByteArrayMap } from '../src/debugAdapter/utils'; export const PROJECT_ROOT = path.join(__dirname, '../'); -export const DEBUG_CLIENT_PATH = path.join( +const DEBUG_CLIENT_PATH = path.join( PROJECT_ROOT, - 'out/debugAdapter/debugAdapter.js', + 'out/src/debugAdapter/debugAdapter.js', ); export const DATA_ROOT = path.join(PROJECT_ROOT, 'sampleWorkspace/'); @@ -50,6 +46,8 @@ export class TestFixture { this._client = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal'); await this._client.start(this._server.port()); + // If you want to invoke the debug adapter separately in a child process and + // communicate through stdin/stdout, use this instead: // this._client = new DebugClient( // 'node', // DEBUG_CLIENT_PATH,