test: add source-built Hopper conformance fixtures
This commit is contained in:
1 parent
4f18e26ee5
commit
d648bcd2e4
13 files changed
+465
No files matched your search
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user