Add complete downstream API gate
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:
165
tools/check_api_coverage.py
Normal file
165
tools/check_api_coverage.py
Normal 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()
|
||||
Reference in New Issue
Block a user