Compare commits

...
Author SHA1 Message Date
N0zoM1z0andKaoru0822 2583c25d22 ci: verify Rizin fixture lifetimes on native macOS
Verification-only branch for candidate 5d85cfe4e; only the workflow differs. Require seven files and all 88 native macOS cases with a fresh JSON report and source identity checks. This workflow is not for integration.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 15:11:17 +00:00
N0zoM1z0andKaoru0822 5d85cfe4eb chore(ci): refresh Rizin fixture audit against current main
Integrate main 9d6651e5b before publishing the shared fixture lifetime fix. The incoming Apple, Kuna and WABT changes do not alter the Rizin or process supervision boundaries.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 14:58:22 +00:00
N0zoM1z0andKaoru0822 349be102ee test(rizin): own fake provider cleanup in shared fixtures
Preserve injected cleanup uncertainty and partial evidence while the fixture harness reaps and awaits its actual spawned children. Retain failed resources and every owned directory for retry; cover missing reaping, missing exit waits, and cleanup recovery.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 14:54:32 +00:00
Dr Kennedy UmegeandClaude Opus 5.5 461dc76978 fix(test): reap the Rizin provider left by startup cleanup uncertainty
"preserves startup cleanup uncertainty after cancellation" starts a fake Rizin
that never answers the detach request. The real stop therefore reports
"inferior detach was not verified" and correctly retains ownership, and the
injected incompleteStop teardown cannot stop it either, so every run left an
orphaned fake-rizin.mjs process group behind (reproduced on macOS 27.0.1).

Reap the owned launch in a finally block with cleanupOwnedProcessGroup and a
SIGKILL fallback, then await its exit, matching the GDB review regression.
Also drop the unused RizinDebugSessionManager import flagged by oxlint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:41:14 +01:00
6 changed files with 279 additions and 1022 deletions

No files matched your search

