Add complete downstream API gate
Some checks failed
Rust API gates / api-gates (push) Has been cancelled

This commit is contained in:
2026-08-08 13:11:55 +02:00
parent 643762fd1d
commit c8a953996e
16 changed files with 183714 additions and 295 deletions

165
tools/check_api_coverage.py Normal file
View File

@@ -0,0 +1,165 @@
#!/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()