rea/tests/boundary/providers/GdbCommandQueue.test.ts

146 lines
4.4 KiB
TypeScript

import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, it } from "vitest";
import { GdbSessionManager } from "../../../src/gdb/GdbSessionManager.js";
let directory: string | undefined;
const managers: GdbSessionManager[] = [];
afterEach(async () => {
await Promise.all(
managers
.splice(0)
.map((manager) => manager.closeAll().catch(() => undefined)),
);
if (directory !== undefined)
await rm(directory, { recursive: true, force: true });
directory = undefined;
});
it("bounds active and queued GDB commands, including cancelled placeholders", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-gdb-command-queue-limit-"));
const provider = join(directory, "fake-gdb-queue.mjs");
const activePath = join(directory, "active-command");
const releasePath = join(directory, "release-command");
await writeFile(
provider,
`#!${process.execPath}
import { access, writeFile } from "node:fs/promises";
process.stdout.write("(gdb)\\n");
let input = "";
let firstConsoleCommand = true;
process.stdin.setEncoding("utf8");
process.stdin.on("data", async (chunk) => {
input += chunk;
for (;;) {
const boundary = input.indexOf("\\n");
if (boundary < 0) break;
const line = input.slice(0, boundary); input = input.slice(boundary + 1);
const match = /^(\\d+)/.exec(line);
if (!match) continue;
if (line.includes("-interpreter-exec console") && firstConsoleCommand) {
firstConsoleCommand = false;
await writeFile(${JSON.stringify(activePath)}, "active");
for (;;) {
try { await access(${JSON.stringify(releasePath)}); break; }
catch { await new Promise((resolve) => setTimeout(resolve, 10)); }
}
}
process.stdout.write(match[1] + "^done\\n");
}
});
`,
);
await chmod(provider, 0o700);
const manager = new GdbSessionManager({
platform: "linux",
environment: { REA_GDB_COMMAND: provider },
});
managers.push(manager);
const started = await manager.start();
expect(started.ok).toBe(true);
if (!started.ok) return;
const active = manager.execute(started.value.session_id, "hold");
let activeSeen = false;
for (let attempt = 0; attempt < 100 && !activeSeen; attempt += 1) {
try {
await readFile(activePath, "utf8");
activeSeen = true;
} catch {
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
expect(activeSeen).toBe(true);
const controllers = Array.from({ length: 15 }, () => new AbortController());
const queued = controllers.map((controller, index) =>
manager.execute(
started.value.session_id,
`queued-${index}`,
controller.signal,
),
);
const safetyRelease = setTimeout(() => {
void writeFile(releasePath, "safety release");
}, 250);
const overflow = await manager.execute(started.value.session_id, "overflow");
clearTimeout(safetyRelease);
expect(overflow).toMatchObject({
ok: false,
error: {
_tag: "AnalysisResourceConstraintError",
operation: "gdb_console",
resource: "transport",
reportedLimits: {
max_pending_commands: 16,
active_or_queued_commands: 16,
},
},
});
for (const controller of controllers) controller.abort();
const cancelled = await Promise.all(queued);
expect(cancelled).toHaveLength(15);
expect(
cancelled.every(
(result) => !result.ok && result.error._tag === "AnalysisCancelledError",
),
).toBe(true);
const cancellationSafetyRelease = setTimeout(() => {
void writeFile(releasePath, "safety release after cancellation");
}, 250);
try {
await expect(
manager.execute(
started.value.session_id,
"cannot-bypass-cancelled-placeholders",
),
).resolves.toMatchObject({
ok: false,
error: {
_tag: "AnalysisResourceConstraintError",
reportedLimits: { active_or_queued_commands: 16 },
},
});
} finally {
clearTimeout(cancellationSafetyRelease);
}
await writeFile(releasePath, "release");
expect((await active).ok).toBe(true);
const admittedAfterDrain = await manager.execute(
started.value.session_id,
"admitted-after-drain",
);
expect(admittedAfterDrain.ok).toBe(true);
await expect(
manager.execute(started.value.session_id, "after-drain"),
).resolves.toMatchObject({
ok: true,
value: { result: { completion_status: "complete" } },
});
});