// @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 { beforeAll, describe, expect, it } from "vitest"; import { JavaEngineAdapter, JAVA_ENGINE_IDENTITY, JAVA_ENGINE_VERSION, } from "../../src/regex/execution/adapters/java/JavaEngineAdapter"; import type { TeaVmJavaRegexModule } from "../../src/regex/execution/adapters/java/java-module"; import type { RegexExecutionRequest, RegexReplacementRequest, } from "../../src/regex/model/match"; const pack = path.resolve("public/engines/java"); let adapter: JavaEngineAdapter; function request( pattern: string, subject: string, overrides: Partial = {}, ): RegexExecutionRequest { return { flavour: "java", flavourVersion: JAVA_ENGINE_IDENTITY, pattern, flags: ["g"], subject, captureMetadata: [], scanAll: false, maximumMatches: 100, maximumCaptureRows: 1_000, ...overrides, }; } function replacement( pattern: string, subject: string, replacementText: string, overrides: Partial = {}, ): RegexReplacementRequest { return { ...request(pattern, subject), replacement: replacementText, maximumOutputBytes: 1_000, ...overrides, }; } beforeAll(async () => { const imported = (await import( `${pathToFileURL(path.join(pack, "java-regex.mjs")).href}?conformance` )) as unknown as TeaVmJavaRegexModule; adapter = new JavaEngineAdapter( imported, "TeaVM JavaScript ยท Node.js conformance ยท not OpenJDK", ); await adapter.load(); }); describe("pinned TeaVM 0.15.0 java.util.regex conformance", () => { it("loads the exact compatibility engine identity", async () => { await expect(adapter.load()).resolves.toEqual( expect.objectContaining({ engineName: "TeaVM java.util.regex", engineVersion: JAVA_ENGINE_VERSION, runtimeVersion: expect.stringContaining("not OpenJDK"), offsetUnit: "utf16", }), ); }); it("returns native UTF-16 offsets and named/numbered metadata", async () => { const result = await adapter.execute( request("(?๐Ÿ˜€)([A-Z]+)?", "๐Ÿ˜€AB ๐Ÿ˜€", { captureMetadata: [ { number: 1, name: "emoji", range: { startUtf16: 0, endUtf16: 12 }, repeated: false, }, { number: 2, name: "tail", range: { startUtf16: 12, endUtf16: 21 }, repeated: false, }, ], }), ); expect(result.accepted).toBe(true); expect(result.matches.map(({ range }) => range)).toEqual([ { startUtf16: 0, endUtf16: 4 }, { startUtf16: 5, endUtf16: 7 }, ]); expect(result.matches[0]?.captures).toEqual([ expect.objectContaining({ groupNumber: 1, groupName: "emoji", value: "๐Ÿ˜€", range: { startUtf16: 0, endUtf16: 2 }, }), expect.objectContaining({ groupNumber: 2, groupName: "tail", value: "AB", range: { startUtf16: 2, endUtf16: 4 }, }), ]); expect(result.matches[1]?.captures[1]).toEqual({ groupNumber: 2, groupName: "tail", status: "did-not-participate", }); }); it("supports TeaVM lookbehind, numeric backreferences and atomic groups", async () => { const lookbehind = await adapter.execute(request("(?<=a)(b)\\1", "abb ab")); expect(lookbehind.matches.map(({ value }) => value)).toEqual(["bb"]); const atomic = await adapter.execute(request("(?>a|ab)c", "abc")); expect(atomic.matches).toHaveLength(0); }); it("advances global zero-length matches in UTF-16 without looping", async () => { const result = await adapter.execute(request("(?=.)", "๐Ÿ˜€x")); expect(result.matches.map(({ range }) => range)).toEqual([ { startUtf16: 0, endUtf16: 0 }, { startUtf16: 1, endUtf16: 1 }, { startUtf16: 2, endUtf16: 2 }, ]); }); it("maps i, m, s, u and x to TeaVM Pattern flags", async () => { const cases = [ ["a", "A", ["i"]], ["รค", "ร„", ["i", "u"]], ["^b$", "a\nb\nc", ["m"]], ["a.b", "a\nb", ["s"]], ["a b # comment", "ab", ["x"]], ] 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 normalized = await adapter.execute( request("a", "A", { flags: ["i", "m", "s", "u", "x"] }), ); expect(normalized.flags.effectiveFlags).toBe("imsux"); }); it("maps d to UNIX_LINES and reports the honest U compatibility boundary", async () => { const ordinaryLines = await adapter.execute( request(".", "\r", { flags: [] }), ); const unixLines = await adapter.execute( request(".", "\r", { flags: ["d"] }), ); expect(ordinaryLines.matches).toHaveLength(0); expect(unixLines.matches).toHaveLength(1); const result = await adapter.execute( request("\\w+", "รฉ", { flags: ["d", "U"] }), ); expect(result.accepted).toBe(true); expect(result.matches).toHaveLength(0); expect(result.flags.userFlags).toBe("dU"); expect(result.flags.effectiveFlags).toBe("duU"); expect(result.flags.internallyAddedIndicesFlag).toBe(false); expect(result.diagnostics[0]).toEqual( expect.objectContaining({ code: "engine-compatibility", message: expect.stringContaining("does not implement OpenJDK"), }), ); }); it("uses TeaVM Matcher native single-digit $n replacement", async () => { const result = await adapter.replace(replacement("(a)", "a a", "<$1>")); expect(result.output).toBe(" "); expect(result.outputTruncated).toBe(false); expect(result.execution.matches).toHaveLength(2); const escaped = await adapter.replace(replacement("(a)", "a a", "\\$1")); expect(escaped.output).toBe("$1 $1"); }); it("rejects ${name}, which TeaVM 0.15.0 does not support", async () => { const unsupportedNamed = await adapter.replace( replacement("(?a)", "a", "${name}"), ); expect(unsupportedNamed.execution.accepted).toBe(false); expect(unsupportedNamed.execution.diagnostics[0]).toEqual( expect.objectContaining({ code: "replacement-error", message: expect.stringMatching(/IllegalArgumentException/u), }), ); }); it("returns compile diagnostics and enforces match/capture/output caps", async () => { const invalid = await adapter.execute(request("(", "x")); expect(invalid.accepted).toBe(false); expect(invalid.diagnostics[0]).toEqual( expect.objectContaining({ code: "compile-error", provenance: "reported", }), ); const matchLimited = await adapter.execute( request("a", "aaa", { maximumMatches: 1 }), ); expect(matchLimited.matches).toHaveLength(1); expect(matchLimited.truncated).toBe(true); const captureLimited = await adapter.execute( request("(a)(b)", "ab", { maximumCaptureRows: 1, captureMetadata: [ { number: 1, range: { startUtf16: 0, endUtf16: 3 }, repeated: false, }, { number: 2, range: { startUtf16: 3, endUtf16: 6 }, repeated: false, }, ], }), ); expect(captureLimited.matches).toHaveLength(0); expect(captureLimited.truncated).toBe(true); const outputLimited = await adapter.replace( replacement("a", "aaaa", "long", { maximumOutputBytes: 5 }), ); expect(outputLimited.output).toBe("longl"); expect(outputLimited.outputBytes).toBe(5); expect(outputLimited.outputTruncated).toBe(true); }); it("ships internally consistent source identity, licence and checksums", async () => { const metadata = JSON.parse( await readFile(path.join(pack, "engine-metadata.json"), "utf8"), ) as { engineName: string; engineVersion: string; semanticIdentity: string; source: { tag: string; commitSha1: string; license: string; }; }; expect(metadata).toEqual( expect.objectContaining({ engineName: "TeaVM java.util.regex", engineVersion: "0.15.0", semanticIdentity: expect.stringContaining("not OpenJDK"), source: expect.objectContaining({ tag: "0.15.0", commitSha1: "ee91b03e616c4b45401cd11fb0cd7eb0daf6649b", license: "Apache-2.0", }), }), ); const checksumText = await readFile(path.join(pack, "SHA256SUMS"), "utf8"); for (const line of checksumText.trim().split("\n")) { const [expected, name] = line.split(/ {2}/u); const contents = await readFile(path.join(pack, name ?? "")); expect(createHash("sha256").update(contents).digest("hex"), name).toBe( expected, ); } expect(await readFile(path.join(pack, "LICENSE.txt"), "utf8")).toContain( "Apache License", ); }); });