taler-monitoring v1.0: standalone suite
This commit is contained in:
commit
a2afe690b7
73 changed files with 18268 additions and 0 deletions
366
android-test/run-goa-gui-chain.sh
Executable file
366
android-test/run-goa-gui-chain.sh
Executable file
|
|
@ -0,0 +1,366 @@
|
|||
#!/usr/bin/env bash
|
||||
# GOA (and optionally stage) GUI chain on Android — adapted from
|
||||
# taler-android branch **dev/hernani-inference/gui-workflows**
|
||||
# (`scripts/goa-chain-emu.sh`, docs/gui-workflows.md).
|
||||
#
|
||||
# Linux + macOS: uses adb + uiautomator only (no Homebrew hard dependency).
|
||||
# Upstream scripts are documented as macOS-only for SDK install paths.
|
||||
#
|
||||
# Flow:
|
||||
# 1) API mint withdraw (explorer) → VIEW taler://withdraw → GUI taps
|
||||
# 2) Merchant template POST → VIEW taler://pay → GUI taps
|
||||
#
|
||||
# Usage:
|
||||
# ./run-goa-gui-chain.sh
|
||||
# STACK=stage ./run-goa-gui-chain.sh
|
||||
# WITHDRAW_AMOUNTS='GOA:10' PAY_LIMIT=2 ./run-goa-gui-chain.sh
|
||||
# PKG=net.taler.wallet.fdroid.debug ./run-goa-gui-chain.sh --no-clear
|
||||
#
|
||||
# Env: ANDROID_SERIAL, PKG, EXP_PW_FILE, BANK, MERCHANT, INSTANCE,
|
||||
# WITHDRAW_AMOUNTS, PRODUCTS, PAY_LIMIT, SHOTDIR, PAUSE
|
||||
#
|
||||
# See GUI-AUTOMATION-NOTES.md
|
||||
#
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
CLEAR=1
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--no-clear) CLEAR=0 ;;
|
||||
-h|--help) sed -n '2,28p' "$0"; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
step() { echo; echo "======== $* ========"; }
|
||||
info() { echo " $*"; }
|
||||
pause() { sleep "${1:-${PAUSE:-0.8}}"; }
|
||||
|
||||
# Platform flags + headless emulator defaults (see GUI-AUTOMATION-NOTES.md)
|
||||
# shellcheck source=lib_android_env.sh
|
||||
. "$ROOT/lib_android_env.sh"
|
||||
[ "${AUTO_ANDROID}" = "1" ] || { echo "skipped: AUTO_ANDROID=${AUTO_ANDROID}"; exit 0; }
|
||||
[ "${AUTO_GUI}" = "1" ] || { echo "skipped: AUTO_GUI=${AUTO_GUI} (chain is GUI)"; exit 0; }
|
||||
|
||||
command -v adb >/dev/null || die "adb not found"
|
||||
command -v python3 >/dev/null || die "python3 required"
|
||||
|
||||
STACK="${STACK:-goa}"
|
||||
case "$STACK" in
|
||||
goa|hacktivism)
|
||||
PKG="${PKG:-net.taler.wallet.fdroid.debug}"
|
||||
BANK="${BANK:-https://bank.hacktivism.ch}"
|
||||
MERCHANT="${MERCHANT:-https://taler.hacktivism.ch}"
|
||||
INSTANCE=$(printf '%s' "${INSTANCE:-goa-shop}" | tr '[:upper:]' '[:lower:]')
|
||||
DONATE_INSTANCE=$(printf '%s' "${DONATE_INSTANCE:-goa-demo-cp4zqk}" | tr '[:upper:]' '[:lower:]')
|
||||
DONATE_TEMPLATE="${DONATE_TEMPLATE:-goa-free}"
|
||||
DEFAULT_AMOUNTS=("GOA:10" "GOA:20")
|
||||
DEFAULT_PAYS=("product:orbit-sticker" "product:nebula-coffee" "product:voidwave-playlist" "donate:GOA:12")
|
||||
CUR_HINT=GOA
|
||||
;;
|
||||
stage|testpaysan)
|
||||
PKG="${PKG:-net.taler.wallet.fdroid.debug}"
|
||||
BANK="${BANK:-https://stage.bank.lefrancpaysan.ch}"
|
||||
MERCHANT="${MERCHANT:-https://stage.monnaie.lefrancpaysan.ch}"
|
||||
INSTANCE=$(printf '%s' "${INSTANCE:-fermes-des-collines}" | tr '[:upper:]' '[:lower:]')
|
||||
DONATE_INSTANCE=$(printf '%s' "${DONATE_INSTANCE:-fermes-des-collines}" | tr '[:upper:]' '[:lower:]')
|
||||
DONATE_TEMPLATE="${DONATE_TEMPLATE:-don-panier-libre}"
|
||||
DEFAULT_AMOUNTS=("TESTPAYSAN:10" "TESTPAYSAN:20")
|
||||
DEFAULT_PAYS=("product:panier-legumes" "product:fromage-chevre" "product:oeufs-6")
|
||||
CUR_HINT=TESTPAYSAN
|
||||
;;
|
||||
*) die "STACK must be goa|stage (got $STACK)" ;;
|
||||
esac
|
||||
|
||||
ACT="${ACT:-$PKG/net.taler.wallet.main.MainActivity}"
|
||||
EXP_USER="${EXP_USER:-explorer}"
|
||||
PAY_LIMIT="${PAY_LIMIT:-4}"
|
||||
PAUSE="${PAUSE:-0.8}"
|
||||
SHOTDIR="${SHOTDIR:-$ROOT/out-gui-chain/$(date +%Y%m%d-%H%M%S)-$STACK}"
|
||||
mkdir -p "$SHOTDIR"
|
||||
|
||||
# Explorer password (same search as taler-monitoring ladder)
|
||||
if [[ -z "${EXP_PW_FILE:-}" ]]; then
|
||||
for cand in \
|
||||
"${HOME}/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank/bank-explorer-password.txt" \
|
||||
"${HOME}/.config/taler-landing/bank-explorer-password.txt"
|
||||
do
|
||||
[[ -f "$cand" ]] && EXP_PW_FILE="$cand" && break
|
||||
done
|
||||
fi
|
||||
# stage: try stagepaysan secret via ssh if missing
|
||||
if [[ ! -f "${EXP_PW_FILE:-}" && "$STACK" = "stage" ]]; then
|
||||
for host in francpaysan-stage-user francpaysan-host; do
|
||||
tmp=$(mktemp)
|
||||
if ssh -o BatchMode=yes -o ConnectTimeout=10 "$host" \
|
||||
'tr -d "\n\r" </mnt/data/stagepaysan/bank/secrets/bank-explorer-password.txt' \
|
||||
>"$tmp" 2>/dev/null && [[ -s "$tmp" ]]; then
|
||||
EXP_PW_FILE="$tmp"
|
||||
info "explorer password via $host"
|
||||
break
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
done
|
||||
fi
|
||||
[[ -n "${EXP_PW_FILE:-}" && -f "$EXP_PW_FILE" ]] || die "set EXP_PW_FILE (explorer password)"
|
||||
|
||||
# Headless AVD by default (no host window). WINDOWED=1 for a visible emulator.
|
||||
SERIAL="${SERIAL:-${ANDROID_SERIAL:-}}"
|
||||
android_ensure_device || die "no adb device (./start-android-emulator.sh --wait)"
|
||||
export ANDROID_SERIAL="$SERIAL"
|
||||
info "ANDROID_SERIAL=$ANDROID_SERIAL STACK=$STACK PKG=$PKG SHOTDIR=$SHOTDIR headless=$EMULATOR_HEADLESS gpu=$EMULATOR_GPU"
|
||||
ADB=(adb -s "$ANDROID_SERIAL")
|
||||
|
||||
adb_sh() { "${ADB[@]}" shell "$@"; }
|
||||
|
||||
shot() {
|
||||
local n="$1"
|
||||
adb_sh screencap -p "/sdcard/demo-$n.png" 2>/dev/null || true
|
||||
"${ADB[@]}" pull "/sdcard/demo-$n.png" "$SHOTDIR/$n.png" >/dev/null 2>&1 || true
|
||||
info "[shot] $SHOTDIR/$n.png"
|
||||
}
|
||||
|
||||
tap_text() {
|
||||
local label="$1"
|
||||
adb_sh uiautomator dump /sdcard/ui.xml >/dev/null 2>&1 || return 1
|
||||
"${ADB[@]}" pull /sdcard/ui.xml "$SHOTDIR/ui.xml" >/dev/null 2>&1 || return 1
|
||||
SERIAL="$ANDROID_SERIAL" python3 - "$label" "$SHOTDIR/ui.xml" <<'PY'
|
||||
import os, re, subprocess, sys
|
||||
from pathlib import Path
|
||||
label, path = sys.argv[1], sys.argv[2]
|
||||
serial = os.environ.get("SERIAL") or os.environ.get("ANDROID_SERIAL") or ""
|
||||
xml = Path(path).read_text(errors="replace")
|
||||
# ANR dismiss
|
||||
if "isn't responding" in xml or "reagiert nicht" in xml.lower():
|
||||
for lab in ("Wait", "Warten"):
|
||||
for m in re.finditer(rf'text="{lab}"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml):
|
||||
x = (int(m.group(1))+int(m.group(3)))//2
|
||||
y = (int(m.group(2))+int(m.group(4)))//2
|
||||
subprocess.run(["adb","-s",serial,"shell","input","tap",str(x),str(y)], check=False)
|
||||
print(f"anr-wait @{x},{y}")
|
||||
sys.exit(0)
|
||||
for pat in [
|
||||
rf'text="{re.escape(label)}"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"',
|
||||
rf'text="[^"]*{re.escape(label)}[^"]*"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"',
|
||||
rf'content-desc="[^"]*{re.escape(label)}[^"]*"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"',
|
||||
]:
|
||||
for m in re.finditer(pat, xml, re.I):
|
||||
chunk = xml[max(0, m.start()-120):m.end()]
|
||||
if 'enabled="false"' in chunk:
|
||||
continue
|
||||
x = (int(m.group(1))+int(m.group(3)))//2
|
||||
y = (int(m.group(2))+int(m.group(4)))//2
|
||||
print(f"tap {label!r} @{x},{y}")
|
||||
cmd = ["adb","shell","input","tap",str(x),str(y)]
|
||||
if serial:
|
||||
cmd = ["adb","-s",serial,"shell","input","tap",str(x),str(y)]
|
||||
subprocess.run(cmd, check=False)
|
||||
sys.exit(0)
|
||||
print("miss", label)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
mint_withdraw() {
|
||||
local amount="$1"
|
||||
EXP_USER="$EXP_USER" EXP_PW_FILE="$EXP_PW_FILE" BANK="$BANK" \
|
||||
python3 - "$amount" <<'PY'
|
||||
import base64, json, os, ssl, sys, urllib.request
|
||||
from pathlib import Path
|
||||
amount = sys.argv[1]
|
||||
bank = os.environ["BANK"].rstrip("/")
|
||||
user = os.environ["EXP_USER"]
|
||||
pw = Path(os.environ["EXP_PW_FILE"]).read_text().strip()
|
||||
auth = "Basic " + base64.b64encode(f"{user}:{pw}".encode()).decode()
|
||||
ctx = ssl.create_default_context()
|
||||
|
||||
def req(method, url, data=None, headers=None):
|
||||
h = dict(headers or {})
|
||||
body = None
|
||||
if data is not None:
|
||||
body = json.dumps(data).encode()
|
||||
h["Content-Type"] = "application/json"
|
||||
r = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
with urllib.request.urlopen(r, context=ctx, timeout=25) as resp:
|
||||
return json.load(resp)
|
||||
|
||||
tok = req(
|
||||
"POST",
|
||||
f"{bank}/accounts/{user}/token",
|
||||
{"scope": "readwrite", "duration": {"d_us": 3600_000_000}},
|
||||
{"Authorization": auth},
|
||||
)["access_token"]
|
||||
wd = req(
|
||||
"POST",
|
||||
f"{bank}/accounts/{user}/withdrawals",
|
||||
{"amount": amount},
|
||||
{"Authorization": f"Bearer {tok}"},
|
||||
)
|
||||
print(wd.get("taler_withdraw_uri") or wd.get("talerWithdrawUri") or "")
|
||||
PY
|
||||
}
|
||||
|
||||
mint_pay() {
|
||||
local mode="$1"
|
||||
MERCHANT="$MERCHANT" INSTANCE="$INSTANCE" \
|
||||
DONATE_INSTANCE="$DONATE_INSTANCE" DONATE_TEMPLATE="$DONATE_TEMPLATE" \
|
||||
python3 - "$mode" <<'PY'
|
||||
import json, os, ssl, sys, urllib.error, urllib.request
|
||||
mode = sys.argv[1]
|
||||
merchant = os.environ["MERCHANT"].rstrip("/")
|
||||
inst = os.environ["INSTANCE"]
|
||||
d_inst = os.environ["DONATE_INSTANCE"]
|
||||
d_tmpl = os.environ["DONATE_TEMPLATE"]
|
||||
ctx = ssl.create_default_context()
|
||||
|
||||
def post(url, data):
|
||||
body = json.dumps(data).encode()
|
||||
r = urllib.request.Request(url, data=body, method="POST",
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(r, context=ctx, timeout=25) as resp:
|
||||
return resp.status, json.load(resp)
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read() if hasattr(e, "read") else b""
|
||||
try:
|
||||
return getattr(e, "code", None), json.loads(raw)
|
||||
except Exception as err:
|
||||
raise SystemExit(f"POST {url} failed: {e} body={raw[:200]!r}") from err
|
||||
|
||||
if mode.startswith("product:"):
|
||||
pid = mode.split(":", 1)[1]
|
||||
st, d = post(f"{merchant}/instances/{inst}/templates/{pid}", {})
|
||||
use_inst = inst
|
||||
elif mode.startswith("donate:"):
|
||||
amt = mode.split(":", 1)[1]
|
||||
st, d = post(f"{merchant}/instances/{d_inst}/templates/{d_tmpl}", {"amount": amt})
|
||||
use_inst = d_inst
|
||||
else:
|
||||
raise SystemExit(f"bad mode {mode!r}")
|
||||
|
||||
if "taler_pay_uri" in d:
|
||||
print(d["taler_pay_uri"]); raise SystemExit(0)
|
||||
oid, tok = d.get("order_id"), d.get("token")
|
||||
if not oid or not tok:
|
||||
raise SystemExit(f"no order: {d!r}"[:200])
|
||||
host = merchant.replace("https://","").replace("http://","").split("/")[0]
|
||||
print(f"taler://pay/{host}/instances/{use_inst}/{oid}/?c={tok}")
|
||||
PY
|
||||
}
|
||||
|
||||
click_through() {
|
||||
local labels=("$@")
|
||||
local i lab misses=0
|
||||
# Prefer dismissing ANR before hunting Confirm (low-RAM hosts thrash System UI)
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if tap_text "Wait" 2>/dev/null || tap_text "Warten" 2>/dev/null; then
|
||||
info "dismissed ANR (Wait)"
|
||||
pause 2.0
|
||||
misses=0
|
||||
continue
|
||||
fi
|
||||
local hit=0
|
||||
for lab in "${labels[@]}"; do
|
||||
if tap_text "$lab" 2>/dev/null; then
|
||||
hit=1
|
||||
misses=0
|
||||
pause 0.9
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$hit" -eq 0 ]]; then
|
||||
misses=$((misses + 1))
|
||||
# stop early if hierarchy has nothing useful (avoid burning order deadlines)
|
||||
[[ "$misses" -ge 3 ]] && break
|
||||
fi
|
||||
pause 0.5
|
||||
done
|
||||
}
|
||||
|
||||
if [[ -n "${WITHDRAW_AMOUNTS:-}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
AMOUNTS=($WITHDRAW_AMOUNTS)
|
||||
else
|
||||
AMOUNTS=("${DEFAULT_AMOUNTS[@]}")
|
||||
fi
|
||||
if [[ -n "${PRODUCTS:-}" ]]; then
|
||||
PAYS=()
|
||||
for p in $PRODUCTS; do PAYS+=("product:$p"); done
|
||||
else
|
||||
PAYS=("${DEFAULT_PAYS[@]}")
|
||||
fi
|
||||
|
||||
############################
|
||||
step "0 device + wallet ($STACK / $CUR_HINT)"
|
||||
adb_sh input keyevent KEYCODE_WAKEUP 2>/dev/null || true
|
||||
adb_sh wm dismiss-keyguard 2>/dev/null || true
|
||||
if [[ "$CLEAR" -eq 1 ]]; then
|
||||
info "pm clear $PKG"
|
||||
adb_sh pm clear "$PKG" >/dev/null 2>&1 || true
|
||||
fi
|
||||
adb_sh am force-stop "$PKG" 2>/dev/null || true
|
||||
pause 0.4
|
||||
# Prefer explicit MainActivity (from gui-workflows); fall back to VIEW launcher
|
||||
if ! adb_sh am start -n "$ACT" >/dev/null 2>&1; then
|
||||
info "MainActivity path failed — monkey launch"
|
||||
adb_sh monkey -p "$PKG" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1 || true
|
||||
fi
|
||||
pause 2.0
|
||||
shot "00-start"
|
||||
|
||||
############################
|
||||
step "2 withdraw chain"
|
||||
idx=0
|
||||
for AMT in "${AMOUNTS[@]}"; do
|
||||
idx=$((idx + 1))
|
||||
step "2.$idx withdraw $AMT"
|
||||
URI="$(mint_withdraw "$AMT" | head -1)"
|
||||
[[ -n "$URI" ]] || die "mint failed for $AMT"
|
||||
# wallet / monitoring QR rules: strip default ports
|
||||
URI=$(printf '%s' "$URI" | sed 's/:443\//\//g; s/:443?/?/g; s/:80\//\//g')
|
||||
info "URI=$URI"
|
||||
echo "$URI" >>"$SHOTDIR/withdraw-uris.txt"
|
||||
adb_sh am start -a android.intent.action.VIEW -d "$URI" "$PKG" >/dev/null
|
||||
pause 1.5
|
||||
shot "w${idx}-open"
|
||||
click_through "Accept" "I accept" "Accept terms" "Confirm" "Continue" "Withdraw" "Next" "OK" "Agree" "Bestätigen" "Abheben" "Akzeptieren"
|
||||
pause 3.0
|
||||
shot "w${idx}-after"
|
||||
adb_sh am start -n "$ACT" >/dev/null 2>&1 || true
|
||||
pause 0.6
|
||||
tap_text "Assets" 2>/dev/null || true
|
||||
pause 0.4
|
||||
shot "w${idx}-assets"
|
||||
done
|
||||
|
||||
############################
|
||||
step "3 pay chain (limit=$PAY_LIMIT)"
|
||||
pidx=0
|
||||
for PAY in "${PAYS[@]}"; do
|
||||
pidx=$((pidx + 1))
|
||||
[[ "$pidx" -gt "$PAY_LIMIT" ]] && break
|
||||
step "3.$pidx pay $PAY"
|
||||
if ! PAYURI="$(mint_pay "$PAY" | head -1)"; then
|
||||
info "mint_pay failed for $PAY — skip"
|
||||
continue
|
||||
fi
|
||||
[[ -n "$PAYURI" ]] || { info "empty pay uri — skip"; continue; }
|
||||
PAYURI=$(printf '%s' "$PAYURI" | sed 's/:443\//\//g; s/:443?/?/g; s/:80\//\//g')
|
||||
info "PAYURI=$PAYURI"
|
||||
echo "$PAYURI" >>"$SHOTDIR/pay-uris.txt"
|
||||
adb_sh am start -a android.intent.action.VIEW -d "$PAYURI" "$PKG" >/dev/null
|
||||
pause 1.5
|
||||
shot "p${pidx}-open"
|
||||
click_through "Pay" "Confirm" "Accept" "Continue" "OK" "Next" "Bezahlen" "Bestätigen" "Payer"
|
||||
pause 1.5
|
||||
shot "p${pidx}-after"
|
||||
adb_sh am start -n "$ACT" >/dev/null 2>&1 || true
|
||||
pause 0.5
|
||||
done
|
||||
|
||||
step "4 done → $SHOTDIR"
|
||||
ls -la "$SHOTDIR" | tail -40
|
||||
adb_sh dumpsys window 2>/dev/null | grep mCurrentFocus | head -1 || true
|
||||
info "PKG=$PKG amounts=${AMOUNTS[*]} pays(limit)=$PAY_LIMIT"
|
||||
echo "See GUI-AUTOMATION-NOTES.md (ported from taler-android dev/hernani-inference/gui-workflows)"
|
||||
Loading…
Add table
Add a link
Reference in a new issue