fix(test): wait for valid debugger fixture PIDs (#9208)

* fix(test): wait for valid debugger fixture PIDs

* test(process): cover delayed and invalid PID publication

* test(process): share validated PID file readiness
This commit is contained in:
lostviolinist authored and GitHub committed 2026-11-07 11:12:43 +08:00
1 parent abfccc0683
commit 57fc2d4617
4 files changed
+85 -45

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
import { open, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { waitForTestPidFile } from "../../support/process/pidFile.js";
it("waits through a missing file and an empty file before accepting its PID", async () => {
const directory = await createTestTempDirectory("rea-pid-file-");
const path = join(directory, "pid");
let settled = false;
const reading = waitForTestPidFile(path, 1_000).finally(() => {
settled = true;
});
const observed = reading.catch(() => undefined);
try {
await delay(30);
expect(settled).toBe(false);
const file = await open(path, "w");
try {
await delay(30);
expect(settled).toBe(false);
await file.writeFile(String(process.pid));
await expect(reading).resolves.toBe(process.pid);
} finally {
await file.close();
}
} finally {
await observed;
}
});
it.each(["", "0", "-1", "1e3", "12x", "9007199254740992"])(
"refuses a PID file containing %j when its deadline expires",
async (text) => {
const directory = await createTestTempDirectory("rea-invalid-pid-file-");
const path = join(directory, "pid");
await writeFile(path, text);
await expect(waitForTestPidFile(path, 30)).rejects.toThrow(
"did not publish a valid PID",
);
},
);
@@ -10,6 +10,7 @@ import { evidenceSchema } from "../../../src/domain/evidence.js";
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import { stopOwnedDebuggerProcess } from "../../../src/process/ownedDebuggerSession.js";
import { waitForTestPidFile } from "../../support/process/pidFile.js";
// These tests launch controlled fake providers; Linux admission is injected so
// lifecycle semantics remain testable on hosts that correctly reject GDB.
@@ -21,9 +22,9 @@ const createGdbManagerForInjectedPlatform = (
let directory: string | undefined;
const managers: Array<{ closeAll(): Promise<void> }> = [];
const expectOwnedProcessExited = async (pidText: string): Promise<void> => {
const pid = Number(pidText);
expect(Number.isInteger(pid)).toBe(true);
const expectOwnedProcessExited = async (pid: number): Promise<void> => {
expect(Number.isSafeInteger(pid)).toBe(true);
expect(pid).toBeGreaterThan(0);
await expect
.poll(() => {
try {
@@ -44,6 +45,10 @@ const expectOwnedProcessExited = async (pidText: string): Promise<void> => {
.toBe(false);
};
it("rejects an empty PID before observing process exit", async () => {
await expect(expectOwnedProcessExited(Number(""))).rejects.toThrow();
});
it("keeps persistent GDB sessions unavailable on Windows", async () => {
const gdb = await createGdbManagerForInjectedPlatform({
environment: { REA_GDB_COMMAND: "/missing/gdb" },
@@ -448,19 +453,7 @@ process.stdin.resume();
managers.push(manager);
const starting = manager.start();
let pidText: string | undefined;
for (
let attempt = 0;
attempt < 100 && pidText === undefined;
attempt += 1
) {
try {
pidText = await readFile(pidPath, "utf8");
} catch {
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
if (pidText === undefined) throw new Error("fake GDB did not start");
const pid = await waitForTestPidFile(pidPath, 1_000);
let closeSettled = false;
const closing = manager.closeAll().then(() => {
@@ -481,7 +474,7 @@ process.stdin.resume();
if (started.ok) throw new Error("GDB unexpectedly started");
expect(started.error._tag).toBe("AnalysisCancelledError");
expect(await readFile(signalPath, "utf8")).toBe("SIGTERM");
await expectOwnedProcessExited(pidText);
await expectOwnedProcessExited(pid);
expect(manager.status("unknown-session")).toBeUndefined();
await expect(manager.start()).resolves.toMatchObject({
ok: false,
@@ -589,7 +582,7 @@ setTimeout(() => process.exit(0), 250).unref();
expect(result.error.cleanupIncomplete).toBe(true);
}
await expectOwnedProcessExited(
await readFile(join(directory, "pid"), "utf8"),
await waitForTestPidFile(join(directory, "pid"), 1_000),
);
expect(manager.status(started.value.session_id)?.state).toBe("closed");
},
@@ -1033,7 +1026,7 @@ setInterval(() => {}, 1000);
child.kill("SIGKILL");
}
await ownedProcessExit;
await expectOwnedProcessExited(String(ownedLaunch.process.pid));
await expectOwnedProcessExited(ownedLaunch.process.pid!);
}
const retried = await manager.retryCleanupFailures();
expect(retried.ok).toBe(false);
@@ -25,6 +25,7 @@ import {
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { createInjectedProcess } from "./RizinDebugSessionCoverage.fixtures.js";
import { waitForTestPidFile } from "../../support/process/pidFile.js";
let terminal: ReturnType<typeof spawn> | undefined;
let manager: RizinDebugSessionManager | undefined;
@@ -484,18 +485,7 @@ it.skipIf(
);
const closed = await closing;
expect(closed.ok, closed.ok ? "" : closed.error.message).toBe(true);
const deadline = Date.now() + 5_000;
let pidText = "";
while (Date.now() < deadline) {
try {
pidText = await readFile(pidPath, "utf8");
break;
} catch {
await new Promise((resolveDelay) => setTimeout(resolveDelay, 50));
}
}
inferiorPid = Number(pidText);
expect(Number.isSafeInteger(inferiorPid) && inferiorPid > 0).toBe(true);
inferiorPid = await waitForTestPidFile(pidPath, 5_000);
expect(() => process.kill(inferiorPid!, 0)).not.toThrow();
await writeFile(releasePath, "release after close");
const outputDeadline = Date.now() + 3_000;
@@ -636,20 +626,7 @@ process.stdin.on("data", (chunk) => {
).toBe(true);
if (!started.ok) throw started.error;
if (launchTarget) {
let pidText: string | undefined;
const deadline = Date.now() + 2_000;
while (pidText === undefined && Date.now() < deadline) {
try {
pidText = await readFile(pidPath, "utf8");
} catch {
await new Promise((resolveDelay) => setTimeout(resolveDelay, 10));
}
}
const parsedInferiorPid =
pidText === undefined ? Number.NaN : Number(pidText);
if (!Number.isSafeInteger(parsedInferiorPid) || parsedInferiorPid <= 0)
throw new Error("The test inferior did not publish a valid PID.");
inferiorPid = parsedInferiorPid;
inferiorPid = await waitForTestPidFile(pidPath, 2_000);
}
return started.value.session_id;
}
+25
View File
@@ -0,0 +1,25 @@
import { readFile } from "node:fs/promises";
import { setTimeout as delay } from "node:timers/promises";
/** Wait for a fixture's PID bytes, not merely the creation of its file. */
export const waitForTestPidFile = async (
path: string,
timeoutMs: number,
): Promise<number> => {
const deadline = performance.now() + timeoutMs;
do {
let text = "";
try {
text = await readFile(path, "utf8");
} catch (cause) {
if (
!(cause instanceof Error && "code" in cause && cause.code === "ENOENT")
)
throw cause;
}
const pid = Number(text);
if (/^[1-9][0-9]*$/.test(text) && Number.isSafeInteger(pid)) return pid;
await delay(10);
} while (performance.now() < deadline);
throw new Error(`Test process did not publish a valid PID in ${path}`);
};