Files
DemonGatanjieu-Anomalous_Mo…/tests/test_backend_names.py
T
DemonGatanjieuandClaude Opus 5.5 7b09909e5b test: publish the plugin's tests, one command to run them
tests/ (Python backend tests, .mjs frontend rule tests) is no longer
ignored. tools/run_tests.mjs runs the structure check, every
tests/*.mjs and the Python tests with $PYTHON, the portable
python_embeded or python. docs/guides/testing.md says how to run and
write them; README links it. Developer notes no longer point at 18 test
files that were removed earlier (AGENTS.md table, backend, frontend and
update-guide notes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:47:35 +08:00

79 lines
3.1 KiB
Python

"""Every name a backend module reads must be bound somewhere in that module.
Splitting a module can leave a function calling a helper, or a standard library module,
that was only imported in the old file. Python reports that only when the line runs, so a
save or thumbnail route fails for users while imports and other tests still pass. This
check is deliberately scope-blind: it catches missing imports, not every local-name slip.
"""
import ast
import builtins
import unittest
from pathlib import Path
PLUGIN_DIR = Path(__file__).resolve().parents[1]
ALWAYS_BOUND = set(dir(builtins)) | {"__file__", "__name__", "__doc__", "__package__", "__spec__", "__path__"}
def _backend_files():
yield from sorted(PLUGIN_DIR.glob("*.py"))
yield from sorted((PLUGIN_DIR / "api").glob("*.py"))
def _arguments(args):
names = [a.arg for a in args.posonlyargs + args.args + args.kwonlyargs]
names += [a.arg for a in (args.vararg, args.kwarg) if a]
return names
def _bound_names(tree, path):
bound = set()
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
bound.add(node.name)
bound.update(_arguments(node.args))
elif isinstance(node, ast.Lambda):
bound.update(_arguments(node.args))
elif isinstance(node, ast.ClassDef):
bound.add(node.name)
elif isinstance(node, (ast.Import, ast.ImportFrom)):
for alias in node.names:
if alias.name == "*":
bound |= _star_names(node, path)
else:
bound.add((alias.asname or alias.name).split(".")[0])
elif isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
bound.add(node.id)
elif isinstance(node, ast.ExceptHandler) and node.name:
bound.add(node.name)
elif isinstance(node, (ast.Global, ast.Nonlocal)):
bound.update(node.names)
elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name:
bound.add(node.name)
return bound
def _star_names(node, path):
"""Top-level names of a sibling module imported with ``from .x import *``."""
if node.level != 1 or not node.module:
raise AssertionError(f"{path.name}: only sibling star imports are checked")
target = path.parent / f"{node.module}.py"
tree = ast.parse(target.read_text(encoding="utf-8"))
return {name for name in _bound_names(tree, target) if not name.startswith("_")}
class BackendNamesTests(unittest.TestCase):
def test_every_read_name_is_bound(self):
problems = []
for path in _backend_files():
tree = ast.parse(path.read_text(encoding="utf-8"))
bound = _bound_names(tree, path) | ALWAYS_BOUND
for node in ast.walk(tree):
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) and node.id not in bound:
problems.append(f"{path.relative_to(PLUGIN_DIR)}:{node.lineno} {node.id}")
self.assertEqual(problems, [])
if __name__ == "__main__":
unittest.main()