taler-monitoring v1.13.12

Multi-phase global SUMMARY, warn-filter dimmed context, SUMMARY chrome
as meta; FP stage host-agent via francpaysan-stage-user (stagepaysan);
monpages GOA + FP stage.
This commit is contained in:
Hernâni Marques 2026-07-19 04:21:16 +02:00
commit 5a5431bfa9
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
88 changed files with 21996 additions and 0 deletions

192
android-test/lib_ui.py Executable file
View file

@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""Minimal adb + uiautomator helpers for Taler wallet GUI smoke (no root).
Does not replace Maestro/Appium; uses only adb shell + uiautomator dump.
"""
from __future__ import annotations
import re
import subprocess
import time
from pathlib import Path
from typing import Iterable, List, Optional, Sequence, Tuple
Bounds = Tuple[int, int, int, int]
def adb(serial: str, *args: str, check: bool = False, timeout: Optional[float] = 60) -> subprocess.CompletedProcess:
cmd = ["adb", "-s", serial, *args]
return subprocess.run(cmd, check=check, capture_output=True, text=True, timeout=timeout)
def dump_ui(serial: str, dest: Path) -> str:
adb(serial, "shell", "uiautomator", "dump", "/sdcard/ui.xml", timeout=90)
adb(serial, "pull", "/sdcard/ui.xml", str(dest), timeout=30)
return dest.read_text(errors="replace") if dest.is_file() else ""
def screencap(serial: str, dest: Path) -> None:
p = subprocess.run(
["adb", "-s", serial, "exec-out", "screencap", "-p"],
capture_output=True,
timeout=60,
)
if p.returncode == 0 and p.stdout:
dest.write_bytes(p.stdout)
def texts(xml: str) -> List[str]:
return [t for t in re.findall(r'text="([^"]*)"', xml) if t.strip()]
def find_bounds(xml: str, label: str) -> List[Bounds]:
"""Return list of (x1,y1,x2,y2) for nodes whose text or content-desc matches label (exact)."""
out: List[Bounds] = []
esc = re.escape(label)
patterns = [
rf'(?:text|content-desc)="{esc}"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"',
rf'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"[^>]*(?:text|content-desc)="{esc}"',
]
for pat in patterns:
for m in re.finditer(pat, xml, flags=re.I):
out.append(tuple(map(int, m.groups()))) # type: ignore[arg-type]
return out
def find_bounds_re(xml: str, pattern: str) -> List[Tuple[str, Bounds]]:
out: List[Tuple[str, Bounds]] = []
for m in re.finditer(
rf'(?:text|content-desc)="([^"]*{pattern}[^"]*)"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"',
xml,
flags=re.I,
):
label = m.group(1)
b = tuple(map(int, m.group(2, 3, 4, 5)))
out.append((label, b)) # type: ignore[arg-type]
return out
def center(b: Bounds) -> Tuple[int, int]:
x1, y1, x2, y2 = b
return (x1 + x2) // 2, (y1 + y2) // 2
def tap(serial: str, x: int, y: int) -> None:
adb(serial, "shell", "input", "tap", str(x), str(y))
def tap_bounds(serial: str, b: Bounds) -> None:
x, y = center(b)
tap(serial, x, y)
def dismiss_anr(serial: str, xml: str) -> bool:
"""If System UI isn't responding, prefer Wait over Close."""
if "isn't responding" not in xml and "reagiert nicht" not in xml.lower():
return False
for label in ("Wait", "Warten", "OK"):
bs = find_bounds(xml, label)
if bs:
tap_bounds(serial, bs[0])
return True
# fallback: right-ish button often Wait
adb(serial, "shell", "input", "keyevent", "22")
adb(serial, "shell", "input", "keyevent", "66")
return True
# Labels seen on wallet UI (EN/DE/FR) — expand as needed
CONFIRM_LABELS = [
"Confirm",
"Confirm withdrawal",
"Withdraw",
"Accept",
"I accept",
"Agree",
"Continue",
"Next",
"OK",
"Pay",
"Pay now",
"Bestätigen",
"Abheben",
"Akzeptieren",
"Zustimmen",
"Weiter",
"Bezahlen",
"Confirmer",
"Retirer",
"Accepter",
"Payer",
]
TOS_LABELS = [
"I accept",
"Accept",
"Agree",
"Akzeptieren",
"Zustimmen",
"Accepter",
]
def try_tap_any(serial: str, xml: str, labels: Sequence[str]) -> Optional[str]:
for label in labels:
bs = find_bounds(xml, label)
if bs:
tap_bounds(serial, bs[0])
return label
return None
def gui_drive(
serial: str,
out_dir: Path,
*,
rounds: int = 8,
sleep_s: float = 3.0,
phase: str = "gui",
) -> dict:
"""Poll UI, dismiss ANR, tap confirm/ToS-like buttons. Returns status dict."""
out_dir.mkdir(parents=True, exist_ok=True)
seen_texts: List[str] = []
taps: List[str] = []
anr = 0
for i in range(1, rounds + 1):
xml_path = out_dir / f"{phase}-{i:02d}-ui.xml"
png_path = out_dir / f"{phase}-{i:02d}.png"
try:
xml = dump_ui(serial, xml_path)
except Exception as e:
taps.append(f"dump-fail:{e}")
time.sleep(sleep_s)
continue
try:
screencap(serial, png_path)
except Exception:
pass
ts = texts(xml)
seen_texts.extend(ts[:20])
if dismiss_anr(serial, xml):
anr += 1
time.sleep(sleep_s)
continue
hit = try_tap_any(serial, xml, CONFIRM_LABELS)
if hit:
taps.append(hit)
time.sleep(sleep_s)
continue
# partial regex for amount confirm rows
for label, b in find_bounds_re(xml, r"Confirm|Withdraw|Pay|Accept|Bestätig|Abheben|Payer"):
tap_bounds(serial, b)
taps.append(f"re:{label}")
time.sleep(sleep_s)
break
else:
time.sleep(sleep_s)
return {
"taps": taps,
"anr_dismissals": anr,
"sample_texts": seen_texts[-40:],
"rounds": rounds,
}