test: add source-built Hopper conformance fixtures

This commit is contained in:
morluto committed 2026-07-12 19:32:29 +08:00
1 parent 4f18e26ee5
commit d648bcd2e4
13 files changed
+465

No files matched your search

+223
View File
@@ -0,0 +1,223 @@
import { execFile } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { arch, platform } from "node:os";
import { join, relative, resolve } from "node:path";
import { promisify } from "node:util";
import {
generateLargeFixture,
LARGE_FIXTURE_COUNT,
sha256,
sourceDigest,
} from "./lib/conformance-fixtures.mjs";
const exec = promisify(execFile);
const root = process.cwd();
const sourceRoot = join(root, "tests/conformance");
const outputRoot = resolve(
process.env.REA_CONFORMANCE_BUILD ?? join(root, "build/conformance"),
);
if (platform() !== "darwin")
throw new Error(
"Mach-O conformance fixtures require macOS and Xcode command-line tools",
);
await rm(outputRoot, { recursive: true, force: true });
await mkdir(outputRoot, { recursive: true });
const clang = (await exec("xcrun", ["--find", "clang"])).stdout.trim();
const clangVersion = (await exec(clang, ["--version"])).stdout.split("\n")[0];
const sdkPath = (await exec("xcrun", ["--show-sdk-path"])).stdout.trim();
const architecture = arch();
const common = [
"-arch",
architecture,
"-isysroot",
sdkPath,
"-O0",
"-g",
"-fno-inline",
];
const fixtures = [];
await build(
"c",
["c/fixture.c"],
[...common, join(sourceRoot, "c/fixture.c"), "-o", join(outputRoot, "c")],
{
expectations: {
symbols: ["_rea_entry", "_rea_branch", "_rea_leaf", "_rea_c_global"],
strings: ["REA_C_ENTRY", "REA_C_LEAF"],
},
},
);
await build(
"version-v1",
["versions/v1.c"],
[
...common,
join(sourceRoot, "versions/v1.c"),
"-o",
join(outputRoot, "version-v1"),
],
{
expectations: {
symbols: ["_rea_version_entry", "_rea_version_leaf"],
strings: ["REA_VERSION_ONE"],
},
},
);
await build(
"version-v2",
["versions/v2.c"],
[
...common,
join(sourceRoot, "versions/v2.c"),
"-o",
join(outputRoot, "version-v2"),
],
{
expectations: {
symbols: ["_rea_version_entry", "_rea_version_leaf", "_rea_added"],
strings: ["REA_VERSION_TWO"],
},
},
);
const largeSource = generateLargeFixture();
const largePath = join(outputRoot, "large.c");
await writeFile(largePath, largeSource);
await build(
"large",
[{ path: "generated/large.c", content: largeSource }],
[...common, largePath, "-o", join(outputRoot, "large")],
{
expectations: {
symbolPrefix: "_rea_page_",
symbolCount: LARGE_FIXTURE_COUNT,
stringPrefix: "REA_PAGE_",
stringCount: LARGE_FIXTURE_COUNT,
},
},
);
await build(
"objc",
["objc/fixture.m"],
[
...common,
"-fobjc-arc",
join(sourceRoot, "objc/fixture.m"),
"-framework",
"Foundation",
"-o",
join(outputRoot, "objc"),
],
{
expectations: {
symbols: ["_OBJC_CLASS_$_REAWidget"],
strings: ["REAWidget", "REAWidgetDelegate", "performAction:error:"],
},
},
);
await build(
"napi",
["napi/fixture.c"],
[
...common,
"-bundle",
"-undefined",
"dynamic_lookup",
join(sourceRoot, "napi/fixture.c"),
"-o",
join(outputRoot, "fixture.node"),
],
{
expectations: {
symbols: ["_napi_register_module_v1"],
strings: ["reaProbeOne", "reaProbeTwo", "reaProbeThree"],
},
},
);
try {
const swiftc = (await exec("xcrun", ["--find", "swiftc"])).stdout.trim();
const swiftVersion = (await exec(swiftc, ["--version"])).stdout.split(
"\n",
)[0];
await build(
"swift",
["swift/fixture.swift"],
[
"-sdk",
sdkPath,
"-Onone",
"-g",
join(sourceRoot, "swift/fixture.swift"),
"-o",
join(outputRoot, "swift"),
],
{
expectations: {
strings: [
"REA_SWIFT_EXECUTE",
"REAService",
"REARecord",
"REAState",
"REAProtocol",
],
},
compiler: swiftc,
compilerVersion: swiftVersion,
},
);
} catch (cause) {
process.stderr.write(
`Skipping optional Swift fixture: ${cause instanceof Error ? cause.message : String(cause)}\n`,
);
}
const manifest = {
schemaVersion: 1,
platform: platform(),
architecture,
generatedAt: new Date(0).toISOString(),
fixtures,
};
await writeFile(
join(outputRoot, "manifest.json"),
`${JSON.stringify(manifest, null, 2)}\n`,
);
process.stdout.write(
`${JSON.stringify({ outputRoot, fixtureCount: fixtures.length })}\n`,
);
async function build(name, sources, args, options) {
const compiler = options.compiler ?? clang;
const compilerVersion = options.compilerVersion ?? clangVersion;
const normalized = await Promise.all(
sources.map(async (source) => {
if (typeof source !== "string") return source;
return {
path: source,
content: await readFile(join(sourceRoot, source), "utf8"),
};
}),
);
await exec(compiler, args, { maxBuffer: 10 * 1024 * 1024 });
const artifactPath = args.at(-1);
if (typeof artifactPath !== "string")
throw new Error(`Missing output for ${name}`);
const artifact = await readFile(artifactPath);
fixtures.push({
name,
artifact: relative(outputRoot, artifactPath),
artifactSha256: sha256(artifact),
sourceSha256: sourceDigest(normalized),
sources: normalized.map(({ path, content }) => ({
path,
sha256: sha256(content),
})),
compiler: { path: compiler, version: compilerVersion, arguments: args },
expectations: options.expectations,
});
}
+6
View File
@@ -0,0 +1,6 @@
export const LARGE_FIXTURE_COUNT: number;
export function sha256(value: string | Uint8Array): string;
export function generateLargeFixture(count?: number): string;
export function sourceDigest(
sources: readonly { readonly path: string; readonly content: string }[],
): string;
+30
View File
@@ -0,0 +1,30 @@
import { createHash } from "node:crypto";
export const LARGE_FIXTURE_COUNT = 1_205;
export const sha256 = (value) =>
createHash("sha256").update(value).digest("hex");
export const generateLargeFixture = (count = LARGE_FIXTURE_COUNT) => {
if (!Number.isInteger(count) || count < 1)
throw new Error("Fixture count must be a positive integer");
const declarations = Array.from(
{ length: count },
(_, index) =>
`__attribute__((noinline, used)) int rea_page_${String(index).padStart(4, "0")}(void) { puts("REA_PAGE_${String(index).padStart(4, "0")}"); return ${index}; }`,
);
return [
"#include <stdio.h>",
...declarations,
"int main(void) { return rea_page_0000(); }",
"",
].join("\n");
};
export const sourceDigest = (sources) => {
const canonical = [...sources]
.sort(({ path: left }, { path: right }) => left.localeCompare(right))
.map(({ path, content }) => `${path}\0${sha256(content)}\n`)
.join("");
return sha256(canonical);
};
+68
View File
@@ -0,0 +1,68 @@
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { sha256 } from "./lib/conformance-fixtures.mjs";
const exec = promisify(execFile);
const manifestPath = resolve(
process.argv[2] ?? "build/conformance/manifest.json",
);
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.fixtures))
throw new Error("Unsupported conformance manifest");
const root = dirname(manifestPath);
for (const fixture of manifest.fixtures) {
const artifactPath = join(root, fixture.artifact);
const artifact = await readFile(artifactPath);
if (sha256(artifact) !== fixture.artifactSha256)
throw new Error(`${fixture.name}: artifact hash mismatch`);
const fileOutput = (await exec("file", [artifactPath])).stdout;
if (!fileOutput.includes("Mach-O"))
throw new Error(`${fixture.name}: not Mach-O`);
const symbols = (
await exec("nm", ["-gU", artifactPath], { maxBuffer: 20 * 1024 * 1024 })
).stdout;
const strings = (
await exec("strings", [artifactPath], { maxBuffer: 20 * 1024 * 1024 })
).stdout;
for (const symbol of fixture.expectations.symbols ?? [])
if (!symbols.includes(symbol))
throw new Error(`${fixture.name}: missing symbol ${symbol}`);
for (const value of fixture.expectations.strings ?? [])
if (!strings.includes(value))
throw new Error(`${fixture.name}: missing string ${value}`);
verifyCount(
symbols,
fixture.expectations.symbolPrefix,
fixture.expectations.symbolCount,
fixture.name,
);
verifyCount(
strings,
fixture.expectations.stringPrefix,
fixture.expectations.stringCount,
fixture.name,
);
}
const v1 = manifest.fixtures.find(({ name }) => name === "version-v1");
const v2 = manifest.fixtures.find(({ name }) => name === "version-v2");
if (v1?.artifactSha256 === v2?.artifactSha256)
throw new Error("Version-pair artifacts unexpectedly have equal hashes");
process.stdout.write(
`${JSON.stringify({ verified: manifest.fixtures.length, manifestPath })}\n`,
);
function verifyCount(output, prefix, expected, fixture) {
if (prefix === undefined || expected === undefined) return;
const count = output
.split("\n")
.filter((line) => line.includes(prefix)).length;
if (count !== expected)
throw new Error(
`${fixture}: expected ${expected} ${prefix} records, received ${count}`,
);
}