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.
This commit is contained in:
@@ -138,6 +138,17 @@ lettura del riassunto e delle statistiche, etichette di stato, corpo dei comandi
|
||||
messaggi d'errore e la formattazione di numeri, importi e mesi della pagina Dati.
|
||||
**La UI Silica si verifica solo sul dispositivo.**
|
||||
|
||||
C'è poi un controllo di **contratto fra bridge e interfaccia**: confronta le
|
||||
chiavi che i QML leggono con quelle che il bridge restituisce davvero (26 da
|
||||
`/summary`, 15 da `/stats`). Serve il bridge in esecuzione, e nasce dal fatto che
|
||||
la UI qui non si può eseguire: un refuso in una chiave si vedrebbe sul telefono
|
||||
come un "—" perenne, senza niente nel log.
|
||||
|
||||
```sh
|
||||
python3 tests/ui_contract_check.py # bridge su 127.0.0.1:8790
|
||||
python3 tests/ui_contract_check.py http://192.168.3.21:8790
|
||||
```
|
||||
|
||||
## Stato
|
||||
|
||||
- **61/61** verifiche del nucleo, compilazione senza warning
|
||||
@@ -186,6 +197,7 @@ qml/components/InfoRow.qml etichetta a sinistra, valore a destra
|
||||
qml/components/ValueRow.qml titolo e sottotitolo a sinistra, valore grande a destra
|
||||
qml/components/ActionButton.qml pulsante di comando, con conferma opzionale
|
||||
tests/core_test.cpp verifiche del nucleo (headless)
|
||||
tests/ui_contract_check.py chiavi dei QML contro quelle del bridge (bridge attivo)
|
||||
bridge/aira_bridge.py servizio HTTP/1.1 + JSON verso il cloud (gRPC)
|
||||
bridge/setup_creds.py salva le credenziali Aira (file 600)
|
||||
bridge/aira_probe.py sonda di sola lettura: login + stato impianto
|
||||
|
||||
@@ -122,6 +122,16 @@ statistics parsing, mode labels, command payloads, error messages, and the
|
||||
number/currency/month formatting used by the Data page.
|
||||
**The Silica UI can only be verified on a device.**
|
||||
|
||||
A second check compares the keys the QML reads with the keys the bridge really
|
||||
returns (26 from `/summary`, 15 from `/stats`). It needs the bridge running, and
|
||||
it exists because the UI cannot be run here: a typo in a key would show up as a
|
||||
permanent “—” on the phone, with nothing in the log.
|
||||
|
||||
```sh
|
||||
python3 tests/ui_contract_check.py # bridge on 127.0.0.1:8790
|
||||
python3 tests/ui_contract_check.py http://192.168.3.21:8790
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
Verified so far:
|
||||
@@ -201,6 +211,7 @@ qml/components/InfoRow.qml label on the left, value on the right
|
||||
qml/components/ValueRow.qml title + subtitle left, big value right
|
||||
qml/components/ActionButton.qml command button, optional confirmation
|
||||
tests/core_test.cpp core checks (headless)
|
||||
tests/ui_contract_check.py QML keys vs bridge keys (bridge must be running)
|
||||
bridge/ local HTTP bridge -> see bridge/README.md
|
||||
translations/ harbour-aira-it.ts (English is the source language)
|
||||
docs/PIANO.md project plan and decisions (Italian)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user