+45 -931
View File
File diff suppressed because it is too large. Load diff
@@ -10,6 +10,10 @@ import {
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { stopOwnedDebuggerProcess } from "../../../src/process/ownedDebuggerSession.js";
import {
ProviderProcessSupervisor,
spawnOwnedProviderProcess,
} from "../../../src/process/ProviderProcess.js";
import type {
ProviderProcessHandle,
SpawnedOwnedProviderProcess,
@@ -24,10 +28,11 @@ interface InjectedProcessControl {
exit(signal?: NodeJS.Signals): void;
}
type RizinTestManagerOptions = NonNullable<
ConstructorParameters<typeof RizinDebugSessionManager>[1]
>;
type InjectedSpawnProcess = NonNullable<
NonNullable<
ConstructorParameters<typeof RizinDebugSessionManager>[1]
>["spawnProcess"]
RizinTestManagerOptions["spawnProcess"]
>;
export const incompleteStop: typeof stopOwnedDebuggerProcess = async (
@@ -176,17 +181,47 @@ export const createInjectedProcess = (
return { spawnProcess, control };
};
export const createRizinCoverageHarness = () => {
let directory: string | undefined;
export const createRizinCoverageHarness = (
controls: {
readonly stopOwnedProcess?: (
supervisor: ProviderProcessSupervisor,
) => ReturnType<ProviderProcessSupervisor["stop"]>;
} = {},
) => {
let latestDirectory: string | undefined;
const directories = new Set<string>();
const managers: RizinDebugSessionManager[] = [];
const track = (manager: RizinDebugSessionManager): void => {
const ownedProcesses = new Set<ProviderProcessSupervisor>();
const spawnOwnedProcess: InjectedSpawnProcess = async (options) => {
const launched = await spawnOwnedProviderProcess(options);
// The fixture owns these fake children independently of the manager's
// intentionally uncertain debugger/inferior lifecycle.
ownedProcesses.add(
new ProviderProcessSupervisor(
{ ...launched, ownsProcessLifetime: true },
{ captureStdout: false, maxDiagnosticBytes: 0 },
),
);
return launched;
};
const createManager: typeof createRizinTestManager = (
environment = process.env,
options = {},
) => {
const manager = createRizinTestManager(environment, {
...options,
spawnProcess: options.spawnProcess ?? spawnOwnedProcess,
});
managers.push(manager);
return manager;
};
const makeProvider = async (
body: string,
shebang = "#!/usr/bin/env node",
): Promise<string> => {
directory = await mkdtemp(join(tmpdir(), "rea-rizin-coverage-"));
const directory = await mkdtemp(join(tmpdir(), "rea-rizin-coverage-"));
directories.add(directory);
latestDirectory = directory;
const provider = join(directory, "fake-rizin.mjs");
await writeFile(provider, `${shebang}\n${body}\n`);
await chmod(provider, 0o700);
@@ -194,13 +229,9 @@ export const createRizinCoverageHarness = () => {
};
const start = async (
provider: string,
options: ConstructorParameters<typeof RizinDebugSessionManager>[1] = {},
options: RizinTestManagerOptions = {},
): Promise<{ manager: RizinDebugSessionManager; sessionId: string }> => {
const manager = createRizinTestManager(
{ REA_RIZIN_COMMAND: provider },
options,
);
track(manager);
const manager = createManager({ REA_RIZIN_COMMAND: provider }, options);
const result = await manager.start({
path: "/tmp/coverage-target",
inferior_tty: await rizinTestInferiorTty(),
@@ -210,13 +241,12 @@ export const createRizinCoverageHarness = () => {
};
const startInjected = async (
spawnProcess: InjectedSpawnProcess,
options: ConstructorParameters<typeof RizinDebugSessionManager>[1] = {},
options: RizinTestManagerOptions = {},
) => {
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ ...options, spawnProcess },
);
track(manager);
const started = await manager.start({
path: "/tmp/injected",
inferior_tty: await rizinTestInferiorTty(),
@@ -229,17 +259,42 @@ export const createRizinCoverageHarness = () => {
.splice(0)
.map((manager) => manager.closeAll().catch(() => undefined)),
);
if (directory !== undefined)
const failures: Error[] = [];
for (const supervisor of ownedProcesses) {
const stopped =
controls.stopOwnedProcess === undefined
? await supervisor.stop()
: await controls.stopOwnedProcess(supervisor);
if (stopped.status === "incomplete") {
failures.push(
new Error(`Fake Rizin cleanup incomplete: ${stopped.reason}`),
);
} else if (supervisor.observedExit() === undefined) {
failures.push(
new Error("Fake Rizin cleanup did not observe child exit"),
);
} else {
ownedProcesses.delete(supervisor);
}
}
// Keep ownership and fixture files available when cleanup needs a retry.
if (failures.length > 0)
throw new AggregateError(failures, "Fake Rizin fixture cleanup failed");
for (const directory of directories) {
await rm(directory, { recursive: true, force: true });
directory = undefined;
directories.delete(directory);
}
latestDirectory = undefined;
};
const missingProviderPath = (): string => join(directory!, "does-not-exist");
const missingProviderPath = (): string =>
join(latestDirectory!, "does-not-exist");
return {
cleanup,
makeProvider,
missingProviderPath,
start,
startInjected,
track,
createManager,
spawnOwnedProcess,
};
};
@@ -1,12 +1,13 @@
import { randomUUID } from "node:crypto";
import { access } from "node:fs/promises";
import { afterEach, expect, it, vi } from "vitest";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import {
createRizinTestManager,
stopFakeNoInferior,
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { rizinCommand } from "../../../src/rizin/RizinCommand.js";
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import * as providerProcess from "../../../src/process/ProviderProcess.js";
import type { SpawnedOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import {
createRizinCoverageHarness,
createInjectedProcess,
@@ -20,7 +21,8 @@ const {
missingProviderPath,
start,
startInjected,
track,
createManager,
spawnOwnedProcess,
} = createRizinCoverageHarness();
afterEach(cleanup);
@@ -40,7 +42,7 @@ it.skipIf(process.platform === "win32")(
it.skipIf(process.platform === "win32")(
"rejects unsupported, pre-cancelled, and failed startup requests",
async () => {
const windows = createRizinTestManager({}, { platform: "win32" });
const windows = createManager({}, { platform: "win32" });
expect(
(
await windows.start({
@@ -61,20 +63,20 @@ it.skipIf(process.platform === "win32")(
).toBe(false);
const failed = await makeProvider("process.exit(9);");
const manager = createRizinTestManager({
const manager = createManager({
REA_RIZIN_COMMAND: failed,
});
track(manager);
const result = await manager.start({
path: "/tmp/a",
inferior_tty: await rizinTestInferiorTty(),
});
expect(result.ok).toBe(false);
const missing = createRizinTestManager({
const missing = createManager({
REA_RIZIN_COMMAND: missingProviderPath(),
});
track(missing);
const spawnFailure = await missing.start({
path: "/tmp/a",
inferior_tty: await rizinTestInferiorTty(),
@@ -87,10 +89,10 @@ it.skipIf(process.platform === "win32")(
"returns a startup timeout when Rizin exits before its initial frame",
async () => {
const provider = await makeProvider("process.exit(0);");
const manager = createRizinTestManager({
const manager = createManager({
REA_RIZIN_COMMAND: provider,
});
track(manager);
const result = await manager.start({
path: "/tmp/early-exit",
inferior_tty: await rizinTestInferiorTty(),
@@ -110,14 +112,14 @@ it.skipIf(process.platform === "win32")(
startup: () => undefined,
beforeSpawnReturns: resolveSpawned,
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{
spawnProcess: injected.spawnProcess,
stopProcess: async () => ({ status: "stopped" }),
},
);
track(manager);
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
try {
const starting = manager.start({
@@ -139,7 +141,7 @@ it.skipIf(process.platform === "win32")(
"does not spawn Rizin when cancellation arrives during TTY validation",
async () => {
let spawned = false;
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{
spawnProcess: async () => {
@@ -164,7 +166,7 @@ it.skipIf(process.platform === "win32")(
"rejects a non-TTY path before spawning Rizin",
async () => {
let spawned = false;
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{
spawnProcess: async () => {
@@ -195,7 +197,7 @@ it.skipIf(process.platform === "win32")(
releaseSpawns = resolve;
});
const injected = createInjectedProcess();
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{
spawnProcess: async (options) => {
@@ -240,10 +242,9 @@ it.skipIf(process.platform === "win32")(
const provider = await makeProvider(
'process.stdout.write("x".repeat(16 * 1024 * 1024 + 1) + "\\0"); process.stdin.resume();',
);
const manager = createRizinTestManager({
const manager = createManager({
REA_RIZIN_COMMAND: provider,
});
track(manager);
const result = await manager.start({
path: "/tmp/oversized-startup",
@@ -264,19 +265,19 @@ it.skipIf(process.platform === "win32")(
const provider = await makeProvider(
'setTimeout(() => process.stdout.write("\\0"), 500); process.stdin.resume();',
);
const manager = createRizinTestManager(
const manager = createManager(
{
REA_RIZIN_COMMAND: provider,
},
{
spawnProcess: async (options) => {
const launched = await spawnOwnedProviderProcess(options);
const launched = await spawnOwnedProcess(options);
resolveSpawned();
return launched;
},
},
);
track(manager);
const abort = new AbortController();
const starting = manager.start(
{ path: "/tmp/cancel-start", inferior_tty: await rizinTestInferiorTty() },
@@ -288,39 +289,138 @@ it.skipIf(process.platform === "win32")(
},
);
it.skipIf(process.platform === "win32")(
"preserves startup cleanup uncertainty after cancellation",
async () => {
it
.skipIf(process.platform === "win32")
.each(["is uncertain", "reports stopped before exit"] as const)(
"cleans startup fixtures after cancellation when manager stop %s",
async (stopMode) => {
const provider = await makeProvider(
"process.stdin.resume(); setInterval(() => {}, 1000);",
);
let resolveSpawned!: () => void;
const spawned = new Promise<void>((resolve) => {
resolveSpawned = resolve;
});
const provider = await makeProvider(
let ownedLaunch: SpawnedOwnedProviderProcess | undefined;
const nativeSpawn = providerProcess.spawnOwnedProviderProcess;
const spy = vi
.spyOn(providerProcess, "spawnOwnedProviderProcess")
.mockImplementation(async (options) => {
const launched = await nativeSpawn(options);
ownedLaunch = launched;
resolveSpawned();
return launched;
});
try {
const stopProcess: typeof incompleteStop =
stopMode === "is uncertain"
? incompleteStop
: async () => ({ status: "stopped" });
const manager = createManager(
{ REA_RIZIN_COMMAND: provider },
{ stopProcess },
);
const abort = new AbortController();
const starting = manager.start(
{
path: "/tmp/cancel-start",
inferior_tty: await rizinTestInferiorTty(),
},
abort.signal,
);
await spawned;
abort.abort();
const result = await starting;
expect(result.ok).toBe(false);
if (!result.ok && stopMode === "is uncertain")
expect(result.error.message).toContain("injected stop uncertainty");
await cleanup();
if (ownedLaunch === undefined)
throw new Error("Fixture launch was not observed");
expect(
ownedLaunch.process.exitCode !== null ||
ownedLaunch.process.signalCode !== null,
"harness returned before fixture child exit",
).toBe(true);
} finally {
spy.mockRestore();
// A failing regression must also reap its observed, directly owned child.
if (
ownedLaunch !== undefined &&
ownedLaunch.process.exitCode === null &&
ownedLaunch.process.signalCode === null
) {
const safety = new providerProcess.ProviderProcessSupervisor(
{ ...ownedLaunch, ownsProcessLifetime: true },
{ captureStdout: false, maxDiagnosticBytes: 0 },
);
expect((await safety.stop()).status).not.toBe("incomplete");
expect(safety.observedExit()).toBeDefined();
}
}
},
);
it.skipIf(process.platform === "win32")(
"retains owned fixture processes and files until a failed cleanup can complete",
async () => {
let failCleanup = true;
const fixture = createRizinCoverageHarness({
stopOwnedProcess: async (supervisor) =>
failCleanup
? { status: "incomplete", reason: "injected fixture stop failure" }
: supervisor.stop(),
});
const provider = await fixture.makeProvider(
"process.stdin.resume(); setInterval(() => {}, 1000);",
);
const manager = createRizinTestManager(
{ REA_RIZIN_COMMAND: provider },
{
stopProcess: incompleteStop,
spawnProcess: async (options) => {
const launched = await spawnOwnedProviderProcess(options);
resolveSpawned();
return launched;
},
},
);
track(manager);
const abort = new AbortController();
const starting = manager.start(
{ path: "/tmp/cancel-start", inferior_tty: await rizinTestInferiorTty() },
abort.signal,
);
await spawned;
abort.abort();
const result = await starting;
expect(result.ok).toBe(false);
if (!result.ok)
expect(result.error.message).toContain("injected stop uncertainty");
const launched = await fixture.spawnOwnedProcess({
command: provider,
arguments: [],
runId: randomUUID(),
stdin: "pipe",
});
try {
await expect(fixture.cleanup()).rejects.toMatchObject({
message: "Fake Rizin fixture cleanup failed",
errors: [
expect.objectContaining({
message: expect.stringContaining("injected fixture stop failure"),
}),
],
});
expect(launched.process.exitCode).toBeNull();
expect(launched.process.signalCode).toBeNull();
await access(provider);
const nextProvider = await fixture.makeProvider("process.exit(0);");
failCleanup = false;
await fixture.cleanup();
expect(
launched.process.exitCode !== null ||
launched.process.signalCode !== null,
).toBe(true);
await expect(access(provider)).rejects.toMatchObject({ code: "ENOENT" });
await expect(access(nextProvider)).rejects.toMatchObject({
code: "ENOENT",
});
} finally {
failCleanup = false;
try {
await fixture.cleanup();
} finally {
if (
launched.process.exitCode === null &&
launched.process.signalCode === null
) {
const safety = new providerProcess.ProviderProcessSupervisor(
{ ...launched, ownsProcessLifetime: true },
{ captureStdout: false, maxDiagnosticBytes: 0 },
);
expect((await safety.stop()).status).not.toBe("incomplete");
expect(safety.observedExit()).toBeDefined();
}
}
}
},
);
@@ -1,9 +1,5 @@
import { afterEach, expect, it } from "vitest";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import {
createRizinTestManager,
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";
import {
createRizinCoverageHarness,
abortOnCheck,
@@ -11,7 +7,7 @@ import {
incompleteStop,
} from "./RizinDebugSessionCoverage.fixtures.js";
const { cleanup, makeProvider, start, startInjected, track } =
const { cleanup, makeProvider, start, startInjected, createManager } =
createRizinCoverageHarness();
afterEach(cleanup);
@@ -88,7 +84,7 @@ it.skipIf(process.platform === "win32")(
async () => {
for (const spawnError of [new Error("injected spawn failure"), "opaque"]) {
const injected = createInjectedProcess({ spawnError });
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -102,7 +98,7 @@ it.skipIf(process.platform === "win32")(
const injected = createInjectedProcess({
startup: (control) => control.closeStdin(),
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -124,7 +120,7 @@ it.skipIf(process.platform === "win32")(
beforeSpawnReturns: () => abort.abort(),
spawnError: new Error("spawn interrupted"),
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -147,7 +143,7 @@ it.skipIf(process.platform === "win32")(
abort.abort();
},
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -166,7 +162,7 @@ it.skipIf(process.platform === "win32")(
const injected = createInjectedProcess({
beforeSpawnReturns: () => abort.abort(),
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -1,17 +1,13 @@
import { afterEach, expect, it, vi } from "vitest";
import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import {
createRizinTestManager,
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";
import {
createRizinCoverageHarness,
abortOnCheck,
createInjectedProcess,
} from "./RizinDebugSessionCoverage.fixtures.js";
const { cleanup, startInjected, track } = createRizinCoverageHarness();
const { cleanup, startInjected, createManager } = createRizinCoverageHarness();
afterEach(cleanup);
it.skipIf(process.platform === "win32")(
@@ -1,17 +1,13 @@
import { afterEach, expect, it } from "vitest";
import { evidenceSchema } from "../../../src/domain/evidence.js";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import {
createRizinTestManager,
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";
import {
createRizinCoverageHarness,
createInjectedProcess,
incompleteStop,
} from "./RizinDebugSessionCoverage.fixtures.js";
const { cleanup, makeProvider, start, startInjected, track } =
const { cleanup, makeProvider, start, startInjected, createManager } =
createRizinCoverageHarness();
afterEach(cleanup);