#!/usr/bin/env node const assert = require("assert"); const fs = require("fs"); const path = require("path"); const { DapClient } = require("./dap-client"); (async () => { const repo = path.resolve(__dirname, ".."); const project = path.join(repo, "examples/launch-build-demo"); const sourcePath = fs.realpathSync(path.join(project, "src/build.rs")); const sourceLines = fs.readFileSync(sourcePath, "utf8").split(/\r?\n/); const buildMainLine = sourceLines.findIndex((line) => line.includes("pub async fn build_main()")) + 1; assert(buildMainLine > 0, "flagship source must contain build_main"); const client = new DapClient(); try { const initialize = client.send("initialize", { adapterID: "clusterflux", linesStartAt1: true, columnsStartAt1: true, }); await client.response(initialize, "initialize"); const launch = client.send("launch", { entry: "build", project, runtimeBackend: "local-services", }); await client.response(launch, "launch"); await client.waitFor( (message) => message.type === "event" && message.event === "initialized" ); const breakpoints = client.send("setBreakpoints", { source: { path: sourcePath }, breakpoints: [{ line: buildMainLine }], }); const breakpointResponse = await client.response( breakpoints, "setBreakpoints" ); assert.strictEqual(breakpointResponse.body.breakpoints.length, 1); assert.strictEqual(breakpointResponse.body.breakpoints[0].verified, true); const configurationDone = client.send("configurationDone"); await client.response(configurationDone, "configurationDone"); const stopped = await client.waitFor( (message) => message.type === "event" && message.event === "stopped" && message.body.reason === "breakpoint" ); assert.strictEqual(stopped.body.allThreadsStopped, true); assert.match(stopped.body.description, /confirmed by every active participant/i); const threadsRequest = client.send("threads"); const threads = (await client.response(threadsRequest, "threads")).body .threads; const mainThread = threads.find((thread) => thread.name.includes("build coordinator main") ); assert(mainThread, "the running Wasm entrypoint must be the DAP coordinator-main thread"); assert.strictEqual(stopped.body.threadId, mainThread.id); const stackRequest = client.send("stackTrace", { threadId: mainThread.id, startFrame: 0, levels: 1, }); const stack = (await client.response(stackRequest, "stackTrace")).body .stackFrames; assert.strictEqual(stack.length, 1); assert.strictEqual(stack[0].line, buildMainLine); assert.strictEqual(stack[0].source.path, sourcePath); assert.match(stack[0].name, /build_main::wasm/); assert.doesNotMatch(stack[0].name, /podman|cmd\.exe|powershell|pid|native child/i); const sourceRequest = client.send("source", { source: stack[0].source }); const source = (await client.response(sourceRequest, "source")).body; assert.match(source.content, /build_main/); assert.match(source.mimeType, /rust/); const scopesRequest = client.send("scopes", { frameId: stack[0].id }); const scopes = (await client.response(scopesRequest, "scopes")).body.scopes; const localsScope = scopes.find((scope) => scope.name === "Source Locals"); const wasmScope = scopes.find((scope) => scope.name === "Wasm Frame Locals"); const argsScope = scopes.find( (scope) => scope.name === "Task Args and Handles" ); const runtimeScope = scopes.find( (scope) => scope.name === "Clusterflux Runtime" ); assert(localsScope && wasmScope && argsScope && runtimeScope); const localsRequest = client.send("variables", { variablesReference: localsScope.variablesReference, }); const locals = (await client.response(localsRequest, "variables")).body .variables; assert( locals.some( (variable) => variable.name === "unavailable-local-diagnostic" && String(variable.value).includes("cannot be inspected") ) ); const wasmRequest = client.send("variables", { variablesReference: wasmScope.variablesReference, }); const wasmLocals = (await client.response(wasmRequest, "variables")).body .variables; assert.deepStrictEqual( wasmLocals.map((variable) => variable.name), ["wasm-local-diagnostic"] ); assert.match(wasmLocals[0].value, /did not report inspectable Wasm frame locals/); const argsRequest = client.send("variables", { variablesReference: argsScope.variablesReference, }); const args = (await client.response(argsRequest, "variables")).body.variables; assert.deepStrictEqual( args.map((variable) => variable.name), ["runtime-boundary-diagnostic"] ); assert.match(args[0].value, /reported no task arguments or handles/); const runtimeRequest = client.send("variables", { variablesReference: runtimeScope.variablesReference, }); const runtime = (await client.response(runtimeRequest, "variables")).body .variables; const value = (name) => runtime.find((variable) => variable.name === name)?.value; assert.strictEqual(value("runtime_backend"), "LocalServices"); assert.strictEqual(value("state"), "Frozen"); assert.strictEqual(value("debug_epoch"), 1); assert.strictEqual(value("coordinator_task_events"), 0); assert.match( String(value("command_status")), /frozen through local services at executing Wasm probe/ ); const step = client.send("next", { threadId: mainThread.id }); const stepFailure = await client.failure(step, "next"); assert.match(stepFailure.message, /source stepping is not yet available/i); assert.match(stepFailure.message, /synthetic step/i); const restart = client.send("restartFrame", { frameId: stack[0].id }); const restartFailure = await client.failure(restart, "restartFrame"); assert.match(restartFailure.message, /checkpoint boundary|still active/i); const incompatibleRestart = client.send("restartFrame", { frameId: stack[0].id, sourceCompatibility: "incompatible", }); const incompatibleFailure = await client.failure( incompatibleRestart, "restartFrame" ); assert.match(incompatibleFailure.message, /incompatible source edit/i); assert.match(incompatibleFailure.message, /whole virtual-process restart/i); await client.close(); } catch (error) { if (client.child.exitCode === null) client.child.kill("SIGKILL"); throw error; } const failMainLine = sourceLines.findIndex((line) => line.includes("pub async fn fail_main()")) + 1; assert(failMainLine > 0, "flagship source must contain fail_main"); const taskTrapLine = sourceLines.findIndex((line) => line.includes("fn task_trap(")) + 1; assert(taskTrapLine > 0, "flagship source must contain task_trap"); const restartClient = new DapClient(); try { const initialize = restartClient.send("initialize", { adapterID: "clusterflux", linesStartAt1: true, columnsStartAt1: true, }); await restartClient.response(initialize, "initialize"); const launch = restartClient.send("launch", { entry: "fail", project, runtimeBackend: "local-services", }); await restartClient.response(launch, "launch"); await restartClient.waitFor( (message) => message.type === "event" && message.event === "initialized" ); const breakpoints = restartClient.send("setBreakpoints", { source: { path: sourcePath }, breakpoints: [{ line: failMainLine }, { line: taskTrapLine }], }); const breakpointResponse = await restartClient.response( breakpoints, "setBreakpoints" ); assert.deepStrictEqual( breakpointResponse.body.breakpoints.map((breakpoint) => breakpoint.verified), [true, true] ); const configurationDone = restartClient.send("configurationDone"); await restartClient.response(configurationDone, "configurationDone"); const initialStop = await restartClient.waitFor( (message) => message.type === "event" && message.event === "stopped" && message.body.reason === "breakpoint" ); assert.strictEqual(initialStop.body.allThreadsStopped, true); const threadsRequest = restartClient.send("threads"); const threads = ( await restartClient.response(threadsRequest, "threads") ).body.threads; const failThread = threads.find( (thread) => thread.id === initialStop.body.threadId ); assert(failThread, "failed entrypoint must remain a virtual task thread"); const stackRequest = restartClient.send("stackTrace", { threadId: failThread.id, startFrame: 0, levels: 1, }); const failedStack = ( await restartClient.response(stackRequest, "stackTrace") ).body.stackFrames; assert.strictEqual(failedStack[0].line, failMainLine); const continueRequest = restartClient.send("continue", { threadId: failThread.id, }); await restartClient.response(continueRequest, "continue"); const childStop = await restartClient.waitFor( (message) => message.seq > initialStop.seq && message.type === "event" && message.event === "stopped" && message.body.reason === "breakpoint", 70000 ); assert.strictEqual(childStop.body.allThreadsStopped, true); const childThreadsRequest = restartClient.send("threads"); const childThreads = ( await restartClient.response(childThreadsRequest, "threads") ).body.threads; const childThread = childThreads.find( (thread) => thread.id === childStop.body.threadId ); assert(childThread, "executing child task must become a DAP virtual thread"); assert.notStrictEqual(childThread.id, failThread.id); assert.match(childThread.name, /task trap/i); const childStackRequest = restartClient.send("stackTrace", { threadId: childThread.id, startFrame: 0, levels: 1, }); const childStack = ( await restartClient.response(childStackRequest, "stackTrace") ).body.stackFrames; assert.strictEqual(childStack[0].line, taskTrapLine); assert.match(childStack[0].name, /task_trap::wasm/); const childScopesRequest = restartClient.send("scopes", { frameId: childStack[0].id, }); const childScopes = ( await restartClient.response(childScopesRequest, "scopes") ).body.scopes; const childArgsScope = childScopes.find( (scope) => scope.name === "Task Args and Handles" ); assert(childArgsScope, "child task argument scope must be present"); const childArgsRequest = restartClient.send("variables", { variablesReference: childArgsScope.variablesReference, }); const childArgs = ( await restartClient.response(childArgsRequest, "variables") ).body.variables; assert( childArgs.some( (variable) => variable.name === "arg_0" && /SmallJson\(Number\(0\)\)/.test(String(variable.value)) ), "child task argument must come from the frozen node participant" ); const parentScopesRequest = restartClient.send("scopes", { frameId: failedStack[0].id, }); const parentScopes = ( await restartClient.response(parentScopesRequest, "scopes") ).body.scopes; const parentArgsScope = parentScopes.find( (scope) => scope.name === "Task Args and Handles" ); const parentArgsRequest = restartClient.send("variables", { variablesReference: parentArgsScope.variablesReference, }); const parentArgs = ( await restartClient.response(parentArgsRequest, "variables") ).body.variables; assert( parentArgs.some( (variable) => /^task_handle_\d+$/.test(variable.name) && /definition=task_trap instance=ti:.*:child:\d+ state=active/.test( variable.value ) && variable.type === "runtime-handle" ), "parent task handle must come from its live Wasm host registry" ); const continueChildRequest = restartClient.send("continue", { threadId: childThread.id, }); await restartClient.response(continueChildRequest, "continue"); await restartClient.waitFor( (message) => message.seq > childStop.seq && message.type === "event" && message.event === "terminated" ); const restartRequest = restartClient.send("restartFrame", { frameId: failedStack[0].id, }); await restartClient.response(restartRequest, "restartFrame"); const restartedStop = await restartClient.waitFor( (message) => message.type === "event" && message.event === "stopped" && /Restarted main from the rebuilt bundle/.test(message.body.description) ); assert.strictEqual(restartedStop.body.allThreadsStopped, true); const restartedStackRequest = restartClient.send("stackTrace", { threadId: restartedStop.body.threadId, startFrame: 0, levels: 1, }); const restartedStack = ( await restartClient.response(restartedStackRequest, "stackTrace") ).body.stackFrames; assert.strictEqual(restartedStack[0].line, failMainLine); await restartClient.close(); } catch (error) { if (restartClient.child.exitCode === null) restartClient.child.kill("SIGKILL"); throw error; } console.log("DAP smoke passed"); })().catch((error) => { console.error(error.stack || error.message); process.exit(1); });