diet-guard/diet_guard/tests/test_gate_no_words_static.py
Krzysztof kuhy Rudnicki 43e4b4070d Replace PC gate text with glyphs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 15:35:34 +01:00

75 lines
2.7 KiB
Python

"""Static twin of ``test_gate_no_words``: no gate source line can paint a word.
The fake-tk guard only sees strings on the paths a scenario drives. This one
parses every ``_gatelock*.py`` module and fails on any string literal handed
to a painted keyword (``text=``, ``notebook.add(..., text=)``) or written into
a status variable that holds a 2+ letter word, so a new sentence on an
unexercised branch cannot slip in. Runs in CI (no display, no installed gate).
"""
from __future__ import annotations
import ast
from pathlib import Path
import re
from diet_guard.tests.test_gate_no_words import UNITS
PACKAGE = Path(__file__).resolve().parent.parent
WORD = re.compile(r"[^\W\d_]{2,}")
# The window title is the OS-level app name: the one kept proper noun.
APP_NAME = "Diet Gate"
# Variable-backed text sinks: StringVar writes and status setters.
SINKS = {"set", "_set_status", "_set_budget_status", "_set_schedule_status"}
def _literals(node: ast.AST) -> list[str]:
"""Every constant string inside ``node`` (f-string parts included)."""
return [
n.value
for n in ast.walk(node)
if isinstance(n, ast.Constant) and isinstance(n.value, str)
]
def _offences(path: Path) -> list[str]:
tree = ast.parse(path.read_text(encoding="utf-8"))
found: list[str] = []
for call in (n for n in ast.walk(tree) if isinstance(n, ast.Call)):
painted: list[ast.AST] = [k.value for k in call.keywords if k.arg == "text"]
name = getattr(call.func, "attr", getattr(call.func, "id", ""))
if name in SINKS:
painted.extend(call.args[:1])
if name == "title":
continue # the window title: APP_NAME, checked below
for node in painted:
found.extend(
f"{path.name}:{call.lineno} {lit!r}"
for lit in _literals(node)
if set(WORD.findall(lit)) - UNITS
)
return found
def test_no_gate_source_paints_a_word() -> None:
paths = sorted(PACKAGE.glob("_gatelock*.py"))
assert len(paths) > 20, "the gate modules were not found"
offences = [o for path in paths for o in _offences(path)]
assert not offences, "\n" + "\n".join(offences)
def test_the_checker_flags_a_sentence(tmp_path: Path) -> None:
"""Canary: a status sentence and a ``text=`` word are both caught."""
bad = tmp_path / "_gatelock_x.py"
bad.write_text(
'def f(v, w):\n v.set("Saved.")\n w.Label(text="Edit")\n'
' v.set("OK")\n',
encoding="utf-8",
)
assert len(_offences(bad)) == 3 # "OK" is two letters: a word too
def test_the_window_title_is_the_app_name() -> None:
source = (PACKAGE / "_gatelock.py").read_text(encoding="utf-8")
assert f'self.root.title("{APP_NAME}")' in source