ladybird/Meta/import-v8-regexp-tests.py
aplefull b3bd8884ae Tests+Meta: Reimport V8 regexp tests
This commit fixes assertEquals, assertThrows/assertDoesNotThrow in
compatibility shim - we were failing some tests purely because of a
bad shim.

Two test files are moved from XFAIL_TESTS to SKIP_FILES:
1. es6/regexp-tostring.js: checks for V8-specific error message wording
2. es6/unicode-regexp-ignore-case-noi18n.js: tests V8's non-i18n build
    behavior, it doesn't case-fold non-ASCII characters, but we do it
    correctly.

The regexp-unicode-sets.js test is now included and passes.

All files that were previously in XFAIL_TESTS and SKIP_TESTS are now
passing, so both lists are cleared.
2026-05-26 10:00:22 +02:00

298 lines
8.3 KiB
Python

#!/usr/bin/env python3
"""Import V8 mjsunit regexp tests into Ladybird's test-js format.
Usage: python3 Meta/import-v8-regexp-tests.py /path/to/v8
This script imports V8's regexp test files that don't depend on V8-specific
native builtins, wrapping them in Ladybird's test()/expect() harness format
with a compatibility shim for V8's assertion functions.
"""
import os
import re
import shutil
import sys
V8_COMPAT_SHIM = """\
// V8 assertion compatibility shim for Ladybird's test-js harness
function assertEquals(expected, actual, msg) {
if (Array.isArray(expected) && Array.isArray(actual)) {
expect(actual).toEqual(expected);
} else if (expected instanceof RegExp && actual instanceof RegExp) {
expect(actual.source).toBe(expected.source);
expect(actual.flags).toBe(expected.flags);
} else if (expected !== null && typeof expected === "object" && actual !== null && typeof actual === "object") {
expect(actual).toEqual(expected);
} else {
expect(actual).toBe(expected);
}
}
function assertTrue(val, msg) {
expect(val).toBeTrue();
}
function assertFalse(val, msg) {
expect(val).toBeFalse();
}
function assertNull(val, msg) {
expect(val).toBeNull();
}
function assertNotNull(val, msg) {
expect(val).not.toBeNull();
}
function assertThrows(fn, type_opt, msg_opt) {
if (typeof fn === "string") {
expect(() => eval(fn)).toThrow();
} else {
expect(fn).toThrow();
}
}
function assertDoesNotThrow(fn, msg) {
if (typeof fn === "string") {
eval(fn);
} else {
fn();
}
}
function assertInstanceof(val, type, msg) {
expect(val instanceof type).toBeTrue();
}
function assertUnreachable(msg) {
expect().fail("unreachable" + (msg ? ": " + msg : ""));
}
function assertEarlyError(code) {
assertThrows(() => new Function(code));
}
function assertThrowsAtRuntime(code, type_opt) {
const f = new Function(code);
assertThrows(f, type_opt);
}
function assertArrayEquals(expected, actual) {
expect(actual).toEqual(expected);
}
"""
DEST_DIR = "Tests/LibJS/Runtime/3rdparty/v8"
# Files that use V8-specific native syntax (%Foo, --allow-natives-syntax)
# and cannot be imported as-is.
SKIP_PATTERNS = [
r"allow-natives-syntax",
r"%[A-Z]",
]
# Files to skip entirely (not importable at all).
SKIP_FILES = {
# Performance benchmarks, not correctness tests.
"ascii-regexp-subject.js",
"string-slices-regexp.js",
# Uses V8's non-standard /l (linear) flag in regex literals, which causes
# parse errors in other engines.
"regexp-14098.js",
"regexp-444637793.js",
# Tests V8-specific error message wording ("incompatible receiver"), we correctly throw but use different message.
"es6/regexp-tostring.js",
# Tests V8's non-i18n build behavior. Spec-compliant engines (including ours) behave differently - we correctly
# case-fold all cases.
"es6/unicode-regexp-ignore-case-noi18n.js",
# Multi-file dependency (lu-ui0..9 depend on testCodePointRange from lu-ui).
"harmony/regexp-property-lu-ui0.js",
"harmony/regexp-property-lu-ui1.js",
"harmony/regexp-property-lu-ui2.js",
"harmony/regexp-property-lu-ui3.js",
"harmony/regexp-property-lu-ui4.js",
"harmony/regexp-property-lu-ui5.js",
"harmony/regexp-property-lu-ui6.js",
"harmony/regexp-property-lu-ui7.js",
"harmony/regexp-property-lu-ui8.js",
"harmony/regexp-property-lu-ui9.js",
}
# Tests that crash or hang -- use test.skip() so they don't block the suite.
SKIP_TESTS = {}
# Tests that fail but should be tracked -- use test.xfail() so we notice
# when they start passing.
XFAIL_TESTS = {}
def should_skip(content):
for pattern in SKIP_PATTERNS:
if re.search(pattern, content):
return True
return False
def file_key(subdir, filename):
"""Get the key used in SKIP_FILES."""
if subdir:
return f"{subdir}/{filename}"
return filename
def test_name(subdir, filename):
"""Get the test name (no .js extension)."""
name = filename.replace(".js", "")
if subdir:
return f"{subdir}/{name}"
return name
def find_regexp_tests(v8_dir):
"""Find all regexp-related test files in V8's mjsunit directory."""
mjsunit = os.path.join(v8_dir, "test", "mjsunit")
files = []
# Top-level regexp files
for f in sorted(os.listdir(mjsunit)):
if f.endswith(".js") and "regexp" in f.lower():
files.append(("", os.path.join(mjsunit, f)))
# es6/ subdirectory
es6_dir = os.path.join(mjsunit, "es6")
if os.path.isdir(es6_dir):
for f in sorted(os.listdir(es6_dir)):
if f.endswith(".js") and ("regexp" in f.lower() or "unicode-regexp" in f.lower()):
files.append(("es6", os.path.join(es6_dir, f)))
# harmony/ subdirectory
harmony_dir = os.path.join(mjsunit, "harmony")
if os.path.isdir(harmony_dir):
for f in sorted(os.listdir(harmony_dir)):
if f.endswith(".js") and "regexp" in f.lower():
files.append(("harmony", os.path.join(harmony_dir, f)))
return files
def extract_copyright_header(content):
"""Extract the BSD copyright header from the file."""
lines = content.split("\n")
header_lines = []
for line in lines:
if line.startswith("//"):
header_lines.append(line)
else:
break
return "\n".join(header_lines)
def extract_body(content):
"""Extract the test body (everything after the copyright header and flags comment)."""
lines = content.split("\n")
body_start = 0
for i, line in enumerate(lines):
if line.startswith("//"):
continue
if line.strip() == "":
body_start = i + 1
continue
body_start = i
break
return "\n".join(lines[body_start:])
def convert_file(subdir, src_path):
"""Convert a V8 test file to Ladybird format."""
with open(src_path) as f:
content = f.read()
filename = os.path.basename(src_path)
if file_key(subdir, filename) in SKIP_FILES:
return None, "skip-file"
if should_skip(content):
return None, "v8-natives"
name = test_name(subdir, filename)
header = extract_copyright_header(content)
body = extract_body(content)
# Determine test wrapper
if name in SKIP_TESTS:
wrapper = "test.skip"
elif name in XFAIL_TESTS:
wrapper = "test.xfail"
else:
wrapper = "test"
# Build the converted file
result = header + "\n\n"
result += V8_COMPAT_SHIM + "\n"
result += f'{wrapper}("{name}", () => {{\n'
# Indent the body
for line in body.split("\n"):
if line.strip():
result += " " + line + "\n"
else:
result += "\n"
result += "});\n"
return result, wrapper
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} /path/to/v8", file=sys.stderr)
sys.exit(1)
v8_dir = sys.argv[1]
if not os.path.isdir(os.path.join(v8_dir, "test", "mjsunit")):
print(f"Error: {v8_dir}/test/mjsunit not found", file=sys.stderr)
sys.exit(1)
# Clean destination
if os.path.exists(DEST_DIR):
shutil.rmtree(DEST_DIR)
os.makedirs(DEST_DIR, exist_ok=True)
os.makedirs(os.path.join(DEST_DIR, "es6"), exist_ok=True)
os.makedirs(os.path.join(DEST_DIR, "harmony"), exist_ok=True)
files = find_regexp_tests(v8_dir)
counts = {"test": 0, "test.skip": 0, "test.xfail": 0, "skipped": 0}
for subdir, src_path in files:
converted, status = convert_file(subdir, src_path)
filename = os.path.basename(src_path)
key = file_key(subdir, filename)
if converted is None:
counts["skipped"] += 1
print(f" SKIP ({status}): {key}")
continue
if subdir:
dest_path = os.path.join(DEST_DIR, subdir, filename)
else:
dest_path = os.path.join(DEST_DIR, filename)
with open(dest_path, "w") as f:
f.write(converted)
counts[status] += 1
tag = "" if status == "test" else f" [{status}]"
rel = os.path.relpath(dest_path)
print(f" OK: {rel}{tag}")
print(
f"\nImported: {counts['test']} pass, {counts['test.xfail']} xfail, "
f"{counts['test.skip']} skip, {counts['skipped']} not imported"
)
if __name__ == "__main__":
main()