runa/vscode/integration/debugger.test.ts

202 lines
8.1 KiB
TypeScript
Raw Permalink Normal View History

import assert from "node:assert/strict";
import test from "node:test";
import { ImageClient } from "../src/client";
import { ProtocolError, STEP_MODES } from "../src/protocol";
import { parseValue } from "../src/values";
import { startImage, control } from "./server";
test(
"real image supports over/out and gives nested breakpoints priority",
{ timeout: 10000 },
async (t) => {
const image = await startImage();
const client = await ImageClient.connect(image.address, (error) =>
assert.equal(error, undefined),
);
t.after(async () => {
await client.close();
await image.stop();
});
const source =
"pub fn handle(ctx, state, sender, message) {\n let next = nested(state);\n next + 1\n}\nfn nested(value) {\n value + 1\n}";
const file = await client.writeSource("tests/step_modes", null, source);
const actor = await client.spawn(file.path, parseValue("10"));
const other = await client.spawn(file.path, parseValue("20"));
const loaded = await client.inspectExecution(actor);
await client.setBreakpoints(actor, loaded.implementation_id, [1]);
await client.send(actor, parseValue("null"));
await client.tick(1, 1000);
await client.setBreakpoints(actor, loaded.implementation_id, []);
await client.send(other, parseValue("null"));
const otherBefore = await client.inspectActor(other);
assert.equal(await client.stepSource(actor, loaded.implementation_id, 1000, "over"), "step");
let stack = (await client.inspectExecution(actor)).execution;
assert.equal(stack?.call_depth, 0n);
assert.equal(stack?.frames[0]?.range?.start.line, 2);
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("10"));
assert.deepEqual(await client.inspectActor(other), otherBefore);
assert.equal(
await client.stepSource(actor, loaded.implementation_id, 1000, "out"),
"completed",
);
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("12"));
assert.equal((await client.inspectActor(actor)).status.status, "paused");
await client.system(actor, { kind: "restart" });
await client.setBreakpoints(actor, loaded.implementation_id, [1]);
await client.send(actor, parseValue("null"));
await client.tick(1, 1000);
await client.setBreakpoints(actor, loaded.implementation_id, [5]);
assert.equal(
await client.stepSource(actor, loaded.implementation_id, 1000, "over"),
"breakpoint",
);
assert.equal((await client.inspectExecution(actor)).execution?.call_depth, 1n);
await client.setBreakpoints(actor, loaded.implementation_id, []);
assert.equal(await client.stepSource(actor, loaded.implementation_id, 1000, "out"), "step");
stack = (await client.inspectExecution(actor)).execution;
assert.equal(stack?.call_depth, 0n);
assert.equal(stack?.frames[0]?.function, "handle");
await assert.rejects(
control(image.address, {
operation: "step_source",
parameters: {
actor_ref: actor,
implementation_id: loaded.implementation_id,
instruction_budget: 100,
mode: "unknown",
},
}),
/Control connection closed/,
);
for (const mode of STEP_MODES) {
await assert.rejects(
client.stepSource(actor, "stale", 100, mode),
(error) => error instanceof ProtocolError && error.code === "conflict",
);
}
},
);
test(
"actor breakpoints and source stepping preserve drafts, isolate actors and reject stale implementations",
{ timeout: 10000 },
async (t) => {
const image = await startImage();
let changes = 0;
let onChange: (() => void) | undefined;
const client = await ImageClient.connect(
image.address,
(error) => assert.equal(error, undefined),
() => {
changes++;
onChange?.();
},
);
t.after(async () => {
await client.close();
await image.stop();
});
const source =
"pub fn handle(ctx, state, sender, message) {\n ctx.send(ctx.this, 99);\n let next = nested(state);\n next\n}\nfn nested(value) {\n value + 1\n}";
const file = await client.writeSource("tests/debugger", null, source);
const actor = await client.spawn(file.path, parseValue("3"));
const other = await client.spawn(file.path, parseValue("10"));
const loaded = await client.inspectExecution(actor);
const bps = await client.setBreakpoints(actor, loaded.implementation_id, [2, 100, 2]);
assert.deepEqual(
bps.map((bp) => bp.line),
[2, 100],
);
assert.ok(bps[0]?.instruction_pointers.length);
assert.deepEqual(bps[1]?.instruction_pointers, []);
await assert.rejects(
client.stepSource(actor, loaded.implementation_id),
/paused actor with an active invocation/,
);
await client.send(actor, parseValue("null"));
await client.tick(1, 1000);
const paused = await client.inspectActor(actor);
const hit = await client.inspectExecution(actor);
assert.equal(paused.status.status, "paused");
assert.equal(hit.debugger?.stop_reason, "breakpoint");
assert.equal(hit.execution?.frames[0]?.range?.start.line, 2);
assert.deepEqual(paused.state, parseValue("3"));
assert.equal(paused.queued.length, 0);
assert.deepEqual(paused.reserved, parseValue("null"));
await client.send(other, parseValue("null"));
const otherBefore = await client.inspectActor(other);
const count = changes;
const changed = new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("step did not emit image_changed")), 2000);
onChange = () => {
clearTimeout(timer);
onChange = undefined;
resolve();
};
});
assert.equal(await client.stepSource(actor, hit.implementation_id), "step");
await changed;
assert.ok(changes > count, "step changes broadcast image invalidation");
assert.deepEqual(await client.inspectActor(other), otherBefore);
const nested = await client.inspectExecution(actor);
assert.equal(nested.execution?.frames[0]?.function, "nested");
assert.equal(nested.debugger?.stop_reason, "step");
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("3"));
let completed = false;
for (let i = 0; i < 20; i++) {
if ((await client.stepSource(actor, hit.implementation_id)) === "completed") {
completed = true;
break;
}
}
assert.ok(completed);
const committed = await client.inspectActor(actor);
assert.equal(committed.status.status, "paused");
assert.deepEqual(committed.state, parseValue("4"));
assert.deepEqual(committed.queued, [parseValue("99")]);
await client.setBreakpoints(actor, hit.implementation_id, []);
await client.writeSource(
file.path,
file.source_id,
"pub fn handle(ctx, state, sender, message) { 42 }",
);
await client.system(actor, { kind: "reload" });
assert.equal((await client.inspectExecution(actor)).source, source);
await client.system(actor, { kind: "resume" });
await client.tick(1, 100);
assert.deepEqual((await client.inspectExecution(actor)).debugger, {
breakpoints: [],
stop_reason: null,
});
await assert.rejects(
client.setBreakpoints(actor, hit.implementation_id, [0]),
(error) => error instanceof ProtocolError && error.code === "conflict",
);
await assert.rejects(
client.stepSource(actor, hit.implementation_id),
(error) => error instanceof ProtocolError && error.code === "conflict",
);
const active = await client.inspectExecution(actor);
for (const budget of [0, 10001]) {
const reply = await control(image.address, {
operation: "step_source",
parameters: {
actor_ref: actor,
implementation_id: active.implementation_id,
instruction_budget: budget,
},
});
assert.match(JSON.stringify(reply), /invalid_request/);
}
const invalid = await control(image.address, {
operation: "set_breakpoints",
parameters: {
actor_ref: actor,
implementation_id: active.implementation_id,
lines: Array(129).fill(0),
},
});
assert.match(JSON.stringify(invalid), /invalid_request/);
},
);