feat: add multi-engine regex flavour support
This commit is contained in:
172
tests/conformance/cpp.conformance.test.ts
Normal file
172
tests/conformance/cpp.conformance.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
CppEngineAdapter,
|
||||
CPP_ENGINE_VERSION,
|
||||
CPP_FLAVOUR_VERSION,
|
||||
} from "../../src/regex/execution/adapters/cpp/CppEngineAdapter";
|
||||
import type { CppRegexModuleFactory } from "../../src/regex/execution/adapters/cpp/cpp-module";
|
||||
import type {
|
||||
RegexExecutionRequest,
|
||||
RegexReplacementRequest,
|
||||
} from "../../src/regex/model/match";
|
||||
|
||||
const pack = path.resolve("public/engines/cpp");
|
||||
let adapter: CppEngineAdapter;
|
||||
|
||||
function request(
|
||||
pattern: string,
|
||||
subject: string,
|
||||
overrides: Partial<RegexExecutionRequest> = {},
|
||||
): RegexExecutionRequest {
|
||||
return {
|
||||
flavour: "cpp",
|
||||
flavourVersion: CPP_FLAVOUR_VERSION,
|
||||
pattern,
|
||||
flags: ["g"],
|
||||
options: { grammar: "ECMAScript" },
|
||||
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 imported = (await import(
|
||||
`${pathToFileURL(path.join(pack, "cpp-regex.mjs")).href}?conformance`
|
||||
)) as { default: CppRegexModuleFactory };
|
||||
const module = await imported.default({
|
||||
locateFile: (name) => path.join(pack, name),
|
||||
print: () => undefined,
|
||||
printErr: () => undefined,
|
||||
});
|
||||
adapter = new CppEngineAdapter(module, "Node.js conformance");
|
||||
await adapter.load();
|
||||
});
|
||||
|
||||
describe("C++ Emscripten libc++ std::wregex conformance", () => {
|
||||
it("loads the exact libc++ and compiler identity", async () => {
|
||||
await expect(adapter.load()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
flavour: "cpp",
|
||||
engineName: "C++ std::wregex (Emscripten libc++)",
|
||||
engineVersion: CPP_ENGINE_VERSION,
|
||||
runtimeVersion: expect.stringContaining(
|
||||
"32-bit wchar_t code-point bridge",
|
||||
),
|
||||
offsetUnit: "code-point",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns code-point-native ranges without losing editor UTF-16 offsets", async () => {
|
||||
const result = await adapter.execute(request("(\\w+)", "😀 one two"));
|
||||
|
||||
expect(result.accepted).toBe(true);
|
||||
expect(
|
||||
result.matches.map(({ nativeRange, range, value }) => ({
|
||||
nativeRange,
|
||||
range,
|
||||
value,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
nativeRange: { start: 2, end: 5, unit: "code-point" },
|
||||
range: { startUtf16: 3, endUtf16: 6 },
|
||||
value: "one",
|
||||
},
|
||||
{
|
||||
nativeRange: { start: 6, end: 9, unit: "code-point" },
|
||||
range: { startUtf16: 7, endUtf16: 10 },
|
||||
value: "two",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses native std::regex replacement and grammar selection", async () => {
|
||||
const replaced = await adapter.replace(
|
||||
replacement("(\\w+)", "one two", "<$1>"),
|
||||
);
|
||||
expect(replaced.execution.accepted).toBe(true);
|
||||
expect(replaced.output).toBe("<one> <two>");
|
||||
|
||||
const basic = await adapter.execute(
|
||||
request("\\(ab\\)", "xxabyy", {
|
||||
options: { grammar: "basic" },
|
||||
}),
|
||||
);
|
||||
expect(basic.matches[0]?.value).toBe("ab");
|
||||
});
|
||||
|
||||
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("reports syntax that libc++ std::regex does not accept", async () => {
|
||||
const result = await adapter.execute(request("(?<name>a)", "a"));
|
||||
expect(result.accepted).toBe(false);
|
||||
expect(result.diagnostics[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
source: "execution-engine",
|
||||
severity: "error",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ships a fixed bridge module without dynamic source evaluation", async () => {
|
||||
const module = await readFile(path.join(pack, "cpp-regex.mjs"), "utf8");
|
||||
expect(module).not.toContain("new Function");
|
||||
expect(module).not.toMatch(/\beval\s*\(/u);
|
||||
|
||||
const metadata = JSON.parse(
|
||||
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
|
||||
) as {
|
||||
bridge: { dynamicExecutionDisabled: boolean };
|
||||
toolchain: { version: string; commitSha1: string };
|
||||
};
|
||||
expect(metadata.bridge.dynamicExecutionDisabled).toBe(true);
|
||||
expect(metadata.toolchain).toEqual(
|
||||
expect.objectContaining({
|
||||
version: "6.0.4",
|
||||
commitSha1: "fe5be6afdff43ad58860d821fcc8572a23f92d19",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user