173 lines
5.1 KiB
TypeScript
173 lines
5.1 KiB
TypeScript
// @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",
|
|
}),
|
|
);
|
|
});
|
|
});
|