#!/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())