Files
regex-tools/tests/conformance/go.conformance.test.ts

261 lines
8.1 KiB
TypeScript

// @vitest-environment node
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
GO_ENGINE_VERSION,
GO_FLAVOUR_VERSION,
GoEngineAdapter,
} from "../../src/regex/execution/adapters/go/GoEngineAdapter";
import { GoWasmBridge } from "../../src/regex/execution/adapters/go/GoWasmBridge";
import type {
RegexExecutionRequest,
RegexReplacementRequest,
} from "../../src/regex/model/match";
const pack = path.resolve("public/engines/go");
let adapter: GoEngineAdapter;
function request(
pattern: string,
subject: string,
overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
return {
flavour: "go",
flavourVersion: GO_FLAVOUR_VERSION,
pattern,
flags: ["g"],
subject,
captureMetadata: [],
scanAll: false,
maximumMatches: 100,
maximumCaptureRows: 1_000,
...overrides,
};
}
function replacement(
pattern: string,
subject: string,
replacementText: string,
overrides: Partial<RegexReplacementRequest> = {},
): RegexReplacementRequest {
return {
...request(pattern, subject),
replacement: replacementText,
maximumOutputBytes: 1_024,
...overrides,
};
}
beforeAll(async () => {
const wasm = await WebAssembly.compile(
await readFile(path.join(pack, "go-regex.wasm")),
);
const bridge = new GoWasmBridge(
pathToFileURL(path.join(pack, "wasm_exec.mjs")),
wasm,
);
adapter = new GoEngineAdapter(bridge, "Node.js conformance");
await adapter.load();
}, 30_000);
afterAll(() => {
adapter?.terminate();
});
describe("Go 1.26.5 standard-library regexp conformance", () => {
it("loads the exact runtime and bridge self-test identity", async () => {
await expect(adapter.load()).resolves.toEqual(
expect.objectContaining({
flavour: "go",
engineName: "Go standard-library regexp",
engineVersion: GO_ENGINE_VERSION,
runtimeVersion: expect.stringContaining("go1.26.5 js/wasm"),
offsetUnit: "utf8-byte",
}),
);
});
it("returns native UTF-8 byte offsets and native named captures", async () => {
const result = await adapter.execute(
request("(?P<emoji>😀+)([A-Z]+)?", "x😀😀AB 😀"),
);
expect(result.accepted).toBe(true);
expect(
result.matches.map(({ nativeRange, range }) => ({ nativeRange, range })),
).toEqual([
{
nativeRange: { start: 1, end: 11, unit: "utf8-byte" },
range: { startUtf16: 1, endUtf16: 7 },
},
{
nativeRange: { start: 12, end: 16, unit: "utf8-byte" },
range: { startUtf16: 8, endUtf16: 10 },
},
]);
expect(result.matches[0]?.captures).toEqual([
expect.objectContaining({
groupNumber: 1,
groupName: "emoji",
value: "😀😀",
nativeRange: { start: 1, end: 9, unit: "utf8-byte" },
}),
expect.objectContaining({ groupNumber: 2, value: "AB" }),
]);
expect(result.matches[1]?.captures[1]).toEqual({
groupNumber: 2,
status: "did-not-participate",
});
});
it("uses Go's native empty-match suppression and RE2 boundary", async () => {
const empty = await adapter.execute(request("a*", "baaab"));
expect(empty.matches.map(({ nativeRange }) => nativeRange)).toEqual([
{ start: 0, end: 0, unit: "utf8-byte" },
{ start: 1, end: 4, unit: "utf8-byte" },
{ start: 5, end: 5, unit: "utf8-byte" },
]);
const unsupported = await adapter.execute(request("(?<=a)b", "ab"));
expect(unsupported.accepted).toBe(false);
expect(unsupported.diagnostics[0]).toEqual(
expect.objectContaining({ code: "compile-error" }),
);
});
it("maps i, m, s and U to native Go inline flags", async () => {
const cases = [
["a", "A", ["i"]],
["^b$", "a\nb\nc", ["m"]],
["a.b", "a\nb", ["s"]],
["a+?", "aaa", ["U"]],
] as const;
for (const [pattern, subject, flags] of cases) {
const result = await adapter.execute(
request(pattern, subject, { flags: [...flags] }),
);
expect(result.matches, `${flags.join("")}: ${pattern}`).toHaveLength(1);
}
const ungreedy = await adapter.execute(
request("a+", "aaa", { flags: ["U"] }),
);
expect(ungreedy.matches[0]?.value).toBe("a");
});
it("uses native ExpandString replacement name and longest-name rules", async () => {
const result = await adapter.replace(
replacement("(?P<word>[a-z]+)-(\\d+)", "a-1 b-22", "${word}[$2]/$1/$$"),
);
expect(result.execution.accepted).toBe(true);
expect(result.output).toBe("a[1]/a/$ b[22]/b/$");
const longest = await adapter.replace(replacement("(a)", "a", "$1x|${1}x"));
expect(longest.output).toBe("|ax");
});
it("bounds capture amplification and preserves the untouched partial suffix", async () => {
const bounded = await adapter.replace(
replacement("(a+)", "a".repeat(256 * 1024), "$1".repeat(4_096), {
maximumOutputBytes: 5,
}),
);
expect(bounded.execution.accepted).toBe(true);
expect(bounded.output).toBe("aaaaa");
expect(bounded.outputBytes).toBe(5);
expect(bounded.outputTruncated).toBe(true);
const partial = await adapter.replace(
replacement("a", "aaa", "x", { maximumMatches: 1 }),
);
expect(partial.execution.matches).toHaveLength(1);
expect(partial.execution.truncated).toBe(true);
expect(partial.output).toBe("xaa");
expect(partial.outputTruncated).toBe(false);
});
it("enforces match/capture/output bounds without splitting UTF-8", async () => {
const limited = await adapter.execute(
request("a", "aaa", { maximumMatches: 1 }),
);
expect(limited.matches).toHaveLength(1);
expect(limited.truncated).toBe(true);
const rows = await adapter.execute(
request("(a)(b)", "ab", { maximumCaptureRows: 1 }),
);
expect(rows.matches).toHaveLength(0);
expect(rows.truncated).toBe(true);
const output = await adapter.replace(
replacement(".", "abc", "😀", { maximumOutputBytes: 5 }),
);
expect(output.output).toBe("😀");
expect(output.outputBytes).toBe(4);
expect(output.outputTruncated).toBe(true);
});
it("rejects lone UTF-16 surrogates before entering Go", async () => {
await expect(adapter.execute(request(".", "\ud800"))).rejects.toThrow(
/unpaired UTF-16 surrogate/u,
);
});
it("accepts control-heavy subjects within the decoded 16 MiB contract", async () => {
const subject = "\u0000".repeat(3 * 1024 * 1024);
const result = await adapter.execute(
request("^\\x00+$", subject, {
flags: [],
maximumMatches: 1,
maximumCaptureRows: 1,
}),
);
expect(result.accepted).toBe(true);
expect(result.matches[0]?.nativeRange).toEqual({
start: 0,
end: subject.length,
unit: "utf8-byte",
});
});
it("ships verified metadata, licence and checksums", async () => {
const metadata = JSON.parse(
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
) as {
engineVersion: string;
source: { tag: string; commitSha1: string; license: string };
toolchain: { archiveSha256: string };
};
expect(metadata).toEqual(
expect.objectContaining({
engineVersion: "1.26.5",
source: expect.objectContaining({
tag: "go1.26.5",
commitSha1: "c19862e5f8415b4f24b189d065ed739517c548ba",
license: "BSD-3-Clause",
}),
toolchain: expect.objectContaining({
archiveSha256:
"5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053",
}),
}),
);
const checksums = await readFile(path.join(pack, "SHA256SUMS"), "utf8");
for (const line of checksums.trim().split("\n")) {
const [expected, name] = line.split(/ {2}/u);
const value = await readFile(path.join(pack, name ?? ""));
expect(createHash("sha256").update(value).digest("hex"), name).toBe(
expected,
);
}
expect(await readFile(path.join(pack, "LICENSE-Go.txt"), "utf8")).toContain(
"Redistribution and use",
);
});
});