166 lines
6.4 KiB
Python
166 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Audit catalog, mapping, exported shims, and the downstream compile fixture."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import re
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
import generate_api_shims as shims
|
|
import generate_rust_mapping as mapping
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
REPORT = ROOT / "api/API-COVERAGE.md"
|
|
FIXTURE = ROOT / "tests/api-compile/src/lib.rs"
|
|
FORBIDDEN = {
|
|
"ShimValue": r"\bShimValue\b",
|
|
"dynamic argument bag": r"\b(?:DynamicArgumentBag|ArgumentBag)\b",
|
|
"invented variadic": r"\b(?:Variadic|VarArgs)\b|\.\.\.",
|
|
"erased Any argument vector": r"Vec\s*<\s*Box\s*<\s*dyn\s+(?:std::any::)?Any",
|
|
}
|
|
|
|
|
|
def require_exact(label: str, expected: set[str], actual: Counter[str]) -> None:
|
|
actual_ids = set(actual)
|
|
missing = expected - actual_ids
|
|
stale = actual_ids - expected
|
|
duplicates = sorted(item for item, count in actual.items() if count != 1)
|
|
if missing or stale or duplicates:
|
|
raise ValueError(
|
|
f"{label}: missing={len(missing)}, stale={len(stale)}, duplicates={len(duplicates)}"
|
|
)
|
|
|
|
|
|
def matches(paths: list[Path], pattern: str) -> Counter[str]:
|
|
found: Counter[str] = Counter()
|
|
regex = re.compile(pattern, re.MULTILINE)
|
|
for path in paths:
|
|
found.update(regex.findall(path.read_text()))
|
|
return found
|
|
|
|
|
|
def audit() -> str:
|
|
catalog = json.loads(mapping.CATALOG.read_text())
|
|
assemblies = {assembly["identity"]["name"]: assembly for assembly in catalog["assemblies"]}
|
|
catalog_types = {
|
|
item["doc_id"]
|
|
for assembly in catalog["assemblies"]
|
|
for item in assembly["types"]
|
|
}
|
|
catalog_members = {
|
|
member["doc_id"]
|
|
for assembly in catalog["assemblies"]
|
|
for item in assembly["types"]
|
|
for member in item["members"]
|
|
}
|
|
external_types = {item["doc_id"] for item in catalog["external_types"]}
|
|
support_types = {f"T:{name}" for name in mapping.SUPPORT_TYPES}
|
|
|
|
type_rows = list(csv.DictReader(mapping.TYPE_LEDGER.open(), delimiter="\t"))
|
|
member_rows = list(csv.DictReader(mapping.MEMBER_LEDGER.open(), delimiter="\t"))
|
|
require_exact(
|
|
"type mapping",
|
|
catalog_types | external_types | support_types,
|
|
Counter(row["csharp_type_id"] for row in type_rows),
|
|
)
|
|
require_exact(
|
|
"member mapping",
|
|
catalog_members,
|
|
Counter(row["csharp_id"] for row in member_rows),
|
|
)
|
|
duplicate_destinations = [
|
|
item
|
|
for item, count in Counter(row["rust_item_path"] for row in member_rows).items()
|
|
if count != 1
|
|
]
|
|
if duplicate_destinations:
|
|
raise ValueError(f"duplicate Rust member destinations: {len(duplicate_destinations)}")
|
|
|
|
generated = [shims.MAIN_TARGET, *shims.TARGETS.values()]
|
|
require_exact(
|
|
"exported types",
|
|
catalog_types,
|
|
matches(generated, r"^\s*/// C# type: `(T:.+)`\.$"),
|
|
)
|
|
require_exact(
|
|
"exported members",
|
|
catalog_members,
|
|
matches(generated, r"^\s*/// C# member: `(.+)`\.$"),
|
|
)
|
|
require_exact(
|
|
"fixture types",
|
|
catalog_types,
|
|
matches([FIXTURE], r"^\s*// C# type: `(T:.+)`\.$"),
|
|
)
|
|
require_exact(
|
|
"fixture members",
|
|
catalog_members,
|
|
matches([FIXTURE], r"^\s*// C# member: `(.+)`\.$"),
|
|
)
|
|
fixture_text = FIXTURE.read_text()
|
|
if len(re.findall(r"^fn type_", fixture_text, re.MULTILINE)) != len(catalog_types):
|
|
raise ValueError("fixture type probe count does not match the catalog")
|
|
if len(re.findall(r"^fn member_", fixture_text, re.MULTILINE)) != len(catalog_members):
|
|
raise ValueError("fixture member probe count does not match the catalog")
|
|
|
|
scan_text = "\n".join(
|
|
path.read_text() for path in [*generated, FIXTURE, mapping.MEMBER_LEDGER]
|
|
)
|
|
forbidden_hits = [name for name, pattern in FORBIDDEN.items() if re.search(pattern, scan_text)]
|
|
if forbidden_hits:
|
|
raise ValueError("forbidden erased shim patterns: " + ", ".join(forbidden_hits))
|
|
mapping.validate_generated_shims()
|
|
|
|
lines = [
|
|
"# Complete API gate coverage",
|
|
"",
|
|
"Generated by `python3 tools/check_api_coverage.py --write`; do not edit by hand.",
|
|
"",
|
|
"| Assembly | Catalog types | Mapped/exported/fixture types | Catalog members | Mapped/exported/fixture members |",
|
|
"|---|---:|---:|---:|---:|",
|
|
]
|
|
for name, assembly in assemblies.items():
|
|
type_count = len(assembly["types"])
|
|
member_count = sum(len(item["members"]) for item in assembly["types"])
|
|
lines.append(
|
|
f"| `{name}` | {type_count:,} | {type_count:,} | {member_count:,} | {member_count:,} |"
|
|
)
|
|
lines += [
|
|
f"| **Total** | **{len(catalog_types):,}** | **{len(catalog_types):,}** | **{len(catalog_members):,}** | **{len(catalog_members):,}** |",
|
|
"",
|
|
"## Exclusions and boundary records",
|
|
"",
|
|
"- Public catalog type exclusions: **0**.",
|
|
"- Public catalog member exclusions: **0**.",
|
|
f"- External signature types: **{len(external_types):,}** mapped to core/std, adopted cross-platform crates, or project-owned boundary types; their external member APIs are intentionally not copied.",
|
|
f"- Referenced non-public support traits: **{len(support_types):,}** mapped because public inheritance signatures require them.",
|
|
f"- Intentional delegate APM differences: **{sum(row['status'] == 'intentional_difference' for row in member_rows):,}**; every replacement is exported and compiled by the fixture.",
|
|
"- Forbidden erased shim patterns: **0** (`ShimValue`, dynamic argument bags, invented variadics, or `Vec<Box<dyn Any>>` argument bags).",
|
|
"",
|
|
"The fixture is a standalone Rust workspace with path dependencies only. Building it requires neither .NET nor compiled LibreMetaverse assemblies.",
|
|
"",
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--write", action="store_true")
|
|
args = parser.parse_args()
|
|
report = audit()
|
|
if args.write:
|
|
REPORT.write_text(report)
|
|
print(f"generated {REPORT.relative_to(ROOT)}")
|
|
elif not REPORT.exists() or REPORT.read_text() != report:
|
|
raise SystemExit(f"stale coverage report: {REPORT.relative_to(ROOT)}")
|
|
else:
|
|
print("API coverage matches the catalog, mappings, exports, and fixture")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|