Files
meshcore-bot/tests/characterization/test_public_api.py
T
agessaman 40989a4085 test(characterization): fail when a public name under modules/ disappears
Codex caught three deleted public methods earlier and a later scan found twelve more, because nothing tested for them. The golden records every public module function, class and method; the test fails only on a removal, and accepts a name that still resolves through a base class or re-export.
2026-10-01 09:26:36 -07:00

64 lines
2.4 KiB
Python

"""Characterization: every public name under modules/ that local plugins could use.
Local plugins and services (local/) may call any public module function,
class or method, so a refactor must not remove one. New names are fine; the
golden file only fails on removals (regenerate with UPDATE_GOLDEN=1 when a
removal is intentional).
"""
import ast
import json
from pathlib import Path
from tests.characterization.golden_util import GOLDEN_DIR
MODULES = Path(__file__).resolve().parents[2] / "modules"
GOLDEN = GOLDEN_DIR / "public_api.json"
def _public_names() -> dict[str, list[str]]:
names: dict[str, list[str]] = {}
for path in sorted(MODULES.rglob("*.py")):
rel = path.relative_to(MODULES.parent).as_posix()
found = set()
for node in ast.parse(path.read_text(encoding="utf-8")).body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and not node.name.startswith("_"):
found.add(node.name)
elif isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
found.add(node.name)
for member in node.body:
if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef)) and not member.name.startswith("_"):
found.add(f"{node.name}.{member.name}")
if found:
names[rel] = sorted(found)
return names
def _resolves(module: str, name: str) -> bool:
import importlib
obj = importlib.import_module(module[:-3].replace("/", "."))
for part in name.split("."):
if not hasattr(obj, part):
return False
obj = getattr(obj, part)
return True
def test_no_public_name_was_removed():
import os
current = _public_names()
if os.environ.get("UPDATE_GOLDEN") == "1" or not GOLDEN.exists():
GOLDEN.write_text(json.dumps(current, indent=1, sort_keys=True) + "\n", encoding="utf-8")
return
recorded = json.loads(GOLDEN.read_text(encoding="utf-8"))
missing = []
for module, names in recorded.items():
defined = set(current.get(module, []))
for name in names:
# A method moved to a base class, or a function re-exported, still counts.
if name not in defined and not _resolves(module, name):
missing.append(f"{module}: {name}")
assert missing == [], "public names removed:\n " + "\n ".join(missing)