Translate Types and Utilities parity tests
Some checks failed
Rust API gates / api-gates (push) Has been cancelled
Some checks failed
Rust API gates / api-gates (push) Has been cancelled
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user