Files
harbour-aira/tests/ui_contract_check.py
kaneda 6eea29f99d controllo di contratto fra i QML e il bridge (tests/ui_contract_check.py)
Le chiavi lette dalle pagine vengono confrontate con quelle che /summary e
/stats restituiscono davvero: la UI non si puo' eseguire senza il kit Silica,
quindi un refuso resterebbe invisibile fino al primo build sul telefono.
2026-10-10 09:16:38 +02:00

78 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Controllo di contratto fra il bridge e l'interfaccia di harbour-aira.
La UI non si puo' eseguire senza il kit Silica (serve il telefono), quindi un
refuso in una chiave letta dal QML resterebbe invisibile fino al primo build:
l'app mostrerebbe "—" per sempre, senza errori. Qui si estraggono le chiavi che
i QML leggono da /summary e da /stats e si confrontano con le risposte vere.
python3 tests/ui_contract_check.py [http://IP:8790]
Il token si legge da $AIRA_CONFIG_DIR/aira_bridge.json (default: la
configurazione personale dell'autore), lo stesso file che usa il bridge.
Serve il bridge in esecuzione; non invia nessun comando all'impianto.
"""
from __future__ import annotations
import json
import os
import pathlib
import re
import sys
import urllib.request
APP = pathlib.Path(__file__).resolve().parent.parent
QML_DIR = APP / "qml/pages"
CONFIG_DIR = pathlib.Path(os.environ.get(
"AIRA_CONFIG_DIR", str(pathlib.Path.home() / ".hermes/profiles/carlo")))
BASE = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8790"
def chiama(rotta: str) -> dict:
token = json.loads((CONFIG_DIR / "aira_bridge.json").read_text())["token"]
req = urllib.request.Request(BASE + rotta, headers={"Authorization": "Bearer " + token})
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read().decode())
def chiavi_qml() -> tuple[set, set]:
"""Chiavi di /summary (raw/count) e di /stats (indici) lette dai QML."""
som, sta = set(), set()
for f in sorted(QML_DIR.glob("*.qml")):
testo = f.read_text()
som |= set(re.findall(r'(?:raw|count)\("([a-z0-9_]+)"\)', testo))
sta |= set(re.findall(r'(?:stats|mese|risparmi|tariffa)\(\)\["([a-z0-9_]+)"\]', testo))
sta |= set(re.findall(r'modelData\["([a-z0-9_]+)"\]', testo))
return som, sta
def main() -> int:
som_qml, sta_qml = chiavi_qml()
som = chiama("/summary")["summary"]
sta = chiama("/stats")["stats"]
# Le statistiche hanno blocchi annidati (month, savings, smart_tariff) e liste
# di oggetti (cop_monthly, monthly_savings, insights): il QML li legge col nome
# semplice, quindi si raccolgono tutte le chiavi interne.
sta_valide = set(sta)
for blocco in ("month", "savings", "smart_tariff"):
sta_valide |= set(sta.get(blocco) or {})
for lista in ("cop_monthly", "monthly_savings", "insights"):
for voce in (sta.get(lista) or []):
sta_valide |= set(voce)
mancanti_som = sorted(k for k in som_qml if k not in som)
mancanti_sta = sorted(k for k in sta_qml if k not in sta_valide)
print(f"chiavi di /summary lette dai QML: {len(som_qml)}")
print(f"chiavi di /stats lette dai QML: {len(sta_qml)}")
print(f"/summary: {len(som)} chiavi, /stats: {len(sta)} chiavi")
print("chiavi di /summary non presenti nel JSON:", mancanti_som or "nessuna")
print("chiavi di /stats non presenti nel JSON: ", mancanti_sta or "nessuna")
print("ESITO:", "CONTRATTO RISPETTATO" if not (mancanti_som or mancanti_sta) else "DISALLINEATO")
return 1 if (mancanti_som or mancanti_sta) else 0
if __name__ == "__main__":
raise SystemExit(main())