mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
test: retain independent ownership of fake GDB processes
This commit is contained in:
1 parent
0c37c22d51
commit
0ea642843a
5 files changed
+198
-32
No files matched your search
@@ -0,0 +1,85 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { access, chmod, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { OwnedDebuggerCleanupError } from "../../../src/process/ownedDebuggerCleanupError.js";
|
||||
import { ProviderProcessSupervisor } from "../../../src/process/ProviderProcess.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js";
|
||||
import { createGdbFixtureOwner } from "./gdbTestHelpers.js";
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"retains an uncertain fake GDB child and files for independent cleanup retry",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-gdb-fixture-owner-");
|
||||
const provider = join(root, "fake-gdb.mjs");
|
||||
await writeFile(
|
||||
provider,
|
||||
`#!${process.execPath}\nprocess.stdout.write("ready\\n"); process.stdin.resume(); setInterval(() => {}, 1000);\n`,
|
||||
);
|
||||
await chmod(provider, 0o700);
|
||||
let refused = true;
|
||||
const fixture = createGdbFixtureOwner({
|
||||
stopOwnedProcess: (supervisor) =>
|
||||
refused
|
||||
? Promise.resolve({
|
||||
status: "incomplete",
|
||||
reason: "injected physical cleanup refusal",
|
||||
})
|
||||
: supervisor.stop(),
|
||||
});
|
||||
const launched = await fixture.spawnProcess({
|
||||
command: provider,
|
||||
arguments: [],
|
||||
runId: randomUUID(),
|
||||
stdin: "pipe",
|
||||
});
|
||||
const managers = [
|
||||
{
|
||||
closeAll: async () => {
|
||||
throw new OwnedDebuggerCleanupError(
|
||||
[new Error("inferior state intentionally unknown")],
|
||||
"controlled debugger refusal",
|
||||
[launched.process.pid!],
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const remove = () => rm(root, { recursive: true, force: true });
|
||||
try {
|
||||
await waitForProviderProcessReady(launched.process);
|
||||
await expect(fixture.cleanup(managers, remove)).rejects.toMatchObject({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining(
|
||||
"injected physical cleanup refusal",
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(managers).toHaveLength(1);
|
||||
expect(launched.process.exitCode).toBeNull();
|
||||
expect(launched.process.signalCode).toBeNull();
|
||||
await access(provider);
|
||||
refused = false;
|
||||
await fixture.cleanup(managers, remove);
|
||||
expect(managers).toHaveLength(0);
|
||||
expect(
|
||||
launched.process.exitCode !== null ||
|
||||
launched.process.signalCode !== null,
|
||||
).toBe(true);
|
||||
await expect(access(provider)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
refused = false;
|
||||
// Independent backstop keeps a broken ownership mutation from leaking its child.
|
||||
const safety = new ProviderProcessSupervisor(
|
||||
{ ...launched, ownsProcessLifetime: true },
|
||||
{ captureStdout: false, maxDiagnosticBytes: 0 },
|
||||
);
|
||||
expect((await safety.stop()).status).not.toBe("incomplete");
|
||||
await fixture.cleanup(managers, remove);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -31,9 +31,13 @@ import { DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES } from "../../../src/domain/a
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { stopOwnedDebuggerProcess } from "../../../src/process/ownedDebuggerSession.js";
|
||||
import { parseLinuxProcessStat } from "../../../src/process/LinuxProcessStat.js";
|
||||
import { expectGdbSessionStarted } from "./gdbTestHelpers.js";
|
||||
import {
|
||||
createGdbFixtureOwner,
|
||||
expectGdbSessionStarted,
|
||||
} from "./gdbTestHelpers.js";
|
||||
|
||||
let directory: string | undefined;
|
||||
const fixtureOwner = createGdbFixtureOwner();
|
||||
const managers: GdbSessionManager[] = [];
|
||||
|
||||
it.each([
|
||||
@@ -208,7 +212,10 @@ it("rejects an invalid caller TTY and platforms without safe GDB ownership", asy
|
||||
expect(macUnavailable.error.message).toContain("available only on Linux");
|
||||
expect(macSpawned).toBe(false);
|
||||
|
||||
const windows = new GdbSessionManager({ platform: "win32" });
|
||||
const windows = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "win32",
|
||||
});
|
||||
managers.push(windows);
|
||||
const unavailable = await windows.start();
|
||||
expect(unavailable).toMatchObject({
|
||||
@@ -1625,6 +1632,7 @@ const managerFor = async (
|
||||
options: ConstructorParameters<typeof GdbSessionManager>[0] = {},
|
||||
): Promise<GdbSessionManager> => {
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: await createProvider(body) },
|
||||
...options,
|
||||
@@ -1653,23 +1661,21 @@ process.stdin.on('data', (chunk) => {
|
||||
`;
|
||||
|
||||
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;
|
||||
await fixtureOwner.cleanup(managers, async () => {
|
||||
if (directory !== undefined)
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
directory = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a pre-cancelled GDB start and an unconfigured executable", async () => {
|
||||
const cancelled = new AbortController();
|
||||
const reason = new Error("caller cancelled before start");
|
||||
cancelled.abort(reason);
|
||||
const startResult = await new GdbSessionManager({ platform: "linux" }).start(
|
||||
cancelled.signal,
|
||||
);
|
||||
const startResult = await new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
}).start(cancelled.signal);
|
||||
expect(startResult.ok).toBe(false);
|
||||
if (!startResult.ok) {
|
||||
expect(startResult.error._tag).toBe("AnalysisCancelledError");
|
||||
@@ -1679,12 +1685,14 @@ it("rejects a pre-cancelled GDB start and an unconfigured executable", async ()
|
||||
const nullReasonController = new AbortController();
|
||||
nullReasonController.abort(null);
|
||||
const nullReasonResult = await new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
}).start(nullReasonController.signal);
|
||||
expect(nullReasonResult.ok).toBe(false);
|
||||
if (!nullReasonResult.ok) expect(nullReasonResult.error.cause).toBeNull();
|
||||
|
||||
const missing = await new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: join(tmpdir(), "does-not-exist-gdb") },
|
||||
}).start();
|
||||
@@ -1713,6 +1721,7 @@ if (process.argv.includes('--interpreter=mi3')) process.exit(0);
|
||||
${responsiveProvider()}
|
||||
`);
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
});
|
||||
@@ -1729,6 +1738,7 @@ it("retains ownership when GDB startup fails before its prompt and stop is incom
|
||||
let retainedSessionId: string | undefined;
|
||||
const provider = await createProvider("process.exit(0);\n");
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
stopProcess: async (session, name) => {
|
||||
@@ -1859,6 +1869,7 @@ it("returns startup timeout when the MI prompt never arrives", async () => {
|
||||
"process.stdin.resume(); setInterval(() => {}, 1000);\n",
|
||||
);
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
startupTimeoutMs: 20,
|
||||
@@ -1939,6 +1950,7 @@ it("retains startup timeout without partial evidence when cleanup is uncertain",
|
||||
"process.stdin.resume(); setInterval(() => {}, 1000);\n",
|
||||
);
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
startupTimeoutMs: 20,
|
||||
@@ -2022,6 +2034,7 @@ it("handles a start signal that becomes aborted during listener registration", a
|
||||
},
|
||||
});
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
});
|
||||
@@ -3167,6 +3180,7 @@ it("retains a session when closing cannot confirm the debugger process stopped",
|
||||
let stopCalls = 0;
|
||||
const provider = await createProvider(responsiveProvider());
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
stopProcess: async (session, name) => {
|
||||
@@ -3222,6 +3236,7 @@ it.each([new Error("simulated stop failure"), "simulated stop failure"])(
|
||||
`),
|
||||
);
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: {
|
||||
REA_GDB_COMMAND: provider,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createGdbFixtureOwner } from "./gdbTestHelpers.js";
|
||||
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -8,17 +9,15 @@ import { GdbSessionManager } from "../../../src/gdb/GdbSessionManager.js";
|
||||
|
||||
describe("persistent debugger session managers", () => {
|
||||
let directory: string | undefined;
|
||||
const fixtureOwner = createGdbFixtureOwner();
|
||||
const managers: Array<{ closeAll(): Promise<void> }> = [];
|
||||
|
||||
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;
|
||||
await fixtureOwner.cleanup(managers, async () => {
|
||||
if (directory !== undefined)
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
directory = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
it("runs a correlated GDB MI command, reports status, and closes its owned process", async () => {
|
||||
@@ -47,6 +46,7 @@ process.stdin.on("data", (chunk) => {
|
||||
);
|
||||
await chmod(provider, 0o700);
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
});
|
||||
@@ -93,6 +93,7 @@ process.stdin.on("data", (chunk) => {
|
||||
);
|
||||
await chmod(provider, 0o700);
|
||||
const manager = new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createGdbFixtureOwner } from "./gdbTestHelpers.js";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -17,9 +18,14 @@ import { waitForTestPidFile } from "../../support/process/pidFile.js";
|
||||
const createGdbManagerForInjectedPlatform = (
|
||||
options: ConstructorParameters<typeof GdbSessionManager>[0] = {},
|
||||
): GdbSessionManager =>
|
||||
new GdbSessionManager({ platform: "linux", ...options });
|
||||
new GdbSessionManager({
|
||||
spawnProcess: fixtureOwner.spawnProcess,
|
||||
platform: "linux",
|
||||
...options,
|
||||
});
|
||||
|
||||
let directory: string | undefined;
|
||||
const fixtureOwner = createGdbFixtureOwner();
|
||||
const managers: Array<{ closeAll(): Promise<void> }> = [];
|
||||
|
||||
const expectOwnedProcessExited = async (pid: number): Promise<void> => {
|
||||
@@ -381,7 +387,7 @@ process.stdin.on("data", (chunk) => {
|
||||
enterSecondSpawn?.();
|
||||
await secondSpawnGate;
|
||||
}
|
||||
return spawnOwnedProviderProcess(options);
|
||||
return fixtureOwner.spawnProcess(options);
|
||||
},
|
||||
});
|
||||
managers.push(manager);
|
||||
@@ -487,14 +493,11 @@ process.stdin.resume();
|
||||
);
|
||||
|
||||
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;
|
||||
await fixtureOwner.cleanup(managers, async () => {
|
||||
if (directory !== undefined)
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
directory = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
@@ -988,7 +991,7 @@ setInterval(() => {}, 1000);
|
||||
const manager = createGdbManagerForInjectedPlatform({
|
||||
environment: { REA_GDB_COMMAND: provider },
|
||||
spawnProcess: async (options) => {
|
||||
ownedLaunch = await spawnOwnedProviderProcess(options);
|
||||
ownedLaunch = await fixtureOwner.spawnProcess(options);
|
||||
ownedProcessExit = new Promise<void>((resolve) => {
|
||||
const child = ownedLaunch!.process;
|
||||
if (child.exitCode !== null || child.signalCode !== null) resolve();
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
ProviderProcessSupervisor,
|
||||
spawnOwnedProviderProcess,
|
||||
} from "../../../src/process/ProviderProcess.js";
|
||||
import { OwnedDebuggerCleanupError } from "../../../src/process/ownedDebuggerCleanupError.js";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import type { GdbSessionManager } from "../../../src/gdb/GdbSessionManager.js";
|
||||
@@ -14,3 +19,60 @@ export const expectGdbSessionStarted = async (
|
||||
if (!started.ok) throw new Error(started.error.message);
|
||||
return started;
|
||||
};
|
||||
|
||||
/** The fixture, unlike GDB, knows these inert provider processes have no real inferior. */
|
||||
export const createGdbFixtureOwner = (
|
||||
controls: {
|
||||
readonly stopOwnedProcess?: (
|
||||
supervisor: ProviderProcessSupervisor,
|
||||
) => ReturnType<ProviderProcessSupervisor["stop"]>;
|
||||
} = {},
|
||||
) => {
|
||||
const processes = new Set<ProviderProcessSupervisor>();
|
||||
const spawnProcess: typeof spawnOwnedProviderProcess = async (options) => {
|
||||
const launched = await spawnOwnedProviderProcess(options);
|
||||
processes.add(
|
||||
new ProviderProcessSupervisor(
|
||||
{ ...launched, ownsProcessLifetime: true },
|
||||
{ captureStdout: false, maxDiagnosticBytes: 0 },
|
||||
),
|
||||
);
|
||||
return launched;
|
||||
};
|
||||
const cleanup = async (
|
||||
managers: Array<{ closeAll(): Promise<void> }>,
|
||||
removeFiles: () => Promise<void>,
|
||||
): Promise<void> => {
|
||||
const outcomes = await Promise.allSettled(
|
||||
managers.map((manager) => manager.closeAll()),
|
||||
);
|
||||
// The tests intentionally model unknown inferior state. Only that tagged
|
||||
// refusal is discharged by the fixture's independent physical process owner.
|
||||
const failures = outcomes.flatMap((outcome) =>
|
||||
outcome.status === "rejected" &&
|
||||
!(outcome.reason instanceof OwnedDebuggerCleanupError)
|
||||
? [outcome.reason as unknown]
|
||||
: [],
|
||||
);
|
||||
for (const supervisor of processes) {
|
||||
try {
|
||||
const result =
|
||||
controls.stopOwnedProcess === undefined
|
||||
? await supervisor.stop()
|
||||
: await controls.stopOwnedProcess(supervisor);
|
||||
if (result.status === "incomplete")
|
||||
throw new Error(`Fake GDB cleanup incomplete: ${result.reason}`);
|
||||
if (supervisor.observedExit() === undefined)
|
||||
throw new Error("Fake GDB cleanup did not observe child exit");
|
||||
processes.delete(supervisor);
|
||||
} catch (cause: unknown) {
|
||||
failures.push(cause);
|
||||
}
|
||||
}
|
||||
if (failures.length > 0)
|
||||
throw new AggregateError(failures, "Fake GDB fixture cleanup failed");
|
||||
managers.length = 0;
|
||||
await removeFiles();
|
||||
};
|
||||
return { spawnProcess, cleanup };
|
||||
};
|
||||
Reference in new issue
Block a user