Translate Types and Utilities parity tests
Some checks failed
Rust API gates / api-gates (push) Has been cancelled

This commit is contained in:
2026-08-08 13:58:25 +02:00
parent 39ee61525a
commit 1c0dfdc35c
19 changed files with 3482 additions and 3318 deletions

View File

@@ -170,6 +170,12 @@ SUPPORT_TYPES = {
MEMBER_RETURN_OVERRIDES = {
"M:LibreMetaverse.Caps.Capabilities": "Vec<String>",
}
TYPE_USAGE_OVERRIDES = {
"LibreMetaverse.DictionaryChangeCallback": (
"std::sync::Arc<dyn Fn(libremetaverse::DictionaryEventAction, "
"libremetaverse_types::compat::DictionaryEntry) + Send + Sync>"
),
}
TYPE_NAME_OVERRIDES = {
"T:LibreMetaverse.LslTools.Error": "LslError",
}
@@ -337,7 +343,7 @@ def external_type_target(item: dict) -> tuple[str, str, str, str]:
output = arguments[-1] if arguments else "()"
return "core", "callback", f"Box<dyn Fn({', '.join(arguments[:-1])}) -> {output} + Send + Sync>", "language"
if base == "System.EventHandler":
return "libremetaverse-types", "compat", f"libremetaverse_types::compat::EventHandler<{arguments[-1] if arguments else '()'}>", "native_cross_platform_replacement"
return "std", "sync", f"std::sync::Arc<dyn Fn({arguments[-1] if arguments else '()'}) + Send + Sync>", "stdlib_or_adopted_crate"
if base in {"System.Tuple", "System.ValueTuple"}:
return "core", "tuple", "(" + ", ".join(arguments) + ("," if len(arguments) == 1 else "") + ")", "language"
if base == "System.Nullable" and arguments:
@@ -502,7 +508,7 @@ class Mapper:
parameters = ", ".join(mapped_arguments[:-1])
target = f"Box<dyn Fn({parameters}) -> {output} + Send + Sync>"
elif base == "System.EventHandler":
target = f"libremetaverse_types::compat::EventHandler<{mapped_arguments[-1] if mapped_arguments else '()'}>"
target = f"std::sync::Arc<dyn Fn({mapped_arguments[-1] if mapped_arguments else '()'}) + Send + Sync>"
elif base in {"System.Tuple", "System.ValueTuple"}:
target = "(" + ", ".join(mapped_arguments) + ("," if len(mapped_arguments) == 1 else "") + ")"
elif base == "System.Nullable" and mapped_arguments:
@@ -514,7 +520,7 @@ class Mapper:
if base in generics or ("." not in base and base.startswith("T")):
target = base
elif target is None:
target = self.resolved.get(base)
target = TYPE_USAGE_OVERRIDES.get(base, self.resolved.get(base))
if target is None:
raise ValueError(f"unresolved referenced type: {value} (base {base})")
if mapped_arguments and base not in {

View File

@@ -70,7 +70,7 @@ PARITY_MARKER_RE = re.compile(
re.MULTILINE,
)
RUST_TEST_RE = re.compile(r"(?:#\[[^\]]+\]\s*)*fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", re.MULTILINE)
EXPECTED_TESTS = 1295
EXPECTED_TESTS = 1289
PARITY_FILES = (
Path("tests/compat/tests/generated_parity.rs"),
Path("tests/PARITY.md"),
@@ -106,6 +106,50 @@ def source_files(root: Path) -> list[Path]:
return sorted(p for p in root.rglob("*.cs") if "obj" not in p.parts and "bin" not in p.parts)
def mask_csharp_comments(text: str) -> str:
"""Blank comments without shifting source offsets or touching string literals."""
masked = list(text)
index = 0
state = "code"
while index < len(text):
char = text[index]
following = text[index + 1] if index + 1 < len(text) else ""
if state == "code":
if char == '"':
state = "string"
elif char == "'":
state = "char"
elif char == "/" and following == "/":
masked[index] = masked[index + 1] = " "
index += 1
state = "line_comment"
elif char == "/" and following == "*":
masked[index] = masked[index + 1] = " "
index += 1
state = "block_comment"
elif state == "string":
if char == '"' and following == '"':
index += 1
elif char == '"' and (index == 0 or text[index - 1] != "\\"):
state = "code"
elif state == "char":
if char == "'" and (index == 0 or text[index - 1] != "\\"):
state = "code"
elif state == "line_comment":
if char in "\r\n":
state = "code"
else:
masked[index] = " "
elif char == "*" and following == "/":
masked[index] = masked[index + 1] = " "
index += 1
state = "code"
elif char not in "\r\n":
masked[index] = " "
index += 1
return "".join(masked)
def namespace_at(text: str, position: int) -> str:
matches = [m for m in NAMESPACE_RE.finditer(text) if m.start() <= position]
return matches[-1].group(1) if matches else ""
@@ -278,10 +322,11 @@ def helper_types(roots: tuple[Path, ...], upstream: Path) -> dict[str, str]:
for root in roots:
for path in source_files(root):
text = path.read_text(encoding="utf-8-sig")
if TEST_ATTR_RE.search(text):
searchable = mask_csharp_comments(text)
if TEST_ATTR_RE.search(searchable):
continue
relative = path.relative_to(upstream).as_posix()
for match in CLASS_RE.finditer(text):
for match in CLASS_RE.finditer(searchable):
helpers.setdefault(match.group(1), relative)
return helpers
@@ -293,29 +338,33 @@ def extract_tests(upstream: Path) -> list[dict[str, object]]:
for root in roots:
for path in source_files(root):
text = path.read_text(encoding="utf-8-sig")
searchable = mask_csharp_comments(text)
relative = path.relative_to(upstream).as_posix()
dependencies = sorted(
helper_path
for helper, helper_path in helpers.items()
if helper_path != relative and re.search(rf"\b{re.escape(helper)}\b", text)
if helper_path != relative and re.search(rf"\b{re.escape(helper)}\b", searchable)
)
attrs = list(TEST_ATTR_RE.finditer(text))
attrs = list(TEST_ATTR_RE.finditer(searchable))
for attr_index, attr in enumerate(attrs):
next_attr = attrs[attr_index + 1].start() if attr_index + 1 < len(attrs) else len(text)
method = METHOD_RE.search(text, attr.end(), next_attr + 1200)
next_attr = attrs[attr_index + 1].start() if attr_index + 1 < len(attrs) else len(searchable)
method = METHOD_RE.search(searchable, attr.end(), next_attr + 1200)
if method is None:
method = METHOD_RE.search(text, attr.end())
method = METHOD_RE.search(searchable, attr.end())
if method is None:
raise RuntimeError(f"No test method after {relative}:{text.count(chr(10), 0, attr.start()) + 1}")
attribute_block = searchable[searchable.rfind("}", 0, method.start()) + 1 : method.start()]
if attr.group(1) == "Test" and "[TestCase" in attribute_block:
continue
name = method.group(1)
class_match = enclosing_class_match(text, method.start())
class_match = enclosing_class_match(searchable, method.start())
fixture = class_match.group(1) if class_match else "UnknownFixture"
attr_text = " ".join(attr.group(0).split())
parameters = " ".join((attr.group(2) or "").split()) or None
line = text.count("\n", 0, attr.start()) + 1
category_text = text[attr.start() : method.start()]
category_text = searchable[attr.start() : method.start()]
if class_match:
category_text += attributes_before(text, class_match.start())
category_text += attributes_before(searchable, class_match.start())
categories = sorted(set(CATEGORY_RE.findall(category_text)))
tests.append(
{

View File

@@ -4,10 +4,37 @@ import unittest
from pathlib import Path
from unittest.mock import patch
from generate_surface import check_test_parity, extract_tests, generate_tests
from generate_surface import check_test_parity, extract_tests, generate_tests, mask_csharp_comments
class ParityGenerationTests(unittest.TestCase):
def test_comment_masking_preserves_offsets_and_ignores_fake_tests(self) -> None:
source = '''var url = "https://example.com/[Test]"; // [Test]\n/* [TestCase(1)] */\n[Test]\n'''
masked = mask_csharp_comments(source)
self.assertEqual(len(masked), len(source))
self.assertEqual(masked.count("\n"), source.count("\n"))
self.assertIn('"https://example.com/[Test]"', masked)
self.assertEqual(masked.count("[Test]"), 2)
with tempfile.TemporaryDirectory() as temporary:
upstream = Path(temporary)
tests = upstream / "LibreMetaverse.Tests"
tests.mkdir()
(upstream / "LibreMetaverse.Rendering.Tests").mkdir()
(tests / "Comments.cs").write_text(
"""public class Comments
{
// [Test]
// public void CommentedOut() {}
[Test]
[TestCase(1)]
[TestCase(2)]
public void Cases(int value) {}
}
"""
)
self.assertEqual([case["parameter_case"] for case in extract_tests(upstream)], ["1", "2"])
def test_reviewed_rust_test_is_preserved_and_excluded_from_placeholders(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)