// @vitest-environment node import path from "node:path"; import { pathToFileURL } from "node:url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { PythonEngineAdapter, PYODIDE_ENGINE_VERSION, PYTHON_ENGINE_VERSION, PYTHON_FLAVOUR_VERSION, } from "../../src/regex/execution/adapters/python/PythonEngineAdapter"; import type { PyodideModule, PyodideRuntime, } from "../../src/regex/execution/adapters/python/python-module"; import type { RegexExecutionRequest, RegexReplacementRequest, } from "../../src/regex/model/match"; const engineDirectory = path.resolve( process.cwd(), "public", "engines", "python", ); function request( overrides: Partial = {}, ): RegexExecutionRequest { return { flavour: "python", flavourVersion: PYTHON_FLAVOUR_VERSION, pattern: "a", flags: ["g"], subject: "a", captureMetadata: [], scanAll: false, maximumMatches: 100, maximumCaptureRows: 1_000, ...overrides, }; } describe("CPython re 3.14.2 conformance through self-hosted Pyodide", () => { let runtime: PyodideRuntime; let adapter: PythonEngineAdapter; beforeAll(async () => { const moduleUrl = pathToFileURL(path.join(engineDirectory, "pyodide.mjs")); const imported = (await import( /* @vite-ignore */ moduleUrl.href )) as PyodideModule; expect(imported.version).toBe(PYODIDE_ENGINE_VERSION); runtime = await imported.loadPyodide({ indexURL: `${engineDirectory}${path.sep}`, stdout: () => undefined, stderr: () => undefined, }); adapter = new PythonEngineAdapter(runtime, "Node conformance runtime"); await adapter.load(); }, 30_000); afterAll(() => { adapter?.terminate(); }); it("reports the exact engine, runtime and native offset identity", async () => { await expect(adapter.load()).resolves.toEqual( expect.objectContaining({ flavour: "python", engineName: "CPython re via Pyodide", engineVersion: PYTHON_ENGINE_VERSION, runtimeVersion: expect.stringContaining("Pyodide 314.0.3"), offsetUnit: "code-point", }), ); }); it("uses native Python named groups and preserves optional captures", async () => { const result = await adapter.execute( request({ pattern: "(?P[A-Za-z]+)(?:-(\\d+))?", subject: "abc-12 xyz", }), ); expect(result.accepted).toBe(true); expect(result.matches).toHaveLength(2); expect(result.matches[0]?.captures).toEqual([ expect.objectContaining({ groupNumber: 1, groupName: "word", value: "abc", }), expect.objectContaining({ groupNumber: 2, value: "12" }), ]); expect(result.matches[1]?.captures).toEqual([ expect.objectContaining({ groupNumber: 1, groupName: "word", value: "xyz", }), { groupNumber: 2, status: "did-not-participate" }, ]); }); it("normalizes astral and zero-length code-point offsets to UTF-16", async () => { const result = await adapter.execute( request({ pattern: "(?=.)", subject: "😀a", }), ); expect( result.matches.map((match) => ({ native: match.nativeRange, editor: match.range, status: match.valueStatus, })), ).toEqual([ { native: { start: 0, end: 0, unit: "code-point" }, editor: { startUtf16: 0, endUtf16: 0 }, status: "complete", }, { native: { start: 1, end: 1, unit: "code-point" }, editor: { startUtf16: 2, endUtf16: 2 }, status: "complete", }, ]); }); it("implements g, a, i, m, s and x with scan-all normalization", async () => { const combined = await adapter.execute( request({ pattern: "^ a . b $", flags: ["i", "m", "s", "x"], subject: "z\nA\nB\nq", scanAll: true, }), ); expect(combined.accepted).toBe(true); expect(combined.flags.effectiveFlags).toBe("gimsx"); expect(combined.flags.internallyAddedGlobalFlag).toBe(true); expect(combined.matches.map((match) => match.value)).toEqual(["A\nB"]); const ascii = await adapter.execute( request({ pattern: "\\w+", flags: ["g", "a"], subject: "é az", }), ); expect(ascii.matches.map((match) => match.value)).toEqual(["az"]); }); it("uses native match.expand syntax for named, numbered and zero-length replacement", async () => { const replacementRequest: RegexReplacementRequest = { ...request({ pattern: "(?P[a-z]+)-(\\d+)", subject: "a-1 b-22", }), replacement: "\\g[\\2]", maximumOutputBytes: 1_024, }; const replaced = await adapter.replace(replacementRequest); expect(replaced.execution.accepted).toBe(true); expect(replaced.output).toBe("a[1] b[22]"); expect(replaced.outputTruncated).toBe(false); const zeroLength = await adapter.replace({ ...request({ pattern: "(?=.)", subject: "😀a", }), replacement: "|", maximumOutputBytes: 1_024, }); expect(zeroLength.output).toBe("|😀|a"); }); it("bounds UTF-8 output without splitting an astral scalar", async () => { const result = await adapter.replace({ ...request({ pattern: ".", subject: "abc" }), replacement: "😀", maximumOutputBytes: 5, }); expect(result.execution.accepted).toBe(true); expect(result.output).toBe("😀"); expect(result.outputBytes).toBe(4); expect(result.outputTruncated).toBe(true); expect(result.truncated).toBe(true); }); it("reports native compile and replacement errors", async () => { const compileError = await adapter.execute(request({ pattern: "😀(" })); expect(compileError.accepted).toBe(false); expect(compileError.diagnostics[0]).toEqual( expect.objectContaining({ code: "compile-error", range: { startUtf16: 2, endUtf16: 2 }, }), ); const replacementError = await adapter.replace({ ...request({ pattern: "(?Pa)" }), replacement: "\\g", maximumOutputBytes: 1_024, }); expect(replacementError.execution.accepted).toBe(false); expect(replacementError.execution.diagnostics[0]?.code).toBe( "replacement-error", ); }); it("enforces authoritative match and capture-row collection caps", async () => { const matchCap = await adapter.execute( request({ pattern: "a", subject: "aaa", maximumMatches: 1, }), ); expect(matchCap.matches).toHaveLength(1); expect(matchCap.truncated).toBe(true); const rowCap = await adapter.execute( request({ pattern: "(a)(b)", subject: "ab", maximumCaptureRows: 1, }), ); expect(rowCap.matches).toHaveLength(0); expect(rowCap.truncated).toBe(true); }); });