mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
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:
1 parent
abfccc0683
commit
57fc2d4617
4 files changed
+85
-45
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
};
|
||||
Reference in new issue
Block a user