mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
test: add source-built Hopper conformance fixtures
This commit is contained in:
@@ -63,6 +63,8 @@
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"verify:hopper": "npm run build && node scripts/verify-real-hopper.mjs",
|
||||
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",
|
||||
"verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
|
||||
"verify:package": "npm run build && node scripts/verify-package.mjs",
|
||||
"config:print": "node scripts/print-mcp-config.mjs",
|
||||
"docs:generate": "typedoc",
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Source-built conformance fixtures
|
||||
|
||||
These sources are deliberately small semantic oracles for REA providers. Run
|
||||
`npm run verify:fixtures` on macOS to compile them into the ignored
|
||||
`build/conformance/` directory, emit a hash-and-toolchain manifest, and verify
|
||||
their Mach-O symbols and strings with native command-line tools.
|
||||
|
||||
Only source files are versioned. Generated binaries, generated large-fixture
|
||||
source, debug information, and manifests must remain under `build/`.
|
||||
|
||||
The C fixture fixes a known call chain and strings. The version pair fixes an
|
||||
added symbol, changed string, and changed call relationship. The generated
|
||||
large fixture contains 1,205 uniquely named functions and strings so provider
|
||||
pagination can be checked at boundaries of 500, 1,000, and 1,205. Objective-C,
|
||||
Swift, and N-API-like fixtures provide metadata-oriented inputs without package
|
||||
or network dependencies.
|
||||
@@ -0,0 +1,19 @@
|
||||
#include <stdio.h>
|
||||
|
||||
volatile int rea_c_global = 7;
|
||||
|
||||
__attribute__((noinline, used)) int rea_leaf(int value) {
|
||||
puts("REA_C_LEAF");
|
||||
return value + rea_c_global;
|
||||
}
|
||||
|
||||
__attribute__((noinline, used)) int rea_branch(int value) {
|
||||
return value > 3 ? rea_leaf(value) : rea_leaf(-value);
|
||||
}
|
||||
|
||||
__attribute__((noinline, used)) int rea_entry(void) {
|
||||
puts("REA_C_ENTRY");
|
||||
return rea_branch(5);
|
||||
}
|
||||
|
||||
int main(void) { return rea_entry() == 12 ? 0 : 1; }
|
||||
@@ -0,0 +1,17 @@
|
||||
typedef void *napi_env;
|
||||
typedef void *napi_value;
|
||||
typedef napi_value (*napi_callback)(napi_env, void *);
|
||||
extern int napi_create_function(napi_env, const char *, unsigned long,
|
||||
napi_callback, void *, napi_value *);
|
||||
extern int napi_set_named_property(napi_env, napi_value, const char *, napi_value);
|
||||
static napi_value rea_probe(napi_env env, void *info) { return (napi_value)info; }
|
||||
__attribute__((visibility("default"))) napi_value napi_register_module_v1(
|
||||
napi_env env, napi_value exports) {
|
||||
const char *names[] = {"reaProbeOne", "reaProbeTwo", "reaProbeThree"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
napi_value function = 0;
|
||||
napi_create_function(env, names[i], 0, rea_probe, 0, &function);
|
||||
napi_set_named_property(env, exports, names[i], function);
|
||||
}
|
||||
return exports;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
@protocol REAWidgetDelegate <NSObject>
|
||||
- (void)widgetDidFinish;
|
||||
@end
|
||||
@interface REAWidget : NSObject
|
||||
@property(nonatomic, weak) id<REAWidgetDelegate> delegate;
|
||||
- (BOOL)performAction:(NSString *)action error:(NSError **)error;
|
||||
@end
|
||||
@implementation REAWidget
|
||||
- (BOOL)performAction:(NSString *)action error:(NSError **)error {
|
||||
NSLog(@"REA_OBJC_ACTION:%@", action);
|
||||
return action.length > 0;
|
||||
}
|
||||
@end
|
||||
int main(void) {
|
||||
@autoreleasepool {
|
||||
Protocol *protocol = @protocol(REAWidgetDelegate);
|
||||
NSLog(@"%@", NSStringFromProtocol(protocol));
|
||||
return [[[REAWidget alloc] init] performAction:@"run" error:0] ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import Darwin
|
||||
|
||||
protocol REAProtocol { func execute() -> Int }
|
||||
struct REARecord { let value: Int }
|
||||
enum REAState { case ready, finished }
|
||||
final class REAService: REAProtocol {
|
||||
@inline(never) func execute() -> Int { print("REA_SWIFT_EXECUTE"); return 42 }
|
||||
}
|
||||
extension REARecord { @inline(never) func doubled() -> Int { value * 2 } }
|
||||
let service = REAService()
|
||||
exit(service.execute() == 42 ? 0 : 1)
|
||||
@@ -0,0 +1,9 @@
|
||||
#include <stdio.h>
|
||||
__attribute__((noinline, used)) int rea_version_leaf(void) {
|
||||
puts("REA_VERSION_ONE");
|
||||
return 1;
|
||||
}
|
||||
__attribute__((noinline, used)) int rea_version_entry(void) {
|
||||
return rea_version_leaf();
|
||||
}
|
||||
int main(void) { return rea_version_entry() == 1 ? 0 : 1; }
|
||||
@@ -0,0 +1,10 @@
|
||||
#include <stdio.h>
|
||||
__attribute__((noinline, used)) int rea_version_leaf(void) {
|
||||
puts("REA_VERSION_TWO");
|
||||
return 2;
|
||||
}
|
||||
__attribute__((noinline, used)) int rea_added(void) { return 40; }
|
||||
__attribute__((noinline, used)) int rea_version_entry(void) {
|
||||
return rea_version_leaf() + rea_added();
|
||||
}
|
||||
int main(void) { return rea_version_entry() == 42 ? 0 : 1; }
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
generateLargeFixture,
|
||||
LARGE_FIXTURE_COUNT,
|
||||
sha256,
|
||||
sourceDigest,
|
||||
} from "../scripts/lib/conformance-fixtures.mjs";
|
||||
|
||||
describe("source-built conformance fixtures", () => {
|
||||
it("generates a deterministic large pagination fixture", () => {
|
||||
const first = generateLargeFixture();
|
||||
const second = generateLargeFixture();
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(first.match(/int rea_page_/gu)).toHaveLength(LARGE_FIXTURE_COUNT);
|
||||
expect(first).toContain("rea_page_0000");
|
||||
expect(first).toContain("rea_page_1204");
|
||||
expect(() => generateLargeFixture(0)).toThrow(/positive integer/u);
|
||||
});
|
||||
|
||||
it("hashes source manifests independently of input ordering", () => {
|
||||
const left = [
|
||||
{ path: "b.c", content: "b" },
|
||||
{ path: "a.c", content: "a" },
|
||||
];
|
||||
const right = [...left].reverse();
|
||||
|
||||
expect(sourceDigest(left)).toBe(sourceDigest(right));
|
||||
expect(sourceDigest(left)).toMatch(/^[0-9a-f]{64}$/u);
|
||||
expect(sha256("fixture")).not.toBe(sha256("changed"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user