refactor(verify): give the real-Ghidra and Kuna verifiers' roots to VerifierFixture (#11170)

This commit is contained in:
morluto authored and GitHub committed 2026-11-15 21:50:19 +08:00
1 parent 6a3d00f85b
commit 5e625e3139
6 files changed
+81 -81

No files matched your search

+8 -5
View File
@@ -7,14 +7,11 @@ import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
copyFile,
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
@@ -33,6 +30,7 @@ import {
completeVerifierRun,
assertVerifierCleanupComplete,
} from "./lib/verifier-run.mjs";
import { VerifierFixture } from "./lib/verifier-fixture.mjs";
if (process.platform === "win32")
throw new Error(
@@ -40,7 +38,12 @@ if (process.platform === "win32")
);
const run = createVerifierRun(),
exec = promisify(execFile);
const root = await mkdtemp(join(tmpdir(), "rea-ghidra-checkpoints-"));
// Ghidra children use the root as HOME and TMPDIR; a plain root admits them on every host.
const workspace = await VerifierFixture.open({
prefix: "rea-ghidra-checkpoints-",
private: false,
});
const root = workspace.path;
const runtime = join(root, "runtime"),
checkpointDirectory = join(root, "checkpoints");
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
@@ -474,7 +477,7 @@ int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf
}
if (cleanupVerified) {
try {
await rm(root, { recursive: true, force: true });
await workspace.release();
} catch (cause) {
report.cleanup_error = String(cause);
report.retained_workspace = root;
+14 -24
View File
@@ -4,28 +4,27 @@
// is intentionally synthetic; it is not a claim about Ghidra's analysis history.
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import {
VerifierFixture,
withVerifierFixture,
} from "./lib/verifier-fixture.mjs";
const installation = process.env.GHIDRA_INSTALL_DIR;
if (!installation || !process.env.JAVA_HOME)
throw new Error(
"Set GHIDRA_INSTALL_DIR and JAVA_HOME to caller-installed Ghidra and JDK 21.",
);
const commands = await createVerifierCommands();
let root;
let commandsClosed = false;
const targetHash = await withVerifierCleanup(async () => {
root = await mkdtemp(join(tmpdir(), "rea-function-bundle-"));
const workspace = await VerifierFixture.open({
prefix: "rea-function-bundle-",
private: false,
});
// Ghidra and the CLI run as owned commands that may use the root until they close.
workspace.releaseAfter(() => commands.close());
const targetHash = await withVerifierFixture(workspace, async () => {
const root = workspace.path;
const target = join(root, "fixture");
const projectDir = join(root, "project");
const scriptDir = join(root, "scripts");
@@ -172,16 +171,7 @@ public class SetHistory extends GhidraScript {
assert.equal(hash(await readFile(target)), targetHash);
assert.deepEqual(await tree(projectDir), before);
return targetHash;
}, [
async () => {
await commands.close();
commandsClosed = true;
},
async () => {
if (commandsClosed && root !== undefined)
await rm(root, { recursive: true, force: true });
},
]);
});
process.stdout.write(
`PASS Ghidra function bundles: complete, summary, no-overwrite; target and project unchanged (${targetHash})\n`,
);
+22 -24
View File
@@ -2,16 +2,8 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
mkdir,
mkdtemp,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { realpathSync } from "node:fs";
import { tmpdir } from "node:os";
import { isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
@@ -24,6 +16,10 @@ import { createAnalysisProfile } from "../dist/domain/analysisProfile.js";
import { createEvidenceBundle } from "../dist/domain/evidenceBundle.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import {
VerifierFixture,
withVerifierFixture,
} from "./lib/verifier-fixture.mjs";
import {
createVerifierMcpOwner,
VerifierStdioTransport,
@@ -56,10 +52,14 @@ export async function verifyProjectCoverage(run) {
new URL("../tests/conformance/ghidra/", import.meta.url),
);
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
const workspace = await mkdtemp(
join(tmpdir(), "rea-ghidra-project-coverage-"),
);
const fixture = await VerifierFixture.open({
prefix: "rea-ghidra-project-coverage-",
private: false,
});
const workspace = fixture.path;
const connections = createVerifierMcpOwner();
// The workspace is removed only after every MCP connection confirms.
fixture.releaseAfter(() => connections.close());
const target = join(workspace, "fixture");
const projects = join(workspace, "projects");
const env = {
@@ -105,7 +105,6 @@ export async function verifyProjectCoverage(run) {
);
return requireMcpOutput(reply, name);
}
let commandCleanupIncomplete = false;
const command = async (executable, args, timeoutMs) => {
try {
return await runOwnedCommand(
@@ -119,13 +118,19 @@ export async function verifyProjectCoverage(run) {
{ timeoutMs, diagnosticBytes: 16 * 1024 * 1024 },
);
} catch (cause) {
commandCleanupIncomplete =
// A command whose process group was not confirmed stopped may still
// use the workspace, so the fixture keeps it.
if (
cause instanceof OwnedCommandFailure &&
cause.cleanupOwner !== undefined;
cause.cleanupOwner !== undefined
)
fixture.retain(
`${executable} cleanup is incomplete; it may still use ${workspace}`,
);
throw cause;
}
};
const result = await withVerifierCleanup(async () => {
return await withVerifierFixture(fixture, async () => {
await mkdir(projects);
// REA_CC is the invoking developer's explicitly selected compiler, not target input.
await command(
@@ -400,14 +405,7 @@ export async function verifyProjectCoverage(run) {
provider: installation.providerVersion,
results: report,
};
}, [
async () => {
await connections.close();
if (!commandCleanupIncomplete)
await rm(workspace, { recursive: true, force: true });
},
]);
return result;
});
}
if (
+28 -11
View File
@@ -3,8 +3,7 @@
import { verifyProjectCoverage } from "./verify-real-ghidra-project-coverage.mjs";
import { execFile } from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { writeFile } from "node:fs/promises";
import { isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
@@ -33,6 +32,11 @@ import {
verifyInventoryOperations,
} from "./verify-real-ghidra-inventory.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import {
VerifierFixture,
onceConfirmed,
withVerifierFixture,
} from "./lib/verifier-fixture.mjs";
import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.mjs";
import { verifyGhidraRelocationSymbols } from "./lib/real-ghidra-relocation-symbols.mjs";
@@ -75,7 +79,12 @@ const expectedNativeTarget = nativeFixtureTarget(
process.platform,
process.arch,
);
const fixtureRoot = await mkdtemp(join(tmpdir(), "rea-ghidra-fixtures-"));
// Ghidra children read and write under the root; a plain root admits them on every host.
const fixtures = await VerifierFixture.open({
prefix: "rea-ghidra-fixtures-",
private: false,
});
const fixtureRoot = fixtures.path;
const sourcePath = fileURLToPath(
new URL("../tests/conformance/ghidra/inventory.c", import.meta.url),
);
@@ -123,7 +132,7 @@ for (const [command, role] of toolchain) {
);
}
}
try {
await withVerifierFixture(fixtures, async () => {
const common = ["-O0", "-g", "-fno-inline"];
if (expectedNativeTarget.format === "elf") common.push("-fno-pie", "-no-pie");
await exec(compiler, [...common, sourcePath, "-o", debugPath]);
@@ -351,9 +360,7 @@ try {
custom_target: custom === null ? null : summary(custom),
cleanup: "complete",
};
} finally {
await rm(fixtureRoot, { recursive: true, force: true });
}
});
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), ...report })}\n`,
);
@@ -521,6 +528,14 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) {
profileDigest: profile.value.profile.digest,
});
// The fixture root is removed only after this client confirms it closed;
// a failed close is retried by the fixture's release.
const closeClient = fixtures.releaseAfter(
onceConfirmed(async () => {
const closed = await client.close();
if (!closed.ok) throw closed.error;
}),
);
let runtimeCoordinates;
let primaryFailure;
try {
@@ -615,14 +630,16 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) {
primaryFailure = cause;
throw cause;
} finally {
const closed = await client.close();
if (!closed.ok)
try {
await closeClient();
} catch (closeFailure) {
throw primaryFailure === undefined
? closed.error
? closeFailure
: new AggregateError(
[primaryFailure, closed.error],
[primaryFailure, closeFailure],
"Ghidra verification and cleanup failed",
);
}
if (runtimeCoordinates !== undefined)
await assertCleanup(runtimeCoordinates);
}
+9 -12
View File
@@ -10,16 +10,7 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
mkdir,
mkdtemp,
readFile,
readdir,
rm,
stat,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs, promisify } from "node:util";
@@ -52,6 +43,7 @@ import {
completeVerifierRun,
createVerifierRun,
} from "./lib/verifier-run.mjs";
import { VerifierFixture } from "./lib/verifier-fixture.mjs";
const exec = promisify(execFile);
const { values } = parseArgs({
@@ -94,7 +86,12 @@ const resourceScope = {
}),
run: (operation) => inspectionResources.run(operation),
};
const workspace = await mkdtemp(join(tmpdir(), "rea-kuna-lane-"));
// Kuna children cache under the root; a plain root admits them on every host.
const workspaceFixture = await VerifierFixture.open({
prefix: "rea-kuna-lane-",
private: false,
});
const workspace = workspaceFixture.path;
const cacheRoot = join(workspace, "cache");
// A server retains its workspace until both provider and transport cleanup
// have positively completed, even if later lineage observation is empty.
@@ -844,7 +841,7 @@ try {
}
if (cleanupComplete)
try {
await rm(workspace, { recursive: true, force: true });
await workspaceFixture.release();
} catch (cause) {
cleanupComplete = false;
failures.push(cause);
@@ -407,11 +407,6 @@
"scripts/verify-pe-load-config.mjs": 1,
"scripts/verify-published-package.mjs": 1,
"scripts/verify-real-cutter.mjs": 1,
"scripts/verify-real-ghidra-checkpoints.mjs": 1,
"scripts/verify-real-ghidra-function-bundle.mjs": 1,
"scripts/verify-real-ghidra-project-coverage.mjs": 1,
"scripts/verify-real-ghidra.mjs": 1,
"scripts/verify-real-kuna.mjs": 1,
"scripts/verify-release-artifact.mjs": 1,
"scripts/verify-release-exposure.mjs": 1,
"scripts/verify-test-discovery.mjs": 1,