202 lines
8.1 KiB
TypeScript
202 lines
8.1 KiB
TypeScript
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/);
|
|
},
|
|
);
|