trainlog/tui/tests/test_presentation_catalog.py
2026-09-16 08:07:37 +02:00

86 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Source-derived, default-fail catalogue audit for active TUI renderers."""
import pathlib, re, sys
ROOT = pathlib.Path(__file__).resolve().parents[2]
# WHY: implementation-directory membership is not presentation reachability;
# scanning core SQL, synchronization codecs, and catalogs confuses durable raw
# diagnostics/identifiers with renderer copy.
# CONTRACT: this reviewed allowlist contains every active renderer plus the two
# known presentation-producing helpers. A new renderer must be added here and
# defaults to failure as soon as it contains uncatalogued French copy.
# INVARIANT: database/sync/sync_history/catalog implementations are not UI-copy
# owners and cannot become implicit exceptions through this audit.
RENDERER_SOURCES = (
"tui/src/tui.c",
"tui/src/chart.c",
"tui/src/app_shell.c",
"tui/src/sync_screen_action.c",
)
PRESENTATION_PRODUCERS = (
"tui/src/timeutil.c",
"tui/src/statistics.c",
)
SOURCES = RENDERER_SOURCES + PRESENTATION_PRODUCERS
assert not {"tui/src/database.c", "tui/src/sync.c", "tui/src/sync_history.c",
"tui/src/catalog.c"}.intersection(SOURCES)
RENDERER_MARKER = re.compile(
r"trainlog_(?:surface|terminal)_(?:printf|put|draw|set)|"
r"trainlog_presentation_(?:text|source|format)"
)
INFRASTRUCTURE = {"tui/src/presentation.c", "tui/src/terminal.c"}
discovered_renderers = {
str(path.relative_to(ROOT))
for path in (ROOT / "tui/src").glob("*.c")
if RENDERER_MARKER.search(path.read_text(encoding="utf-8"))
} - INFRASTRUCTURE
unreviewed_renderers = discovered_renderers - set(RENDERER_SOURCES)
if unreviewed_renderers:
sys.stderr.write("UNREVIEWED presentation renderer(s):\n" +
"\n".join(f" {path}" for path in sorted(unreviewed_renderers)) + "\n")
sys.exit(1)
P = (ROOT / "tui/src/presentation.c").read_text(encoding="utf-8")
CATALOG = set(re.findall(r'\{\s*"(?:[^"\\]|\\.)*"\s*,\s*"((?:[^"\\]|\\.)*)"\s*,\s*"', P))
LIT = re.compile(r'"((?:[^"\\]|\\.)*)"')
FR = re.compile(r'[À-ÿ]|\b(?:aucun|aucune|abandonner|annuler|choisir|durée|entrée|exercice|fusion|historique|impossible|mesure|objectif|profil|recherche|séance|synchron|taille|zone|série|charge)\b', re.I)
# Exact source-bound exceptions, never pattern exemptions. Each is a
# non-copy token: punctuation/layout, a protocol token, or an internal key.
EXCLUDED = {
"tui/src/tui.c": {
1083:"—",1495:"— kg",3990:"J−%zu",3993:"M−%zu",3996:"P−%zu",4779:"msg.zone.required",5224:"statistics.zone.previous",5226:"statistics.zone.next",5290:"generator.warning.zone",5632:"—",5675:"— ",5680:"— ",5698:"— ",5713:"—",5716:"—",5840:"—",5843:"—",5846:"—",5901:"—",6332:"%-22s —",6632:"▶",6635:"▶",6663:"▶",6672:"▶",6685:"▶",6688:"▶",6712:"▶",6717:"▶",6908:"%02d/%02d … %02d/%02d",6950:"— ",7060:"PC↔Android",7106:"▶",7330:"%s · %s",7338:"—",7339:"—",7415:"—",7418:"—",7587:"—",7615:"—",7617:"—",7619:"—",7737:"aucun",7739:"aucun",7904:"—",7955:"—",8085:" · ↑",8086:" · ↓"},
"tui/src/chart.c": {254:"◆ %.2f %s"},
"tui/src/app_shell.c": {317:"…"},
"tui/src/sync_screen_action.c": {132:"Android→PC",136:"PC→Android",139:"PC↔Android"},
"tui/src/statistics.c": {230:("août", "déc.")},
}
def candidates(text, path):
"""Independent source scanner: adding uncatalogued French copy fails."""
out=[]
for line_no,line in enumerate(text.splitlines(),1):
for match in LIT.finditer(line):
value=match.group(1)
if FR.search(value): out.append((path,line_no,value))
return out
# Non-tautology proof: this never appears in the catalogue/manifest.
probe=candidates('trainlog_surface_printf(s, 1, 1, "Bonjour séance");','fixture.c')
assert probe == [('fixture.c',1,'Bonjour séance')]
assert probe[0][2] not in CATALOG
assert RENDERER_MARKER.search('trainlog_surface_printf(s, 1, 1, "new copy")')
unknown=[]; used=set()
for path in SOURCES:
for candidate in candidates((ROOT/path).read_text(encoding='utf-8'),path):
_,line,value=candidate
if value in CATALOG: continue
allowed = EXCLUDED.get(path,{}).get(line)
if allowed == value or isinstance(allowed, tuple) and value in allowed:
used.add(candidate); continue
unknown.append(candidate)
if unknown:
for kind, rows in (("TRANSLATABLE_UI catalogue omissions",unknown),):
if rows: sys.stderr.write(kind+':\n'+'\n'.join(' %s:%s: %s'%r for r in rows)+'\n')
sys.exit(1)
print('TRANSLATABLE_UI=0 unexplained; source-derived exclusions:',len(used))