taler-monitoring v1.0: standalone suite
This commit is contained in:
commit
6b561f3d7e
73 changed files with 18268 additions and 0 deletions
11
android-test/.gitignore
vendored
Normal file
11
android-test/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
apks/*.apk
|
||||
out/
|
||||
*.png
|
||||
out-source/
|
||||
out-source-*/
|
||||
out-gui/
|
||||
out-source-gui/
|
||||
out-gui-chain/
|
||||
out-matrix-*/
|
||||
out-stack-run-*/
|
||||
out-survey-*/
|
||||
362
android-test/GUI-AUTOMATION-NOTES.md
Normal file
362
android-test/GUI-AUTOMATION-NOTES.md
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
# GUI automation notes (Android wallet)
|
||||
|
||||
Canonical notes for **graphical** Android wallet tests against GOA / stage.
|
||||
|
||||
---
|
||||
|
||||
## Minimal variant (implemented first) — status 2026-07
|
||||
|
||||
This is the **minimal vanilla** path we actually shipped; full dual-platform
|
||||
GUI is planned via flags (below), not required to use these scripts today.
|
||||
|
||||
| Piece | Status | Notes |
|
||||
|-------|--------|--------|
|
||||
| F-Droid published APK install | **done** | `net.taler.wallet.fdroid` 1.6.1 / 854 |
|
||||
| Source build + same smoke | **done** | `:wallet:assembleFdroidDebug` → `net.taler.wallet.fdroid.debug` |
|
||||
| Default source branch | **done** | `dev/hernani-inference/fix-bank-withdraw-auto-exchange` (minimal GOA exchange auto-add) |
|
||||
| Deep-link entry (≡ QR) | **done** | `adb` `VIEW` **withdraw then pay** (`taler://withdraw/…` → `taler://pay…`) |
|
||||
| Explorer API mint (chain) | **done** | `run-goa-gui-chain.sh` (GOA + stage) |
|
||||
| UI taps via uiautomator | **done** | best-effort Confirm/ToS/Pay; ANR → prefer **Wait** |
|
||||
| Screenshots / logcat artifacts | **done** | under `out*`, `out-gui-chain/` (gitignored) |
|
||||
| Host CLI e2e settlement | **separate** | `taler-monitoring.sh e2e` / `ladder` (not Android UI) |
|
||||
| Reliable unattended Confirm on 4 GiB Linux host | **limited** | System UI ANR; use phone or more RAM |
|
||||
| iOS GUI | **not in this tree** | see taler-ios `dev/hernani-inference/gui-workflows` |
|
||||
| macOS full dual AVD helpers | **upstream** | taler-android `gui-workflows` (Homebrew); ported ideas here for Linux |
|
||||
|
||||
**Ground rule:** bring it to run; app diffs only if needed and **minimal**; prefer
|
||||
extending existing `dev/hernani-inference/*` branches over new trees.
|
||||
|
||||
### Minimalvariante: was geht / was nicht
|
||||
|
||||
Einschränkungen der **aktuell implementierten** Minimalvariante (deep-link +
|
||||
uiautomator best-effort). Kein voller Dual-Platform-GUI-Stack, kein Ersatz für
|
||||
Host-CLI-Settlement.
|
||||
|
||||
#### Geht (grün)
|
||||
|
||||
| Fähigkeit | Wie / Hinweis |
|
||||
|-----------|----------------|
|
||||
| APK installieren | F-Droid `net.taler.wallet.fdroid` **oder** Source `assembleFdroidDebug` (`.debug`) |
|
||||
| GOA-Exchange ohne manuelles Add | Source-Build von `fix-bank-withdraw-auto-exchange` (Default in `run-android-build-and-smoke.sh`) |
|
||||
| **Abheben + Bezahlen** | Beide Beine: erst Withdraw-URI + Taps, dann Pay-URI + Taps (`DO_WITHDRAW`/`DO_PAY`) |
|
||||
| Light UI-Drive | Confirm / Accept / Pay-Taps über Label-Suche (`lib_ui.py`, gui-smoke, chain) |
|
||||
| Multi-Runden-Taps | `run-android-gui-smoke.sh` / `run-goa-gui-chain.sh` (best-effort) |
|
||||
| Explorer-Mint (Kette) | `run-goa-gui-chain.sh` + `EXP_PW_FILE` (GOA + stage) |
|
||||
| Stack-Wahl | `STACK=goa` / `stage` (Bank/Merchant-URLs) |
|
||||
| Artefakte | Screenshots, UI-XML, logcat unter `out*` / `out-gui-chain/` (gitignored) |
|
||||
| Hybrid-Smoke | Install + Intent + leichte Taps: `run-android-pay-smoke.sh` |
|
||||
| Settlement **nachweisen** | **Host-seitig**: `taler-monitoring.sh e2e` / `ladder` (nicht Android-UI) |
|
||||
| Flags abschalten | `AUTO_ANDROID=0` skip; `AUTO_GUI=0` → nur deep-link/pay-smoke |
|
||||
|
||||
#### Geht **nicht** oder nur eingeschränkt (rot / gelb)
|
||||
|
||||
| Beschränkung | Folge / Workaround |
|
||||
|--------------|--------------------|
|
||||
| **Unattended Confirm → settled im Emulator (~4 GiB Host)** | Häufig **System UI ANR**; Taps bleiben hängen. Prefer **Wait** auf ANR-Dialog, echtes Gerät, oder Host mit mehr RAM. Kein zuverlässiges grünes E2E nur über GUI auf dem schwachen Emulator. |
|
||||
| **Vollständige Wallet-Settlement-Assertion in der GUI** | Minimalvariante prüft **nicht** „Balance final / transfer_done“ in der App-Oberfläche. Proof bleibt CLI/e2e. |
|
||||
| **Kamera / physischer QR-Scan** | Absichtlich nicht automatisiert; deep-link ist der Shortcut. Landing-QR-Pfade sind **HTTP**-Thema, nicht Android-GUI. |
|
||||
| **Browser-Shop-Checkout-UI** | Kein Chromium/WebView-Drive; Pay-URI kommt per Template-`POST` + Intent. |
|
||||
| **Volles ToS-Scroll / alle Dialoge** | Best-effort Accept-Tap; lange ToS oder unerwartete Sheets können hängen bleiben. |
|
||||
| **Published F-Droid allein auf GOA** | Kann Exchange-Add / Spinner-Probleme zeigen → **fix-branch-APK** bauen; bei Spinner-Hang optional `fix-withdraw-spinner-fallback`. |
|
||||
| **iOS-GUI** | Nicht in diesem Tree; siehe taler-ios `gui-workflows`. |
|
||||
| **macOS dual AVD / Homebrew-Helpers** | Upstream `gui-workflows`; hier nur Linux-taugliche Port-Ideen. |
|
||||
| **`AUTO_*` in jedem Script erzwungen** | Design + teilweise verdrahtet; Wrapper-Checkliste noch offen. |
|
||||
| **`run-until-done` / CLI-Wallet in GUI-Skripten** | Gehört zu CLI-Automation, nicht GUI; siehe `CLI-AUTOMATION-NOTES.md`. |
|
||||
| **CI grün = „User hat bezahlt“** | Smoke = Intent + best-effort UI. Wirtschaftlicher Erfolg = Host-e2e / ladder / Explorer. |
|
||||
|
||||
#### Erwartetes Ergebnis pro Schicht
|
||||
|
||||
| Schicht | Erfolgskriterium Minimalvariante |
|
||||
|---------|----------------------------------|
|
||||
| A Install | APK installed, App startet |
|
||||
| B Deep-link | Intent delivered, Wallet öffnet Withdraw/Pay-Flow |
|
||||
| C GUI taps | Dump+Tap ohne Crash; ANR ggf. mit Wait; **kein** Garant für „Paid“ |
|
||||
| D Evidence | Artefakte geschrieben |
|
||||
| Settlement | **Außerhalb** GUI: Host e2e/ladder |
|
||||
|
||||
#### Wann Minimalvariante reicht
|
||||
|
||||
- Smoke nach APK-/Branch-Build (Intent kommt an, App crasht nicht).
|
||||
- Manuelle Nacharbeit am Gerät nach vorbereiteten URIs.
|
||||
- GOA/stage **Erreichbarkeit** von Landing/Template + Wallet-Einstieg.
|
||||
|
||||
#### Wann nicht reicht → nächster Schritt
|
||||
|
||||
- Unattended grünes Pay-E2E auf dem Emulator → mehr RAM / physisches Gerät, ggf. `fix-withdraw-spinner-fallback`.
|
||||
- Beweis „Münzen da / Merchant paid“ → CLI e2e/ladder, nicht GUI-smoke.
|
||||
- iOS / Dual-Platform → andere Repos/Flags, nicht diese Skripte.
|
||||
|
||||
```bash
|
||||
cd scripts/taler-monitoring/android-test
|
||||
|
||||
# Minimal hybrid (install + deep-link + light taps)
|
||||
STACK=goa ./run-android-pay-smoke.sh
|
||||
|
||||
# GUI multi-round taps
|
||||
STACK=goa ./run-android-gui-smoke.sh
|
||||
STACK=stage ./run-android-gui-smoke.sh
|
||||
|
||||
# Explorer mint + multi withdraw/pay chain (from gui-workflows)
|
||||
EXP_PW_FILE=$HOME/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank/bank-explorer-password.txt \
|
||||
STACK=goa PKG=net.taler.wallet.fdroid.debug \
|
||||
./run-goa-gui-chain.sh
|
||||
|
||||
# Build fix-branch APK then smoke (GUI=1 for gui smoke)
|
||||
./run-android-build-and-smoke.sh
|
||||
GUI=1 ./run-android-build-and-smoke.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Platform capability flags (planned + defaults)
|
||||
|
||||
Automation can run on hosts that support **Android only**, **GUI tooling only**
|
||||
(conceptual), or **both**. Flags keep that explicit for CI and laptops.
|
||||
|
||||
### Proposed env flags
|
||||
|
||||
| Variable | Values | Meaning |
|
||||
|----------|--------|---------|
|
||||
| `AUTO_ANDROID` | `0` / `1` | Run Android wallet automation (adb, APK, emulator/device) |
|
||||
| `AUTO_GUI` | `0` / `1` | Drive **graphical** UI (uiautomator taps, multi-round). If `0`, deep-link/CLI-only install smoke. |
|
||||
| `AUTO_PLATFORM` | `auto` / `linux` / `macos` / `ios` | Host family (optional override) |
|
||||
| `EMULATOR_HEADLESS` | **`1` (default)** / `0` | No host window (`-no-window`). Server/CI-safe. |
|
||||
| `WINDOWED` | `0` / `1` | Shortcut: `WINDOWED=1` ⇒ show emulator window (`EMULATOR_HEADLESS=0`) |
|
||||
| `EMULATOR_GPU` | default **`swiftshader_indirect`** | Guest GLES still runs headless (layout/taps/screenshots). Override `host` only if windowed + real GPU. |
|
||||
| `AUTO_START_EMULATOR` | **`1` (default)** / `0` | If no adb device, start AVD via `start-android-emulator.sh --wait` |
|
||||
|
||||
---
|
||||
|
||||
## Headless mode (default)
|
||||
|
||||
**Default for all GUI / pay smokes:** no window on the host. Opening an emulator
|
||||
GUI on servers (or headless CI) is wrong; these scripts therefore start the AVD
|
||||
with **`-no-window`** and still keep **graphics logic** in the guest:
|
||||
|
||||
| Piece | Headless default |
|
||||
|-------|------------------|
|
||||
| Host window | **off** (`-no-window`, `QT_QPA_PLATFORM=offscreen`, no `DISPLAY`) |
|
||||
| Guest GLES | **on** via **SwiftShader** (`-gpu swiftshader_indirect`) |
|
||||
| UI automation | **on** — `uiautomator` dump/tap + `screencap` (no X11 needed) |
|
||||
| Audio / boot anim | off (`-no-audio -no-boot-anim`) |
|
||||
|
||||
```bash
|
||||
# explicit (same as default)
|
||||
./start-android-emulator.sh --wait
|
||||
|
||||
# or let smoke auto-start headless AVD when no phone is plugged in
|
||||
STACK=goa ./run-android-gui-smoke.sh
|
||||
|
||||
# laptop: show the emulator window
|
||||
WINDOWED=1 ./start-android-emulator.sh --wait
|
||||
# or: EMULATOR_HEADLESS=0 EMULATOR_GPU=host ./start-android-emulator.sh --wait
|
||||
```
|
||||
|
||||
Shared code: `lib_android_env.sh` + `start-android-emulator.sh`.
|
||||
|
||||
**Semantics:**
|
||||
|
||||
| `AUTO_ANDROID` | `AUTO_GUI` | Behaviour |
|
||||
|----------------|------------|-----------|
|
||||
| `1` | `0` | Android install + deep-link smoke only (no multi-round taps) |
|
||||
| `1` | `1` | Android + graphical drive (vanilla GUI / chain) |
|
||||
| `0` | `1` | Reserved (e.g. future desktop/web GUI); currently **no-op** with a clear message |
|
||||
| `0` | `0` | Skip mobile automation |
|
||||
|
||||
### Defaults by host (when flags unset)
|
||||
|
||||
| Host (`uname -s`) | Default `AUTO_ANDROID` | Default `AUTO_GUI` | Rationale |
|
||||
|-------------------|------------------------|--------------------|-----------|
|
||||
| **Linux** | **`1`** | **`1`** if device/emulator present, else scripts exit 3 | This repo’s day-to-day path; **Android only** (no iOS here) |
|
||||
| **Darwin (macOS)** | `1` | `1` | Can run Android emulators **and** (separately) taler-ios GUI helpers; both flags on for Android scripts; iOS is out of tree |
|
||||
| **Other** | `0` | `0` | Fail closed |
|
||||
|
||||
**Linux default = Android** (no second mobile platform in this suite).
|
||||
**macOS** may enable both ecosystems in the wider monorepo sense; for *these*
|
||||
scripts only Android is implemented — set `AUTO_ANDROID=1` (default) and keep
|
||||
iOS under `taler-ios`.
|
||||
|
||||
### Resolution helper (convention for future wrappers)
|
||||
|
||||
```bash
|
||||
# Example for a future run-all-mobile.sh
|
||||
os=$(uname -s)
|
||||
: "${AUTO_PLATFORM:=auto}"
|
||||
case "$AUTO_PLATFORM" in
|
||||
auto) case "$os" in Linux) AUTO_PLATFORM=linux ;; Darwin) AUTO_PLATFORM=macos ;; *) AUTO_PLATFORM=other ;; esac ;;
|
||||
esac
|
||||
case "$AUTO_PLATFORM" in
|
||||
linux)
|
||||
: "${AUTO_ANDROID:=1}"
|
||||
: "${AUTO_GUI:=1}"
|
||||
# no iOS
|
||||
;;
|
||||
macos)
|
||||
: "${AUTO_ANDROID:=1}"
|
||||
: "${AUTO_GUI:=1}"
|
||||
# optional later: AUTO_IOS=1 for taler-ios scripts
|
||||
;;
|
||||
*)
|
||||
: "${AUTO_ANDROID:=0}"
|
||||
: "${AUTO_GUI:=0}"
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
Scripts **today** implement Android only; they should honour:
|
||||
|
||||
- `AUTO_ANDROID=0` → exit 0 with “skipped (AUTO_ANDROID=0)”
|
||||
- `AUTO_GUI=0` → call deep-link smoke without multi-round GUI (or set `GUI_ROUNDS=0`)
|
||||
|
||||
---
|
||||
|
||||
## Vanilla layers (detail)
|
||||
|
||||
| Layer | What | Tooling |
|
||||
|-------|------|---------|
|
||||
| **A — Install** | F-Droid or from-source APK | `adb install` |
|
||||
| **B — Entry (shortcut)** | `taler://withdraw/…` / `taler://pay…` | `adb am start -a VIEW` |
|
||||
| **C — Graphical UI** | Confirm / ToS / Pay taps; ANR Wait | `uiautomator` + `lib_ui.py` / chain |
|
||||
| **D — Evidence** | Screenshots, XML, logcat, URIs | `out*/` |
|
||||
|
||||
### Legitimate shortcuts
|
||||
|
||||
| Shortcut | Replaces |
|
||||
|----------|----------|
|
||||
| Deep-link withdraw/pay | Camera QR / opening paywall |
|
||||
| Host `POST` template → pay URI | Browser shop checkout UI |
|
||||
| Label-list taps | Human reading button text |
|
||||
| Skip full ToS scroll | Tap Accept if shown |
|
||||
|
||||
### Scripts
|
||||
|
||||
| Script | Role |
|
||||
|--------|------|
|
||||
| `run-android-pay-smoke.sh` | Hybrid: install + deep-link + light taps |
|
||||
| `run-android-gui-smoke.sh` | Multi-round GUI drive |
|
||||
| `run-goa-gui-chain.sh` | Multi mint/withdraw/pay (gui-workflows port) |
|
||||
| `run-android-build-and-smoke.sh` | Build git ref (branch/tag) + smoke (`GUI=1` optional) |
|
||||
| `run-android-variant-matrix.sh` | **published + stable-self + master** (compare builds) |
|
||||
| `start-android-emulator.sh` | Start AVD (**headless default**, SwiftShader) |
|
||||
| `lib_android_env.sh` | Flags, PATH, `android_ensure_device` |
|
||||
| `lib_ui.py` | Dump/tap/ANR helpers |
|
||||
|
||||
### Build variants (anti–“weird local build”)
|
||||
|
||||
| Variant | Source | Severity | Purpose |
|
||||
|---------|--------|----------|---------|
|
||||
| **stable-self** | **only** rebuild of current stable tag | WARN | Self-build vs published for **one** release |
|
||||
| **published** | F-Droid current APK | **≤14d → BLOCKER**, else WARN | What users install |
|
||||
| **older / milestones** | F-Droid APK ~**3 / 6 / 9 / 12 mo** | **always WARN** | No self-build of old tags |
|
||||
| **master** | `origin/master` rebuild | WARN | Tip — never hard-blocks |
|
||||
| **fix** (optional) | inference branch | soft | GOA automation fixes |
|
||||
|
||||
**Self-build:** current stable only (plus master/fix if requested) — not older releases.
|
||||
**Blocker window:** releases ≤ 14 days.
|
||||
**Milestones:** nearest tags to 90/180/270/365d with a still-hosted F-Droid APK.
|
||||
|
||||
---
|
||||
|
||||
## Stacks under test
|
||||
|
||||
| Stack | Withdraw | Pay |
|
||||
|-------|----------|-----|
|
||||
| **GOA** | explorer mint / `demo-withdraw.json` @ bank.hacktivism.ch | goa-shop templates / Paivana pay-template |
|
||||
| **stage** | explorer / demo-withdraw @ stage.bank… | fermes / jardin public templates |
|
||||
|
||||
### Lageübersicht GOA + stage (as-is survey · 2026-07-17 ~22:05–22:18)
|
||||
|
||||
**Device:** headless `emulator-5554` · host ~3.7 GiB RAM (often <300 MiB free) · timeout per leg ≤420 s.
|
||||
**Legs:** Abheben + Bezahlen (`DO_WITHDRAW=1 DO_PAY=1`). Artefakte: `out-survey-20260717-220527/` (gitignored).
|
||||
|
||||
#### Varianten
|
||||
|
||||
| Variante | Quelle | Bemerkung |
|
||||
|----------|--------|-----------|
|
||||
| **stable (published)** | F-Droid `net.taler.wallet.fdroid` **1.6.1 / 854** | as-is |
|
||||
| **master as-is** | rebuild `origin/master` @ `f65a976c` → `.fdroid.debug` | as-is, no patches |
|
||||
| **ältere Releases** (3/6/9/12 mon) | F-Droid APKs für `1.5.0`/`1.3.0`/`1.1.0`/`1.0.8` | **nicht auffindbar** (HTTP miss) → **SKIP** (kein Selbstbau älterer Tags) |
|
||||
| **master + inference** (extra) | local branch `local/master-with-inference` @ `3ebe8b09` | merges `fix-bank-withdraw-auto-exchange` + `fix-withdraw-spinner-fallback` · **not pushed** |
|
||||
|
||||
#### Ergebnis-Matrix
|
||||
|
||||
| Variant | Stack | Install / deep-link | Withdraw URI | Pay URI | wallet-core Abheben | wallet-core Pay | ANR | Exit |
|
||||
|---------|-------|---------------------|--------------|---------|---------------------|-----------------|-----|------|
|
||||
| stable published | **goa** (hacktivism) | OK | OK bank.hacktivism.ch | OK paivana template | **NO** | soft/YES once* | **YES** | **10** |
|
||||
| stable published | **stage** (*.lefrancpaysan) | OK | OK stage.bank… | OK fermes order | **NO** | **NO** | **YES** | **10** |
|
||||
| master as-is | **goa** | OK | OK | OK paivana | **NO** | **NO** | **YES** | **10** |
|
||||
| master as-is | **stage** | OK | OK | OK fermes | **NO** | **NO** | **YES** | **10** |
|
||||
| master+inference | **goa** | OK | OK | OK | **NO** | **NO** | **YES** | **10** |
|
||||
| master+inference | **stage** | OK | OK | OK | **NO** | **NO** | **YES** | **10** |
|
||||
|
||||
\*stable-goa console once reported pay wallet-core YES mid-run; final score still ANR-dominated / not reliable settlement.
|
||||
|
||||
#### Was pro Stack **steht** (Infra)
|
||||
|
||||
| Check | **goa / hacktivism** | **stage / \*.lefrancpaysan** |
|
||||
|-------|----------------------|------------------------------|
|
||||
| Bank `…/intro/demo-withdraw.json` → `taler://withdraw/…` | **OK** | **OK** |
|
||||
| Pay entry | paivana `pay-template` **OK** | fermes `panier-legumes` POST → `taler://pay/…` **OK** |
|
||||
| adb `VIEW` Intent delivery | **OK** | **OK** |
|
||||
| Unattended Confirm/Abheben/Pay GUI | **FAIL (ANR)** | **FAIL (ANR)** |
|
||||
| wallet-core prepare/accept withdraw in logcat | **not seen** | **not seen** |
|
||||
|
||||
**Fazit as-is:** Beide Stacks liefern die **Eingänge** (Withdraw- + Pay-URIs). Die **GUI/wallet-core-Strecke** ist auf diesem Emulator für **alle** getesteten App-Varianten (stable, master, inference) **gleich tot** — System UI / Android System ANR, keine brauchbaren Taps. Das ist **kein Stack-Unterschied** GOA vs stage und **kein** belastbarer App-Versions-Vergleich.
|
||||
|
||||
#### Inference-Patch-Versuch (lokal, **kein Push**)
|
||||
|
||||
Weil **nichts** der as-is-Varianten wallet-core-grün war, wurde zusätzlich gebaut:
|
||||
|
||||
```text
|
||||
taler-android branch: local/master-with-inference # NOT pushed
|
||||
base: origin/master
|
||||
merged: origin/dev/hernani-inference/fix-bank-withdraw-auto-exchange
|
||||
origin/dev/hernani-inference/fix-withdraw-spinner-fallback
|
||||
commit: 3ebe8b091379
|
||||
APK: apks/wallet-master-inference-3ebe8b091379.apk (local)
|
||||
```
|
||||
|
||||
| Stack | Inference smoke | Besser als master/stable? |
|
||||
|-------|-----------------|---------------------------|
|
||||
| goa | exit 10, ANR, no withdraw core | **Nein** |
|
||||
| stage | exit 10, ANR, no withdraw core | **Nein** |
|
||||
|
||||
**Kein grünes Inference-Signal** auf diesem Host → wir können **nicht** belegen, dass die Patches Withdraw/Pay „retten“.
|
||||
Ebenso können wir **nicht** belegen, dass master ohne Patches „schuld“ ist: die Umgebung erreicht wallet-core gar nicht.
|
||||
|
||||
> **Wenn später auf Gerät/mehr RAM die Inference-APK plötzlich grün und master/stable rot ist:**
|
||||
> das ist der **klare Fix-Hinweis** — Patches aus `dev/hernani-inference/*` upstreamen / in master holen.
|
||||
> **Heute:** noch **nicht** der Fall; primärer Blocker = **Emulator/RAM/ANR**.
|
||||
|
||||
#### Nächste sinnvolle Schritte (später)
|
||||
|
||||
1. Physisches Gerät oder Host ≥6–8 GiB free RAM
|
||||
2. Nur **eine** Wallet-Package-ID installiert (kein Open-with)
|
||||
3. Survey wiederholen: stable · master · `local/master-with-inference` auf **goa + stage**
|
||||
4. Ältere F-Droid-APKs nur wenn wieder gehostet; kein Selbstbau alter Tags
|
||||
|
||||
---
|
||||
|
||||
## Upstream branches (`taler-android` `dev/hernani-inference/*`)
|
||||
|
||||
| Branch | Role |
|
||||
|--------|------|
|
||||
| `gui-workflows` | macOS emulator helpers + original `goa-chain-emu.sh` |
|
||||
| `fix/bank-withdraw-auto-exchange` | **Minimal** GOA exchange auto-add (default build branch) |
|
||||
| `fix/withdraw-spinner-fallback` | Spinner + OIM; use if spinner still hangs |
|
||||
| `experimental-oim*` | Optional cash UI |
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [x] Minimal variant documented
|
||||
- [x] Minimalvariante: was geht / was nicht (Beschränkungen)
|
||||
- [x] Two stacks (goa / stage)
|
||||
- [x] Dual-stack run documented (2026-07-17: both FAIL ANR, landings OK)
|
||||
- [x] Flags design (`AUTO_ANDROID` / `AUTO_GUI` / platform defaults)
|
||||
- [x] **Headless emulator default** (`EMULATOR_HEADLESS=1`, SwiftShader)
|
||||
- [x] Linux default = Android suite only
|
||||
- [x] Inference-branch policy
|
||||
- [ ] Wrapper enforces flags in every entry script (incremental)
|
||||
- [ ] Reliable unattended Confirm on low-RAM Linux emulator
|
||||
111
android-test/README.md
Normal file
111
android-test/README.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Android wallet smoke (Abheben + Bezahlen)
|
||||
|
||||
Smokes always cover **withdraw first**, then **pay** (not pay-only).
|
||||
|
||||
| Leg | Default | What |
|
||||
|-----|---------|------|
|
||||
| **1 WITHDRAW** | on | `taler://withdraw/…` + Confirm/Abheben taps |
|
||||
| **2 PAY** | on | `taler://pay…` / pay-template + Pay taps |
|
||||
|
||||
```bash
|
||||
# needs adb device/emulator
|
||||
./run-android-pay-smoke.sh
|
||||
STACK=goa ./run-android-pay-smoke.sh
|
||||
DO_PAY=0 ./run-android-pay-smoke.sh # withdraw only
|
||||
DO_WITHDRAW=0 DO_PAY=1 … # pay only (unusual)
|
||||
```
|
||||
|
||||
APK: F-Droid **1.6.1** (`net.taler.wallet.fdroid`).
|
||||
|
||||
## 2) Source build + same tests
|
||||
|
||||
Builds `taler-android` at a git **ref** (`:wallet:assembleFdroidDebug`), then
|
||||
runs the **same** smoke (install + withdraw/pay deep-links).
|
||||
|
||||
```bash
|
||||
# default: inference fix branch (GOA withdraw reliability)
|
||||
./run-android-build-and-smoke.sh
|
||||
# tip of master
|
||||
BRANCH=master ./run-android-build-and-smoke.sh
|
||||
# self-build of published stable **tag** (same release as F-Droid 1.6.1)
|
||||
BRANCH=wallet-1.6.1 LABEL=stable-self ./run-android-build-and-smoke.sh
|
||||
SKIP_PULL=1 BRANCH=master ./run-android-build-and-smoke.sh
|
||||
TALER_ANDROID_SRC=/path/to/taler-android ./run-android-build-and-smoke.sh
|
||||
```
|
||||
|
||||
Built package id: `net.taler.wallet.fdroid.debug`.
|
||||
Needs `ANDROID_HOME` (e.g. `$HOME/Android/Sdk`), Java 17+, network for Maven.
|
||||
|
||||
## 2b) Variant matrix (published + stable self-build + master)
|
||||
|
||||
Runs several smokes so a single odd developer build cannot be the only sample:
|
||||
|
||||
| Variant | What |
|
||||
|---------|------|
|
||||
| Order | Variant | Severity | What |
|
||||
|------|---------|----------|------|
|
||||
| **1** | **stable-self** | WARN | **Only** self-build: current stable tag |
|
||||
| **2** | **published** | age ≤14d → **BLOCKER**, else WARN | Current F-Droid APK |
|
||||
| **3** | **older / milestones** | **always WARN** | F-Droid APKs nearest **3 / 6 / 9 / 12 months** (if still hosted) — **no self-build** |
|
||||
| **4** | **master** | WARN | Rebuild `origin/master` |
|
||||
|
||||
**Self-build policy:** only **current published stable** (`stable-self`) + optional **master**.
|
||||
Historical versions: published F-Droid APK only; skip if APK gone.
|
||||
**Milestones:** `MILESTONE_DAYS="90 180 270 365"` (±50d), warn only.
|
||||
**Young releases** (≤ 14 days): hard blocker on fail.
|
||||
|
||||
```bash
|
||||
./run-android-variant-matrix.sh
|
||||
STACK=goa ./run-android-variant-matrix.sh
|
||||
INCLUDE_OLDER=0 VARIANTS=stable-self,published,master ./run-android-variant-matrix.sh
|
||||
# custom milestones (days):
|
||||
MILESTONE_DAYS="90 180 270 365" ./run-android-variant-matrix.sh
|
||||
```
|
||||
|
||||
Artifacts + `SUMMARY.txt` under `out-matrix-<timestamp>/`.
|
||||
|
||||
## 3) Graphical UI (vanilla)
|
||||
|
||||
Deep-link entry (≡ QR) **plus** multi-round uiautomator taps:
|
||||
|
||||
```bash
|
||||
STACK=goa ./run-android-gui-smoke.sh
|
||||
STACK=stage ./run-android-gui-smoke.sh
|
||||
# after source build:
|
||||
GUI=1 ./run-android-build-and-smoke.sh
|
||||
```
|
||||
|
||||
## 4) Full GOA/stage chain (from `dev/hernani-inference/gui-workflows`)
|
||||
|
||||
Bank API mint → withdraw deep-link → taps → template pay → taps:
|
||||
|
||||
```bash
|
||||
# needs explorer password + device
|
||||
EXP_PW_FILE=$HOME/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank/bank-explorer-password.txt \
|
||||
STACK=goa PKG=net.taler.wallet.fdroid.debug \
|
||||
./run-goa-gui-chain.sh
|
||||
|
||||
STACK=stage ./run-goa-gui-chain.sh
|
||||
```
|
||||
|
||||
Canonical notes (minimal variant, **flags**, Linux defaults):
|
||||
**[GUI-AUTOMATION-NOTES.md](./GUI-AUTOMATION-NOTES.md)**.
|
||||
|
||||
| Flag | Linux default | Meaning |
|
||||
|------|---------------|---------|
|
||||
| `AUTO_ANDROID` | `1` | Run Android adb/APK automation |
|
||||
| `AUTO_GUI` | `1` | Graphical uiautomator drive (else deep-link only) |
|
||||
| `AUTO_PLATFORM` | `auto` → `linux` | Host family; macOS can later pair with iOS out of tree |
|
||||
| **`EMULATOR_HEADLESS`** | **`1`** | **No host window** (`-no-window`). Server-safe. |
|
||||
| `EMULATOR_GPU` | `swiftshader_indirect` | Guest GLES still active for UI/taps/screenshots |
|
||||
| `AUTO_START_EMULATOR` | `1` | Auto-start headless AVD if no adb device |
|
||||
| `WINDOWED` | `0` | `WINDOWED=1` → show emulator window |
|
||||
|
||||
```bash
|
||||
# headless AVD only (default path for servers)
|
||||
./start-android-emulator.sh --wait
|
||||
# windowed (laptops)
|
||||
WINDOWED=1 ./start-android-emulator.sh --wait
|
||||
```
|
||||
|
||||
See also parent [VERSIONS.md](../VERSIONS.md).
|
||||
158
android-test/lib_android_env.sh
Normal file
158
android-test/lib_android_env.sh
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# shellcheck shell=bash
|
||||
# Shared Android env for GUI / adb smokes.
|
||||
# Source: . "$ROOT/lib_android_env.sh"
|
||||
#
|
||||
# Defaults favour **servers / CI**: no host window (headless emulator), while the
|
||||
# guest still runs GLES via SwiftShader so UI layout / uiautomator work.
|
||||
#
|
||||
# See GUI-AUTOMATION-NOTES.md → Headless mode
|
||||
|
||||
# --- SDK / PATH ---------------------------------------------------------------
|
||||
export ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}"
|
||||
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
|
||||
for d in \
|
||||
"$ANDROID_HOME/platform-tools" \
|
||||
"$ANDROID_HOME/emulator" \
|
||||
"$ANDROID_HOME/cmdline-tools/latest/bin"
|
||||
do
|
||||
[ -d "$d" ] && case ":$PATH:" in *":$d:"*) ;; *) PATH="$d:$PATH" ;; esac
|
||||
done
|
||||
export PATH
|
||||
|
||||
# AVD path (some hosts keep AVDs under ~/.config/.android)
|
||||
if [ -z "${ANDROID_AVD_HOME:-}" ]; then
|
||||
if [ -d "$HOME/.android/avd" ]; then
|
||||
export ANDROID_AVD_HOME="$HOME/.android/avd"
|
||||
elif [ -d "$HOME/.config/.android/avd" ]; then
|
||||
export ANDROID_AVD_HOME="$HOME/.config/.android/avd"
|
||||
# emulator also looks at ~/.android/avd — soft-link if missing
|
||||
if [ ! -e "$HOME/.android/avd" ]; then
|
||||
mkdir -p "$HOME/.android"
|
||||
ln -sfn "$HOME/.config/.android/avd" "$HOME/.android/avd" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Platform / feature flags -------------------------------------------------
|
||||
os=$(uname -s)
|
||||
case "${AUTO_PLATFORM:-auto}" in
|
||||
auto)
|
||||
case "$os" in
|
||||
Linux) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
|
||||
Darwin) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
|
||||
*) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;;
|
||||
esac
|
||||
;;
|
||||
linux|macos) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
|
||||
*) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;;
|
||||
esac
|
||||
|
||||
# Headless = default (no X11/Wayland window on host). GUI *logic* still runs in
|
||||
# the guest via software GPU. Set EMULATOR_HEADLESS=0 or WINDOWED=1 for a window.
|
||||
: "${EMULATOR_HEADLESS:=1}"
|
||||
if [ "${WINDOWED:-0}" = "1" ]; then
|
||||
EMULATOR_HEADLESS=0
|
||||
fi
|
||||
export EMULATOR_HEADLESS
|
||||
|
||||
# Soft GLES for headless; windowed may override to host/auto if set by user.
|
||||
if [ "${EMULATOR_HEADLESS}" = "1" ]; then
|
||||
: "${EMULATOR_GPU:=swiftshader_indirect}"
|
||||
else
|
||||
: "${EMULATOR_GPU:=swiftshader_indirect}" # still safe default; host GPU: EMULATOR_GPU=host
|
||||
fi
|
||||
export EMULATOR_GPU
|
||||
|
||||
: "${EMULATOR_AVD:=TalerWallet34}"
|
||||
: "${EMULATOR_MEMORY:=1536}"
|
||||
: "${EMULATOR_CORES:=2}"
|
||||
# If no adb device: start headless AVD (0 = require phone/manual emulator)
|
||||
: "${AUTO_START_EMULATOR:=1}"
|
||||
: "${EMULATOR_BOOT_TIMEOUT:=360}"
|
||||
: "${EMULATOR_LOG:=/tmp/taler-android-emulator.log}"
|
||||
: "${EMULATOR_NO_SNAPSHOT:=1}"
|
||||
|
||||
android_adb_serial() {
|
||||
adb devices 2>/dev/null | awk '/\tdevice$/{print $1; exit}'
|
||||
}
|
||||
|
||||
android_has_device() {
|
||||
[ -n "$(android_adb_serial)" ]
|
||||
}
|
||||
|
||||
# Build emulator argv for current headless/windowed mode.
|
||||
# Prints args; caller exec/nohup.
|
||||
android_emulator_args() {
|
||||
local avd="${1:-$EMULATOR_AVD}"
|
||||
local args=( -avd "$avd" -gpu "$EMULATOR_GPU" -no-audio -no-boot-anim )
|
||||
if [ "${EMULATOR_HEADLESS}" = "1" ]; then
|
||||
args+=( -no-window )
|
||||
# Avoid Qt trying to open a display on servers
|
||||
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-offscreen}"
|
||||
fi
|
||||
# Broken default_boot snapshots leave adb offline forever — cold boot by default.
|
||||
if [ "${EMULATOR_NO_SNAPSHOT:-1}" = "1" ]; then
|
||||
args+=( -no-snapshot -no-snapshot-load -no-snapshot-save )
|
||||
fi
|
||||
if [ -n "${EMULATOR_MEMORY:-}" ]; then
|
||||
args+=( -memory "$EMULATOR_MEMORY" )
|
||||
fi
|
||||
if [ -n "${EMULATOR_CORES:-}" ]; then
|
||||
args+=( -cores "$EMULATOR_CORES" )
|
||||
fi
|
||||
# extra user flags
|
||||
# shellcheck disable=SC2206
|
||||
if [ -n "${EMULATOR_EXTRA_ARGS:-}" ]; then
|
||||
# intentional word-split for extra flags
|
||||
args+=( ${EMULATOR_EXTRA_ARGS} )
|
||||
fi
|
||||
printf '%s\n' "${args[@]}"
|
||||
}
|
||||
|
||||
# Ensure an adb "device" is online. Optionally start headless emulator.
|
||||
# Sets SERIAL / ANDROID_SERIAL on success. Returns 0 ok, 3 no device.
|
||||
android_ensure_device() {
|
||||
local serial
|
||||
serial="${SERIAL:-${ANDROID_SERIAL:-}}"
|
||||
if [ -z "$serial" ]; then
|
||||
serial=$(android_adb_serial || true)
|
||||
fi
|
||||
if [ -n "$serial" ]; then
|
||||
SERIAL="$serial"
|
||||
export ANDROID_SERIAL="$serial"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "${AUTO_START_EMULATOR}" != "1" ]; then
|
||||
echo "no adb device (AUTO_START_EMULATOR=0 — plug phone or start AVD manually)" >&2
|
||||
return 3
|
||||
fi
|
||||
|
||||
if ! command -v emulator >/dev/null 2>&1; then
|
||||
echo "no adb device and emulator binary missing under $ANDROID_HOME" >&2
|
||||
return 3
|
||||
fi
|
||||
|
||||
echo "no adb device → starting AVD ${EMULATOR_AVD} (headless=${EMULATOR_HEADLESS} gpu=${EMULATOR_GPU})"
|
||||
local starter
|
||||
starter="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/start-android-emulator.sh"
|
||||
if [ ! -x "$starter" ]; then
|
||||
echo "missing $starter" >&2
|
||||
return 3
|
||||
fi
|
||||
EMULATOR_HEADLESS="$EMULATOR_HEADLESS" EMULATOR_GPU="$EMULATOR_GPU" \
|
||||
EMULATOR_AVD="$EMULATOR_AVD" EMULATOR_MEMORY="$EMULATOR_MEMORY" \
|
||||
EMULATOR_CORES="$EMULATOR_CORES" EMULATOR_LOG="$EMULATOR_LOG" \
|
||||
EMULATOR_BOOT_TIMEOUT="$EMULATOR_BOOT_TIMEOUT" \
|
||||
"$starter" --wait || return 3
|
||||
|
||||
serial=$(android_adb_serial || true)
|
||||
if [ -z "$serial" ]; then
|
||||
echo "emulator started but no adb device yet — see $EMULATOR_LOG" >&2
|
||||
return 3
|
||||
fi
|
||||
SERIAL="$serial"
|
||||
export ANDROID_SERIAL="$serial"
|
||||
echo "device: $SERIAL (headless=${EMULATOR_HEADLESS})"
|
||||
return 0
|
||||
}
|
||||
133
android-test/lib_release_age.sh
Normal file
133
android-test/lib_release_age.sh
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# shellcheck shell=bash
|
||||
# Release age helpers for Android wallet matrix.
|
||||
# Source after ROOT/lib_android_env.sh (optional).
|
||||
#
|
||||
# BLOCKER only when a *released* app is within BLOCKER_MAX_AGE_DAYS (default 14).
|
||||
# Older releases: try them, but failures are WARN only.
|
||||
|
||||
: "${BLOCKER_MAX_AGE_DAYS:=14}"
|
||||
: "${TALER_ANDROID_SRC:=$HOME/taler/taler-android}"
|
||||
|
||||
# Days since git tag (tagger/committer date). Empty if unknown.
|
||||
# Usage: release_age_days wallet-1.6.1 → e.g. 28
|
||||
release_age_days() {
|
||||
local tag="$1"
|
||||
local src="${2:-$TALER_ANDROID_SRC}"
|
||||
[ -d "$src/.git" ] || return 1
|
||||
local epoch
|
||||
epoch=$(git -C "$src" log -1 --format=%ct "refs/tags/${tag}" 2>/dev/null || true)
|
||||
[ -n "$epoch" ] || epoch=$(git -C "$src" log -1 --format=%ct "$tag" 2>/dev/null || true)
|
||||
[ -n "$epoch" ] || return 1
|
||||
local now
|
||||
now=$(date +%s)
|
||||
echo $(( (now - epoch) / 86400 ))
|
||||
}
|
||||
|
||||
# severity from age: blocker if age_days <= BLOCKER_MAX_AGE_DAYS, else warn
|
||||
# Unknown age → warn (never accidental hard fail on mystery builds)
|
||||
severity_from_age_days() {
|
||||
local days="${1:-}"
|
||||
if [ -z "$days" ] || ! [[ "$days" =~ ^[0-9]+$ ]]; then
|
||||
echo warn
|
||||
return
|
||||
fi
|
||||
if [ "$days" -le "${BLOCKER_MAX_AGE_DAYS}" ]; then
|
||||
echo blocker
|
||||
else
|
||||
echo warn
|
||||
fi
|
||||
}
|
||||
|
||||
severity_for_tag() {
|
||||
local tag="$1"
|
||||
local days
|
||||
days=$(release_age_days "$tag" 2>/dev/null || true)
|
||||
severity_from_age_days "$days"
|
||||
}
|
||||
|
||||
# List wallet-* release tags newest first (exclude rc/dev).
|
||||
# Args: max count (default 5). Pass 0 or "all" for no limit.
|
||||
list_wallet_release_tags() {
|
||||
local n="${1:-5}"
|
||||
local src="${TALER_ANDROID_SRC:-$HOME/taler/taler-android}"
|
||||
[ -d "$src/.git" ] || return 1
|
||||
git -C "$src" fetch --tags origin 2>/dev/null || true
|
||||
local cmd=(git -C "$src" for-each-ref --sort=-creatordate --format='%(refname:short)' refs/tags)
|
||||
if [ "$n" = "all" ] || [ "$n" = "0" ]; then
|
||||
"${cmd[@]}" | grep -E '^wallet-[0-9]+\.[0-9]+(\.[0-9]+)?$'
|
||||
else
|
||||
"${cmd[@]}" | grep -E '^wallet-[0-9]+\.[0-9]+(\.[0-9]+)?$' | head -n "$n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Pick one tag closest to each age milestone (days). Always intended as WARN samples.
|
||||
# Default milestones: 3,6,9,12 months ≈ 90,180,270,365 days.
|
||||
# Env: MILESTONE_DAYS="90 180 270 365" MILESTONE_TOLERANCE_DAYS=50
|
||||
# Prints lines: TAG AGE_DAYS TARGET_DAYS (only when a tag within tolerance exists)
|
||||
select_milestone_tags() {
|
||||
local src="${TALER_ANDROID_SRC:-$HOME/taler/taler-android}"
|
||||
local milestones="${MILESTONE_DAYS:-90 180 270 365}"
|
||||
local tol="${MILESTONE_TOLERANCE_DAYS:-50}"
|
||||
local skip="${1:-}" # optional tag to exclude (e.g. current STABLE_TAG)
|
||||
local -a tags=()
|
||||
mapfile -t tags < <(list_wallet_release_tags all 2>/dev/null || true)
|
||||
[ "${#tags[@]}" -gt 0 ] || return 0
|
||||
|
||||
local target best_tag best_age best_delta age t delta
|
||||
local -A used=()
|
||||
for target in $milestones; do
|
||||
best_tag=""
|
||||
best_age=""
|
||||
best_delta=99999
|
||||
for t in "${tags[@]}"; do
|
||||
[ -n "$t" ] || continue
|
||||
[ "$t" = "$skip" ] && continue
|
||||
[ -n "${used[$t]:-}" ] && continue
|
||||
age=$(release_age_days "$t" "$src" 2>/dev/null || true)
|
||||
[[ "$age" =~ ^[0-9]+$ ]] || continue
|
||||
delta=$(( age > target ? age - target : target - age ))
|
||||
if [ "$delta" -le "$tol" ] && [ "$delta" -lt "$best_delta" ]; then
|
||||
best_delta=$delta
|
||||
best_tag=$t
|
||||
best_age=$age
|
||||
fi
|
||||
done
|
||||
if [ -n "$best_tag" ]; then
|
||||
used[$best_tag]=1
|
||||
echo "$best_tag $best_age $target"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Guess F-Droid APK URL for a wallet version name (e.g. 1.6.1).
|
||||
# Uses versionCode from tag's build.gradle if possible, else STABLE mapping.
|
||||
# Prints: versionCode apk_url or returns 1
|
||||
fdroid_apk_for_version_name() {
|
||||
local vn="$1" # 1.6.1
|
||||
local src="${TALER_ANDROID_SRC:-$HOME/taler/taler-android}"
|
||||
local tag="wallet-${vn}"
|
||||
local vc=""
|
||||
if git -C "$src" cat-file -e "refs/tags/${tag}:wallet/build.gradle" 2>/dev/null; then
|
||||
vc=$(git -C "$src" show "refs/tags/${tag}:wallet/build.gradle" 2>/dev/null \
|
||||
| sed -n 's/^[[:space:]]*versionCode[[:space:]]\+\([0-9]\+\).*/\1/p' | head -1)
|
||||
fi
|
||||
# ABI x86_64 → +4 in override scheme 10*vc+abi (common F-Droid multi-apk)
|
||||
# Prefer universal-style code used by F-Droid: for 1.6.1 → 854 = 10*85+4
|
||||
local apk_vc=""
|
||||
if [ -n "$vc" ]; then
|
||||
apk_vc=$((10 * vc + 4))
|
||||
fi
|
||||
# Known overrides / suggested
|
||||
case "$vn" in
|
||||
1.6.1) apk_vc=854 ;;
|
||||
esac
|
||||
[ -n "$apk_vc" ] || return 1
|
||||
echo "${apk_vc}|https://f-droid.org/repo/net.taler.wallet.fdroid_${apk_vc}.apk"
|
||||
}
|
||||
|
||||
# Check HTTP HEAD/GET that APK exists (0 = ok)
|
||||
fdroid_apk_exists() {
|
||||
local url="$1"
|
||||
curl -sfS -m 15 -o /dev/null -r 0-0 "$url" 2>/dev/null \
|
||||
|| curl -sfS -m 15 -o /dev/null -I "$url" 2>/dev/null
|
||||
}
|
||||
192
android-test/lib_ui.py
Executable file
192
android-test/lib_ui.py
Executable 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,
|
||||
}
|
||||
167
android-test/run-android-build-and-smoke.sh
Executable file
167
android-test/run-android-build-and-smoke.sh
Executable file
|
|
@ -0,0 +1,167 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build GNU Taler Android wallet from source at a git **ref**, then run the same
|
||||
# adb deep-link (or GUI) smoke as run-android-pay-smoke.sh.
|
||||
#
|
||||
# Source (default): $HOME/taler/taler-android
|
||||
# Build: ./gradlew :wallet:assembleFdroidDebug
|
||||
# Package id: net.taler.wallet.fdroid.debug
|
||||
#
|
||||
# Typical refs:
|
||||
# BRANCH=master # tip of master
|
||||
# BRANCH=wallet-1.6.1 # published stable **tag** (self-build)
|
||||
# BRANCH=dev/hernani-inference/fix-… # GOA automation fix branch
|
||||
#
|
||||
# For a multi-variant matrix (master + stable self-build + published APK):
|
||||
# ./run-android-variant-matrix.sh
|
||||
#
|
||||
# Usage:
|
||||
# ./run-android-build-and-smoke.sh
|
||||
# BRANCH=master STACK=goa ./run-android-build-and-smoke.sh
|
||||
# BRANCH=wallet-1.6.1 LABEL=stable-self ./run-android-build-and-smoke.sh
|
||||
# SKIP_PULL=1 BRANCH=master ./run-android-build-and-smoke.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
SMOKE="$ROOT/run-android-pay-smoke.sh"
|
||||
SRC="${TALER_ANDROID_SRC:-$HOME/taler/taler-android}"
|
||||
SKIP_PULL="${SKIP_PULL:-0}"
|
||||
# Prefer existing inference fix branch for GOA withdraw (spinner/exchange resolve).
|
||||
# Matrix uses BRANCH=master and BRANCH=$STABLE_TAG explicitly.
|
||||
BRANCH="${BRANCH:-dev/hernani-inference/fix-bank-withdraw-auto-exchange}"
|
||||
# Optional human label for logs / artifact names (default = sanitized BRANCH)
|
||||
LABEL="${LABEL:-}"
|
||||
VARIANT="${VARIANT:-fdroidDebug}"
|
||||
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
|
||||
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
|
||||
|
||||
if [ ! -x "$SRC/gradlew" ]; then
|
||||
echo "taler-android not found at $SRC (set TALER_ANDROID_SRC=…)" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ ! -d "$ANDROID_HOME" ]; then
|
||||
echo "ANDROID_HOME missing: $ANDROID_HOME" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
printf 'sdk.dir=%s\n' "$ANDROID_HOME" >"$SRC/local.properties"
|
||||
|
||||
# --- checkout REF (branch, origin/branch, or tag) --------------------------------
|
||||
cd "$SRC"
|
||||
if [ -d .git ]; then
|
||||
git fetch origin --tags 2>/dev/null || git fetch origin 2>/dev/null || true
|
||||
REF="$BRANCH"
|
||||
checked=0
|
||||
if [ -n "$REF" ]; then
|
||||
if git rev-parse --verify "refs/tags/${REF}" >/dev/null 2>&1; then
|
||||
echo "checkout tag ${REF} (stable / release self-build)"
|
||||
git checkout -f "refs/tags/${REF}"
|
||||
checked=1
|
||||
SKIP_PULL=1
|
||||
elif git rev-parse --verify "origin/${REF}" >/dev/null 2>&1; then
|
||||
echo "checkout origin/${REF}"
|
||||
git checkout -B "$REF" "origin/${REF}" 2>/dev/null || git checkout "$REF" 2>/dev/null || true
|
||||
checked=1
|
||||
elif git rev-parse --verify "${REF}" >/dev/null 2>&1; then
|
||||
echo "checkout ${REF}"
|
||||
git checkout -f "$REF"
|
||||
checked=1
|
||||
else
|
||||
echo "WARN: ref '${REF}' not found locally after fetch — staying on current HEAD" >&2
|
||||
fi
|
||||
fi
|
||||
if [ "$SKIP_PULL" != "1" ] && [ "$checked" = "1" ]; then
|
||||
# only pull real branches, never tags
|
||||
if git symbolic-ref -q HEAD >/dev/null 2>&1; then
|
||||
br=$(git rev-parse --abbrev-ref HEAD)
|
||||
git pull --ff-only origin "$br" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
COMMIT=$(git rev-parse --short=12 HEAD 2>/dev/null || echo unknown)
|
||||
HEAD_DESC=$(git describe --tags --always 2>/dev/null || echo "$COMMIT")
|
||||
BR_NOW=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo detached)
|
||||
if [ -z "$LABEL" ]; then
|
||||
LABEL=$(printf '%s' "$BRANCH" | tr '/:' '--' | tr -c 'A-Za-z0-9._-' '-' | cut -c1-48)
|
||||
fi
|
||||
echo "source: $SRC @ $COMMIT ($HEAD_DESC) ref=$BRANCH label=$LABEL head=$BR_NOW"
|
||||
echo "variant: $VARIANT"
|
||||
|
||||
# Map VARIANT → gradle task
|
||||
case "$VARIANT" in
|
||||
fdroidDebug|FdroidDebug) task=assembleFdroidDebug ;;
|
||||
fdroidRelease|FdroidRelease) task=assembleFdroidRelease ;;
|
||||
nightlyDebug|NightlyDebug) task=assembleNightlyDebug ;;
|
||||
googleDebug|GoogleDebug) task=assembleGoogleDebug ;;
|
||||
*)
|
||||
task="assemble$(printf '%s' "$VARIANT" | python3 -c 'import sys; s=sys.stdin.read().strip(); print(s[:1].upper()+s[1:] if s else "FdroidDebug")')"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Low-RAM hosts: cap JVM
|
||||
export GRADLE_OPTS="${GRADLE_OPTS:--Xmx1280m -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=1}"
|
||||
echo "gradle :wallet:$task …"
|
||||
./gradlew ":wallet:$task" --no-daemon --max-workers="${GRADLE_MAX_WORKERS:-1}" \
|
||||
-Dorg.gradle.jvmargs="${GRADLE_JVMARGS:--Xmx1280m}"
|
||||
|
||||
# Prefer fdroid debug APK if multiple exist
|
||||
APK=$(find "$SRC/wallet/build/outputs/apk" -path '*fdroid*debug*.apk' -type f 2>/dev/null | sort | tail -1)
|
||||
if [ -z "$APK" ] || [ ! -f "$APK" ]; then
|
||||
APK=$(find "$SRC/wallet/build/outputs/apk" -name '*.apk' -type f 2>/dev/null | sort | tail -1)
|
||||
fi
|
||||
if [ -z "$APK" ] || [ ! -f "$APK" ]; then
|
||||
echo "no APK under wallet/build/outputs/apk" >&2
|
||||
exit 4
|
||||
fi
|
||||
echo "built apk: $APK ($(wc -c <"$APK") bytes)"
|
||||
|
||||
PKG="${PKG:-}"
|
||||
if [ -z "$PKG" ] && command -v aapt >/dev/null 2>&1; then
|
||||
PKG=$(aapt dump badging "$APK" 2>/dev/null | sed -n "s/^package: name='\([^']*\)'.*/\1/p" | head -1)
|
||||
fi
|
||||
if [ -z "$PKG" ] && command -v aapt2 >/dev/null 2>&1; then
|
||||
PKG=$(aapt2 dump badging "$APK" 2>/dev/null | sed -n "s/^package: name='\([^']*\)'.*/\1/p" | head -1)
|
||||
fi
|
||||
: "${PKG:=net.taler.wallet.fdroid.debug}"
|
||||
|
||||
mkdir -p "$ROOT/apks" "$ROOT/out"
|
||||
STAMP_APK="$ROOT/apks/wallet-${LABEL}-${COMMIT}.apk"
|
||||
cp -f "$APK" "$STAMP_APK"
|
||||
echo "copied → $STAMP_APK"
|
||||
{
|
||||
echo "label=$LABEL"
|
||||
echo "ref=$BRANCH"
|
||||
echo "commit=$COMMIT"
|
||||
echo "describe=$HEAD_DESC"
|
||||
echo "apk=$STAMP_APK"
|
||||
echo "pkg=$PKG"
|
||||
echo "task=$task"
|
||||
} | tee "$ROOT/out/build-${LABEL}.txt"
|
||||
echo "$COMMIT" >"$ROOT/out/build-commit.txt"
|
||||
echo "$APK" >"$ROOT/out/build-apk-path.txt"
|
||||
|
||||
export APK_PATH="$STAMP_APK"
|
||||
export APK_NAME
|
||||
APK_NAME="$(basename "$STAMP_APK")"
|
||||
export PKG
|
||||
export OUT_DIR="${OUT_DIR:-$ROOT/out-source-$LABEL}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
cp -f "$ROOT/out/build-${LABEL}.txt" "$OUT_DIR/build-meta.txt" 2>/dev/null || true
|
||||
|
||||
SMOKE_CMD="$SMOKE"
|
||||
if [ "${GUI:-0}" = "1" ]; then
|
||||
SMOKE_CMD="$ROOT/run-android-gui-smoke.sh"
|
||||
case "$OUT_DIR" in
|
||||
*-gui) ;;
|
||||
*) OUT_DIR="${OUT_DIR}-gui"; export OUT_DIR; mkdir -p "$OUT_DIR" ;;
|
||||
esac
|
||||
fi
|
||||
echo "=== smoke label=$LABEL PKG=$PKG GUI=${GUI:-0} OUT_DIR=$OUT_DIR ==="
|
||||
# Do not exec: allow matrix wrapper to run multiple variants
|
||||
set +e
|
||||
env APK_DIR="$ROOT/apks" APK_NAME="$APK_NAME" APK_PATH="$APK_PATH" PKG="$PKG" OUT_DIR="$OUT_DIR" \
|
||||
"$SMOKE_CMD"
|
||||
rc=$?
|
||||
set -e
|
||||
echo "=== smoke label=$LABEL exit=$rc ==="
|
||||
exit "$rc"
|
||||
201
android-test/run-android-gui-smoke.sh
Executable file
201
android-test/run-android-gui-smoke.sh
Executable file
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env bash
|
||||
# GUI-oriented Android smoke: **withdraw (Abheben)** then **pay**, each with
|
||||
# deep-link entry + multi-round uiautomator taps. Not pay-only.
|
||||
#
|
||||
# 1) taler://withdraw/… + Confirm/Abheben/ToS taps
|
||||
# 2) taler://pay… + Pay/Confirm taps
|
||||
#
|
||||
# See GUI-AUTOMATION-NOTES.md
|
||||
#
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib_android_env.sh
|
||||
. "$ROOT/lib_android_env.sh"
|
||||
|
||||
[ "${AUTO_ANDROID}" = "1" ] || { echo "skipped: AUTO_ANDROID=${AUTO_ANDROID}"; exit 0; }
|
||||
if [ "${AUTO_GUI}" != "1" ]; then
|
||||
echo "AUTO_GUI=0 → deep-link smoke only"
|
||||
exec env AUTO_ANDROID=1 "$ROOT/run-android-pay-smoke.sh"
|
||||
fi
|
||||
|
||||
export OUT_DIR="${OUT_DIR:-$ROOT/out-gui}"
|
||||
export GUI_ROUNDS="${GUI_ROUNDS:-10}"
|
||||
export GUI_SLEEP="${GUI_SLEEP:-3}"
|
||||
|
||||
# Install + deliver intents via existing smoke, but skip its weak single-tap
|
||||
# by setting GUI_MODE=1 which we implement here more fully.
|
||||
# Simpler: do install/URI here by sourcing patterns from pay-smoke via env + python.
|
||||
|
||||
# Delegate install + URI start to pay-smoke with GUI_AFTER=1 if we patch it;
|
||||
# instead run a self-contained flow calling pay-smoke pieces.
|
||||
|
||||
APK_DIR="${APK_DIR:-$ROOT/apks}"
|
||||
APK_URL="${APK_URL:-https://f-droid.org/repo/net.taler.wallet.fdroid_854.apk}"
|
||||
APK_NAME="${APK_NAME:-net.taler.wallet.fdroid_854.apk}"
|
||||
APK_PATH="${APK_PATH:-}"
|
||||
PKG="${PKG:-net.taler.wallet.fdroid}"
|
||||
STACK="${STACK:-auto}"
|
||||
SERIAL="${SERIAL:-}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
command -v adb >/dev/null || { echo "adb missing" >&2; exit 2; }
|
||||
|
||||
# Headless default: no host window; guest GLES via SwiftShader (uiautomator).
|
||||
android_ensure_device || { echo "no adb device (try ./start-android-emulator.sh --wait)" >&2; exit 3; }
|
||||
ADB=(adb -s "$SERIAL")
|
||||
echo "device: $SERIAL mode: GUI (uiautomator) headless=${EMULATOR_HEADLESS} gpu=${EMULATOR_GPU}"
|
||||
|
||||
case "$STACK" in
|
||||
auto)
|
||||
if curl -sfS -m 5 -o /dev/null https://bank.hacktivism.ch/config; then STACK=goa; else STACK=stage; fi
|
||||
;;
|
||||
esac
|
||||
case "$STACK" in
|
||||
goa)
|
||||
BANK=https://bank.hacktivism.ch
|
||||
MERCHANT=https://taler.hacktivism.ch
|
||||
WITHDRAW_JSON="$BANK/intro/demo-withdraw.json"
|
||||
;;
|
||||
stage)
|
||||
BANK=https://stage.bank.lefrancpaysan.ch
|
||||
MERCHANT=https://stage.monnaie.lefrancpaysan.ch
|
||||
WITHDRAW_JSON="$BANK/intro/demo-withdraw.json"
|
||||
;;
|
||||
*) echo "unknown STACK" >&2; exit 2 ;;
|
||||
esac
|
||||
echo "stack: $STACK"
|
||||
|
||||
if [ -n "$APK_PATH" ] && [ -f "$APK_PATH" ]; then
|
||||
:
|
||||
elif [ -f "$APK_DIR/$APK_NAME" ]; then
|
||||
APK_PATH="$APK_DIR/$APK_NAME"
|
||||
else
|
||||
APK_PATH="$APK_DIR/$APK_NAME"
|
||||
mkdir -p "$APK_DIR"
|
||||
curl -fL --retry 3 -o "$APK_PATH" "$APK_URL"
|
||||
fi
|
||||
echo "apk: $APK_PATH pkg: $PKG"
|
||||
|
||||
"${ADB[@]}" wait-for-device
|
||||
"${ADB[@]}" install -r "$APK_PATH"
|
||||
"${ADB[@]}" shell settings put global window_animation_scale 0 || true
|
||||
"${ADB[@]}" shell settings put global transition_animation_scale 0 || true
|
||||
"${ADB[@]}" shell settings put global animator_duration_scale 0 || true
|
||||
|
||||
: "${DO_WITHDRAW:=1}"
|
||||
: "${DO_PAY:=1}"
|
||||
: "${SMOKE_STRICT:=0}"
|
||||
|
||||
WURI=""
|
||||
PAY_URI=""
|
||||
WITHDRAW_OK=0
|
||||
PAY_OK=0
|
||||
|
||||
"${ADB[@]}" shell am force-stop "$PKG" 2>/dev/null || true
|
||||
"${ADB[@]}" logcat -c 2>/dev/null || true
|
||||
|
||||
if [ "$DO_WITHDRAW" = "1" ]; then
|
||||
echo
|
||||
echo "======== 1 WITHDRAW (Abheben) ========"
|
||||
WURI=$(curl -sfS -m 25 "$WITHDRAW_JSON" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("taler_withdraw_uri") or d.get("qr_payload") or "")')
|
||||
[ -n "$WURI" ] || { echo "no withdraw uri" >&2; exit 4; }
|
||||
echo "withdraw: $WURI"
|
||||
echo "$WURI" >"$OUT_DIR/last-withdraw.uri"
|
||||
"${ADB[@]}" shell am start -a android.intent.action.VIEW -d "$WURI" | tee "$OUT_DIR/start-withdraw.txt"
|
||||
sleep 5
|
||||
python3 - "$ROOT/lib_ui.py" "$SERIAL" "$OUT_DIR" "$GUI_ROUNDS" "$GUI_SLEEP" <<'PY'
|
||||
import json, sys, importlib.util
|
||||
from pathlib import Path
|
||||
lib_path, serial, out, rounds, sleep_s = (
|
||||
Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("lib_ui", lib_path)
|
||||
lib = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(lib)
|
||||
st = lib.gui_drive(serial, out / "gui-withdraw", rounds=rounds, sleep_s=sleep_s, phase="wd")
|
||||
(out / "gui-withdraw-status.json").write_text(json.dumps(st, indent=2))
|
||||
print("gui-withdraw taps:", st.get("taps"))
|
||||
print("gui-withdraw anr:", st.get("anr_dismissals"))
|
||||
PY
|
||||
else
|
||||
echo "======== 1 WITHDRAW skipped (DO_WITHDRAW=0) ========"
|
||||
fi
|
||||
|
||||
if [ "$DO_PAY" = "1" ]; then
|
||||
echo
|
||||
echo "======== 2 PAY (Bezahlen) ========"
|
||||
if [ "$STACK" = "stage" ]; then
|
||||
if RESP=$(curl -sfS -m 20 -X POST -H 'Content-Type: application/json' -d '{}' \
|
||||
"$MERCHANT/instances/fermes-des-collines/templates/panier-legumes" 2>/dev/null); then
|
||||
PAY_URI=$(printf '%s' "$RESP" | python3 -c 'import json,sys;d=json.load(sys.stdin);oid=d.get("order_id")or"";tok=d.get("token")or"";
|
||||
print("taler://pay/stage.monnaie.lefrancpaysan.ch/instances/fermes-des-collines/%s/?c=%s"%(oid,tok) if oid and tok else "")')
|
||||
fi
|
||||
else
|
||||
PAY_URI="taler://pay-template/taler.hacktivism.ch/instances/goa-shop/paivana"
|
||||
fi
|
||||
if [ -n "$PAY_URI" ]; then
|
||||
echo "pay: $PAY_URI"
|
||||
echo "$PAY_URI" >"$OUT_DIR/last-pay.uri"
|
||||
"${ADB[@]}" shell am start -a android.intent.action.VIEW -d "$PAY_URI" | tee "$OUT_DIR/start-pay.txt"
|
||||
sleep 5
|
||||
python3 - "$ROOT/lib_ui.py" "$SERIAL" "$OUT_DIR" "$GUI_ROUNDS" "$GUI_SLEEP" <<'PY'
|
||||
import json, sys, importlib.util
|
||||
from pathlib import Path
|
||||
lib_path, serial, out, rounds, sleep_s = (
|
||||
Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("lib_ui", lib_path)
|
||||
lib = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(lib)
|
||||
st = lib.gui_drive(serial, out / "gui-pay", rounds=rounds, sleep_s=sleep_s, phase="pay")
|
||||
(out / "gui-pay-status.json").write_text(json.dumps(st, indent=2))
|
||||
print("gui-pay taps:", st.get("taps"))
|
||||
print("gui-pay anr:", st.get("anr_dismissals"))
|
||||
PY
|
||||
else
|
||||
echo "pay: (no URI)"
|
||||
fi
|
||||
else
|
||||
echo "======== 2 PAY skipped (DO_PAY=0) ========"
|
||||
fi
|
||||
|
||||
"${ADB[@]}" logcat -d -t 500 >"$OUT_DIR/logcat.txt" || true
|
||||
grep -iE 'taler-wallet|prepareBank|acceptWithdrawal|confirmWithdrawal|preparePay|confirmPay|Error|success|withdraw|pay' \
|
||||
"$OUT_DIR/logcat.txt" | tail -100 >"$OUT_DIR/logcat-taler.txt" || true
|
||||
|
||||
if [ "$DO_WITHDRAW" = "1" ] && grep -qE 'prepareBankIntegratedWithdrawal|acceptWithdrawal|confirmWithdrawal' \
|
||||
"$OUT_DIR/logcat.txt" 2>/dev/null; then
|
||||
WITHDRAW_OK=1
|
||||
fi
|
||||
if [ "$DO_PAY" = "1" ] && [ -n "$PAY_URI" ] && \
|
||||
grep -qiE 'preparePay|confirmPay|preparePurchase|checkPay|pay-template|PayForTemplate' \
|
||||
"$OUT_DIR/logcat.txt" 2>/dev/null; then
|
||||
PAY_OK=1
|
||||
fi
|
||||
|
||||
rc=0
|
||||
echo
|
||||
echo "=== GUI smoke summary ($STACK) — withdraw + pay ==="
|
||||
echo "package: $PKG DO_WITHDRAW=$DO_WITHDRAW DO_PAY=$DO_PAY STRICT=$SMOKE_STRICT"
|
||||
if [ "$DO_WITHDRAW" = "1" ]; then
|
||||
echo "withdraw URI: delivered → $OUT_DIR/gui-withdraw-status.json"
|
||||
echo "withdraw wallet-core: $([ "$WITHDRAW_OK" = 1 ] && echo YES || echo NO)"
|
||||
if [ "$SMOKE_STRICT" = "1" ] && [ "$WITHDRAW_OK" != "1" ]; then
|
||||
echo "FAIL: strict withdraw missing"; rc=11
|
||||
fi
|
||||
fi
|
||||
if [ "$DO_PAY" = "1" ]; then
|
||||
echo "pay URI: ${PAY_URI:-none} → $OUT_DIR/gui-pay-status.json"
|
||||
echo "pay wallet-core: $([ "$PAY_OK" = 1 ] && echo YES || echo NO)"
|
||||
if [ "$SMOKE_STRICT" = "1" ] && [ "$PAY_OK" != "1" ]; then
|
||||
echo "FAIL: strict pay missing"; [ "$rc" -eq 0 ] && rc=13
|
||||
fi
|
||||
fi
|
||||
if grep -qiE "isn't responding|not responding" "$OUT_DIR/logcat.txt" 2>/dev/null \
|
||||
|| grep -rqiE "isn't responding" "$OUT_DIR/gui-withdraw" "$OUT_DIR/gui-pay" 2>/dev/null; then
|
||||
echo "FAIL: ANR"; rc=10
|
||||
fi
|
||||
echo "artifacts: $OUT_DIR/"
|
||||
ls -la "$OUT_DIR" | sed 's/^/ /'
|
||||
echo "Documented shortcuts → GUI-AUTOMATION-NOTES.md"
|
||||
exit "$rc"
|
||||
308
android-test/run-android-pay-smoke.sh
Executable file
308
android-test/run-android-pay-smoke.sh
Executable file
|
|
@ -0,0 +1,308 @@
|
|||
#!/usr/bin/env bash
|
||||
# Android wallet smoke: install APK, then **withdraw (Abheben)** and **pay** via
|
||||
# deep-link + best-effort UI taps. Both legs are first-class (not pay-only).
|
||||
#
|
||||
# Flow (default both on):
|
||||
# 1) WITHDRAW taler://withdraw/… from bank demo-withdraw.json + confirm taps
|
||||
# 2) PAY taler://pay… / pay-template + optional taps
|
||||
#
|
||||
# Env:
|
||||
# DO_WITHDRAW=1 (default) DO_PAY=1 (default)
|
||||
# SMOKE_STRICT=1 → require wallet-core withdraw (+ pay if DO_PAY) + no ANR
|
||||
# WITHDRAW_GUI_ROUNDS multi-round Abheben taps (default 6)
|
||||
#
|
||||
# Usage:
|
||||
# ./android-test/run-android-pay-smoke.sh
|
||||
# STACK=goa ./android-test/run-android-pay-smoke.sh
|
||||
# DO_PAY=0 ./android-test/run-android-pay-smoke.sh # withdraw-only
|
||||
#
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib_android_env.sh
|
||||
. "$ROOT/lib_android_env.sh"
|
||||
|
||||
if [ "${AUTO_ANDROID}" != "1" ]; then
|
||||
echo "skipped: AUTO_ANDROID=${AUTO_ANDROID} (platform flags — see GUI-AUTOMATION-NOTES.md)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
APK_DIR="${APK_DIR:-$ROOT/apks}"
|
||||
APK_URL="${APK_URL:-https://f-droid.org/repo/net.taler.wallet.fdroid_854.apk}"
|
||||
APK_NAME="${APK_NAME:-net.taler.wallet.fdroid_854.apk}"
|
||||
# Optional override: full path to an already-built APK (from-source)
|
||||
APK_PATH="${APK_PATH:-}"
|
||||
PKG="${PKG:-net.taler.wallet.fdroid}"
|
||||
STACK="${STACK:-auto}" # auto | goa | stage
|
||||
SERIAL="${SERIAL:-}"
|
||||
OUT_DIR="${OUT_DIR:-$ROOT/out}"
|
||||
mkdir -p "$APK_DIR" "$OUT_DIR"
|
||||
|
||||
if ! command -v adb >/dev/null 2>&1; then
|
||||
echo "adb missing — install android-tools-adb or user SDK platform-tools" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Default: headless AVD (no host window). WINDOWED=1 / EMULATOR_HEADLESS=0 for UI.
|
||||
if ! android_ensure_device; then
|
||||
cat >&2 <<EOF
|
||||
No adb device. Options:
|
||||
1) Plug phone with USB debugging
|
||||
2) Headless AVD (default): $ROOT/start-android-emulator.sh --wait
|
||||
3) Windowed AVD: WINDOWED=1 $ROOT/start-android-emulator.sh --wait
|
||||
4) AUTO_START_EMULATOR=0 disables auto-start from this smoke
|
||||
EOF
|
||||
exit 3
|
||||
fi
|
||||
ADB=(adb -s "$SERIAL")
|
||||
echo "device: $SERIAL emulator_headless=${EMULATOR_HEADLESS} gpu=${EMULATOR_GPU}"
|
||||
|
||||
# Resolve stack endpoints
|
||||
case "$STACK" in
|
||||
auto)
|
||||
if curl -sfS -m 5 -o /dev/null https://bank.hacktivism.ch/config 2>/dev/null; then
|
||||
STACK=goa
|
||||
else
|
||||
STACK=stage
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
case "$STACK" in
|
||||
goa)
|
||||
BANK=https://bank.hacktivism.ch
|
||||
MERCHANT=https://taler.hacktivism.ch
|
||||
WITHDRAW_JSON="$BANK/intro/demo-withdraw.json"
|
||||
PAY_HINT="paivana template / goa-shop"
|
||||
;;
|
||||
stage)
|
||||
BANK=https://stage.bank.lefrancpaysan.ch
|
||||
MERCHANT=https://stage.monnaie.lefrancpaysan.ch
|
||||
WITHDRAW_JSON="$BANK/intro/demo-withdraw.json"
|
||||
PAY_HINT="farmer shop templates"
|
||||
;;
|
||||
*) echo "unknown STACK=$STACK" >&2; exit 2 ;;
|
||||
esac
|
||||
echo "stack: $STACK bank: $BANK"
|
||||
|
||||
# APK: explicit path, or download published F-Droid into APK_DIR
|
||||
if [ -n "$APK_PATH" ] && [ -f "$APK_PATH" ]; then
|
||||
:
|
||||
elif [ -f "$APK_DIR/$APK_NAME" ]; then
|
||||
APK_PATH="$APK_DIR/$APK_NAME"
|
||||
else
|
||||
APK_PATH="$APK_DIR/$APK_NAME"
|
||||
echo "downloading published wallet APK…"
|
||||
curl -fL --retry 3 -o "$APK_PATH" "$APK_URL"
|
||||
fi
|
||||
echo "apk: $APK_PATH ($(wc -c <"$APK_PATH") bytes)"
|
||||
echo "pkg: $PKG"
|
||||
|
||||
: "${DO_WITHDRAW:=1}"
|
||||
: "${DO_PAY:=1}"
|
||||
: "${WITHDRAW_GUI_ROUNDS:=6}"
|
||||
: "${PAY_GUI_ROUNDS:=4}"
|
||||
: "${SMOKE_STRICT:=0}"
|
||||
|
||||
"${ADB[@]}" wait-for-device
|
||||
"${ADB[@]}" install -r "$APK_PATH"
|
||||
"${ADB[@]}" shell settings put global window_animation_scale 0 || true
|
||||
"${ADB[@]}" shell settings put global transition_animation_scale 0 || true
|
||||
"${ADB[@]}" shell settings put global animator_duration_scale 0 || true
|
||||
|
||||
"${ADB[@]}" shell am force-stop "$PKG" 2>/dev/null || true
|
||||
"${ADB[@]}" logcat -c 2>/dev/null || true
|
||||
|
||||
WURI=""
|
||||
PAY_URI=""
|
||||
WITHDRAW_OK=0
|
||||
PAY_OK=0
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 1) WITHDRAW / Abheben (required by default — not pay-only)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
if [ "$DO_WITHDRAW" = "1" ]; then
|
||||
echo
|
||||
echo "======== 1 WITHDRAW (Abheben) ========"
|
||||
WURI=$(curl -sfS -m 25 "$WITHDRAW_JSON" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("taler_withdraw_uri") or d.get("qr_payload") or "")')
|
||||
if [ -z "$WURI" ]; then
|
||||
echo "FAIL: no taler_withdraw_uri from $WITHDRAW_JSON" >&2
|
||||
exit 4
|
||||
fi
|
||||
echo "withdraw URI: $WURI"
|
||||
echo "$WURI" >"$OUT_DIR/last-withdraw.uri"
|
||||
|
||||
"${ADB[@]}" shell am start -a android.intent.action.VIEW -d "$WURI" 2>&1 | tee "$OUT_DIR/start-withdraw.txt"
|
||||
sleep 6
|
||||
"${ADB[@]}" exec-out screencap -p >"$OUT_DIR/01-after-withdraw-uri.png" || true
|
||||
"${ADB[@]}" shell uiautomator dump /sdcard/ui.xml 2>/dev/null || true
|
||||
"${ADB[@]}" pull /sdcard/ui.xml "$OUT_DIR/01-ui.xml" 2>/dev/null || true
|
||||
|
||||
# Multi-round confirm / Abheben / ToS (same idea as GUI smoke)
|
||||
if [ -f "$ROOT/lib_ui.py" ]; then
|
||||
python3 - "$ROOT/lib_ui.py" "$SERIAL" "$OUT_DIR" "$WITHDRAW_GUI_ROUNDS" 2 <<'PY' || true
|
||||
import json, sys, importlib.util
|
||||
from pathlib import Path
|
||||
lib_path, serial, out, rounds, sleep_s = (
|
||||
Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("lib_ui", lib_path)
|
||||
lib = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(lib)
|
||||
st = lib.gui_drive(serial, out / "gui-withdraw", rounds=rounds, sleep_s=sleep_s, phase="wd")
|
||||
(out / "gui-withdraw-status.json").write_text(json.dumps(st, indent=2))
|
||||
print("withdraw GUI taps:", st.get("taps"), "anr:", st.get("anr_dismissals"))
|
||||
PY
|
||||
else
|
||||
python3 - "$SERIAL" "$OUT_DIR/01-ui.xml" <<'PY' || true
|
||||
import re, subprocess, sys
|
||||
serial, path = sys.argv[1], sys.argv[2]
|
||||
try:
|
||||
xml = open(path, errors="replace").read()
|
||||
except FileNotFoundError:
|
||||
raise SystemExit(0)
|
||||
labels = [
|
||||
"Confirm", "Confirm withdrawal", "Withdraw", "Accept", "Continue", "OK",
|
||||
"I accept", "Agree", "Bestätigen", "Abheben", "Akzeptieren", "Retirer",
|
||||
]
|
||||
for label in labels:
|
||||
for pat in (
|
||||
r'text="%s"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"' % re.escape(label),
|
||||
r'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"[^>]*text="%s"' % re.escape(label),
|
||||
):
|
||||
for m in re.finditer(pat, xml, flags=re.I):
|
||||
x1, y1, x2, y2 = map(int, m.groups())
|
||||
x, y = (x1 + x2) // 2, (y1 + y2) // 2
|
||||
print(f"tap {label} @ {x},{y}")
|
||||
subprocess.run(["adb", "-s", serial, "shell", "input", "tap", str(x), str(y)], check=False)
|
||||
PY
|
||||
sleep 8
|
||||
fi
|
||||
"${ADB[@]}" exec-out screencap -p >"$OUT_DIR/02-after-withdraw-taps.png" || true
|
||||
# Mid-flow logcat: withdraw must register before pay
|
||||
"${ADB[@]}" logcat -d -t 400 >"$OUT_DIR/logcat-after-withdraw.txt" || true
|
||||
if grep -qE 'prepareBankIntegratedWithdrawal|acceptWithdrawal|confirmWithdrawal' \
|
||||
"$OUT_DIR/logcat-after-withdraw.txt" 2>/dev/null; then
|
||||
WITHDRAW_OK=1
|
||||
echo "withdraw: wallet-core activity YES"
|
||||
else
|
||||
echo "withdraw: wallet-core activity NOT seen yet (onboarding/ANR/slow?)"
|
||||
fi
|
||||
else
|
||||
echo "======== 1 WITHDRAW skipped (DO_WITHDRAW=0) ========"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 2) PAY (after withdraw; still first-class, but second leg)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
if [ "$DO_PAY" = "1" ]; then
|
||||
echo
|
||||
echo "======== 2 PAY (Bezahlen) ========"
|
||||
if [ "$STACK" = "stage" ]; then
|
||||
if RESP=$(curl -sfS -m 20 -X POST -H 'Content-Type: application/json' -d '{}' \
|
||||
"$MERCHANT/instances/fermes-des-collines/templates/panier-legumes" 2>/dev/null); then
|
||||
PAY_URI=$(printf '%s' "$RESP" | python3 -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
oid=d.get("order_id") or ""
|
||||
tok=d.get("token") or ""
|
||||
if oid and tok:
|
||||
print("taler://pay/stage.monnaie.lefrancpaysan.ch/instances/fermes-des-collines/%s/?c=%s"%(oid,tok))
|
||||
' 2>/dev/null || true)
|
||||
fi
|
||||
elif [ "$STACK" = "goa" ]; then
|
||||
PAY_URI="taler://pay-template/taler.hacktivism.ch/instances/goa-shop/paivana"
|
||||
fi
|
||||
|
||||
if [ -n "$PAY_URI" ]; then
|
||||
echo "pay URI: $PAY_URI ($PAY_HINT)"
|
||||
echo "$PAY_URI" >"$OUT_DIR/last-pay.uri"
|
||||
"${ADB[@]}" shell am start -a android.intent.action.VIEW -d "$PAY_URI" 2>&1 | tee "$OUT_DIR/start-pay.txt"
|
||||
sleep 6
|
||||
if [ -f "$ROOT/lib_ui.py" ]; then
|
||||
python3 - "$ROOT/lib_ui.py" "$SERIAL" "$OUT_DIR" "$PAY_GUI_ROUNDS" 2 <<'PY' || true
|
||||
import json, sys, importlib.util
|
||||
from pathlib import Path
|
||||
lib_path, serial, out, rounds, sleep_s = (
|
||||
Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("lib_ui", lib_path)
|
||||
lib = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(lib)
|
||||
st = lib.gui_drive(serial, out / "gui-pay", rounds=rounds, sleep_s=sleep_s, phase="pay")
|
||||
(out / "gui-pay-status.json").write_text(json.dumps(st, indent=2))
|
||||
print("pay GUI taps:", st.get("taps"), "anr:", st.get("anr_dismissals"))
|
||||
PY
|
||||
fi
|
||||
"${ADB[@]}" exec-out screencap -p >"$OUT_DIR/03-after-pay-uri.png" || true
|
||||
else
|
||||
echo "pay: (no public template URI for stack=$STACK)"
|
||||
fi
|
||||
else
|
||||
echo "======== 2 PAY skipped (DO_PAY=0) ========"
|
||||
fi
|
||||
|
||||
# Final evidence
|
||||
"${ADB[@]}" logcat -d -t 500 >"$OUT_DIR/logcat.txt" || true
|
||||
grep -iE 'taler-wallet|prepareBank|acceptWithdrawal|confirmWithdrawal|preparePay|confirmPay|error|Error|success|withdraw|pay' \
|
||||
"$OUT_DIR/logcat.txt" | tail -100 >"$OUT_DIR/logcat-taler.txt" || true
|
||||
|
||||
# Re-evaluate withdraw/pay from full logcat
|
||||
if [ "$DO_WITHDRAW" = "1" ]; then
|
||||
if grep -qE 'prepareBankIntegratedWithdrawal|acceptWithdrawal|confirmWithdrawal' \
|
||||
"$OUT_DIR/logcat.txt" 2>/dev/null; then
|
||||
WITHDRAW_OK=1
|
||||
fi
|
||||
fi
|
||||
if [ "$DO_PAY" = "1" ] && [ -n "$PAY_URI" ]; then
|
||||
if grep -qiE 'preparePay|confirmPay|preparePurchase|checkPay|pay-template|PayForTemplate' \
|
||||
"$OUT_DIR/logcat.txt" 2>/dev/null; then
|
||||
PAY_OK=1
|
||||
fi
|
||||
fi
|
||||
|
||||
rc=0
|
||||
echo
|
||||
echo "=== summary (withdraw + pay) ==="
|
||||
echo "package: $PKG stack: $STACK"
|
||||
echo "DO_WITHDRAW=$DO_WITHDRAW DO_PAY=$DO_PAY SMOKE_STRICT=$SMOKE_STRICT"
|
||||
if [ "$DO_WITHDRAW" = "1" ]; then
|
||||
echo "withdraw URI: ${WURI:-(none)}"
|
||||
if [ "$WITHDRAW_OK" = "1" ]; then
|
||||
echo "withdraw wallet-core: YES"
|
||||
else
|
||||
echo "withdraw wallet-core: NO"
|
||||
if [ "$SMOKE_STRICT" = "1" ]; then
|
||||
echo "FAIL: SMOKE_STRICT requires withdraw (prepareBank/accept/confirm) in logcat"
|
||||
rc=11
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "$DO_PAY" = "1" ]; then
|
||||
echo "pay URI: ${PAY_URI:-(none)}"
|
||||
if [ -z "$PAY_URI" ]; then
|
||||
echo "pay: no URI (template/API)"
|
||||
if [ "$SMOKE_STRICT" = "1" ]; then
|
||||
echo "FAIL: SMOKE_STRICT requires a pay URI when DO_PAY=1"
|
||||
rc=12
|
||||
fi
|
||||
elif [ "$PAY_OK" = "1" ]; then
|
||||
echo "pay wallet-core: YES"
|
||||
else
|
||||
echo "pay wallet-core: NO (intent may still have been delivered)"
|
||||
if [ "$SMOKE_STRICT" = "1" ]; then
|
||||
echo "FAIL: SMOKE_STRICT requires pay path activity in logcat"
|
||||
[ "$rc" -eq 0 ] && rc=13
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if grep -qiE "isn't responding|isn.t responding|System UI isn" "$OUT_DIR/01-ui.xml" 2>/dev/null \
|
||||
|| grep -qiE "isn't responding|not responding" "$OUT_DIR/logcat.txt" 2>/dev/null \
|
||||
|| grep -rqiE "isn't responding" "$OUT_DIR/gui-withdraw" 2>/dev/null; then
|
||||
echo "FAIL: System UI / app ANR"
|
||||
rc=10
|
||||
fi
|
||||
echo "artifacts: $OUT_DIR/"
|
||||
ls -la "$OUT_DIR" | sed 's/^/ /'
|
||||
echo
|
||||
echo "NOTE: Full unattended Abheben+Bezahlen needs responsive device (≥6–8 GiB host RAM)."
|
||||
echo "Smoke legs: withdraw=$( [ "$DO_WITHDRAW" = 1 ] && echo on || echo off) pay=$( [ "$DO_PAY" = 1 ] && echo on || echo off) rc=$rc"
|
||||
exit "$rc"
|
||||
322
android-test/run-android-variant-matrix.sh
Executable file
322
android-test/run-android-variant-matrix.sh
Executable file
|
|
@ -0,0 +1,322 @@
|
|||
#!/usr/bin/env bash
|
||||
# Run Android wallet smoke against several build / published variants.
|
||||
#
|
||||
# Default order:
|
||||
# 1) stable-self — **only** self-build of current published stable (WARN)
|
||||
# 2) published — current F-Droid APK
|
||||
# 3) older — F-Droid APKs ~3/6/9/12 mo if still hosted (WARN, no self-build)
|
||||
# 4) master — tip self-build (WARN)
|
||||
#
|
||||
# Self-build policy: current stable + master/fix only — never rebuild old releases.
|
||||
#
|
||||
# Severity: age ≤14d → BLOCKER; milestones/older/self-builds → WARN
|
||||
#
|
||||
# Env:
|
||||
# STABLE_TAG, STABLE_APK_URL, STABLE_APK_NAME
|
||||
# MASTER_BRANCH, FIX_BRANCH
|
||||
# VARIANTS default: stable-self,published,older,master
|
||||
# INCLUDE_OLDER=1 expand token "older" / "milestones" (default 1)
|
||||
# MILESTONE_DAYS default "90 180 270 365" (≈ 3,6,9,12 months)
|
||||
# MILESTONE_TOLERANCE_DAYS default 50 — max distance to accept a tag
|
||||
# BLOCKER_MAX_AGE_DAYS default 14
|
||||
# PUBLISHED_FORCE_BLOCKER=1 always treat current published as blocker
|
||||
# FAIL_FAST=1, GUI=0, STACK=…
|
||||
#
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib_android_env.sh
|
||||
. "$ROOT/lib_android_env.sh"
|
||||
# shellcheck source=lib_release_age.sh
|
||||
. "$ROOT/lib_release_age.sh"
|
||||
|
||||
: "${STABLE_TAG:=wallet-1.6.1}"
|
||||
: "${STABLE_APK_URL:=https://f-droid.org/repo/net.taler.wallet.fdroid_854.apk}"
|
||||
: "${STABLE_APK_NAME:=net.taler.wallet.fdroid_854.apk}"
|
||||
: "${MASTER_BRANCH:=master}"
|
||||
: "${FIX_BRANCH:=dev/hernani-inference/fix-bank-withdraw-auto-exchange}"
|
||||
: "${VARIANTS:=stable-self,published,older,master}"
|
||||
: "${INCLUDE_OLDER:=1}"
|
||||
: "${MILESTONE_DAYS:=90 180 270 365}"
|
||||
: "${MILESTONE_TOLERANCE_DAYS:=50}"
|
||||
: "${BLOCKER_MAX_AGE_DAYS:=14}"
|
||||
: "${PUBLISHED_FORCE_BLOCKER:=0}"
|
||||
: "${FAIL_FAST:=1}"
|
||||
: "${GUI:=0}"
|
||||
: "${STACK:=auto}"
|
||||
: "${TALER_ANDROID_SRC:=$HOME/taler/taler-android}"
|
||||
export TALER_ANDROID_SRC BLOCKER_MAX_AGE_DAYS MILESTONE_DAYS MILESTONE_TOLERANCE_DAYS
|
||||
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
MATRIX_OUT="${MATRIX_OUT:-$ROOT/out-matrix-$TS}"
|
||||
mkdir -p "$MATRIX_OUT"
|
||||
SUMMARY="$MATRIX_OUT/SUMMARY.txt"
|
||||
: >"$SUMMARY"
|
||||
|
||||
echo "=== Android variant matrix ===" | tee -a "$SUMMARY"
|
||||
echo "STACK=$STACK GUI=$GUI headless=${EMULATOR_HEADLESS} variants=$VARIANTS" | tee -a "$SUMMARY"
|
||||
echo "STABLE_TAG=$STABLE_TAG BLOCKER_MAX_AGE_DAYS=$BLOCKER_MAX_AGE_DAYS" | tee -a "$SUMMARY"
|
||||
echo "milestones(days)=$MILESTONE_DAYS tolerance=$MILESTONE_TOLERANCE_DAYS (always WARN)" | tee -a "$SUMMARY"
|
||||
echo "out: $MATRIX_OUT" | tee -a "$SUMMARY"
|
||||
|
||||
android_ensure_device || {
|
||||
echo "no adb device — start ./start-android-emulator.sh --wait" >&2
|
||||
exit 3
|
||||
}
|
||||
export SERIAL ANDROID_SERIAL
|
||||
echo "device: $SERIAL" | tee -a "$SUMMARY"
|
||||
|
||||
# --- expand VARIANTS (token "older" → concrete older-pub / older-self entries) ---
|
||||
# Append one older release as WARN-only — **F-Droid APK only** (no self-build).
|
||||
# Returns 0 if queued, 1 if skipped (no published APK).
|
||||
append_older_warn() {
|
||||
local t="$1" age="$2" target="${3:-}"
|
||||
local vn meta apk_vc apk_url
|
||||
vn="${t#wallet-}"
|
||||
if [ -n "$target" ]; then
|
||||
echo " milestone ~${target}d → $t (age=${age}d) severity=WARN" | tee -a "$SUMMARY"
|
||||
else
|
||||
echo " older $t (age=${age}d) severity=WARN" | tee -a "$SUMMARY"
|
||||
fi
|
||||
meta=$(fdroid_apk_for_version_name "$vn" 2>/dev/null || true)
|
||||
if [ -n "$meta" ]; then
|
||||
apk_vc="${meta%%|*}"
|
||||
apk_url="${meta#*|}"
|
||||
if fdroid_apk_exists "$apk_url"; then
|
||||
out+=("older-pub:${vn}:${apk_vc}:warn")
|
||||
return 0
|
||||
fi
|
||||
echo " SKIP $t — no F-Droid APK at ${apk_url:-?} (self-build only for current stable)" | tee -a "$SUMMARY"
|
||||
else
|
||||
echo " SKIP $t — cannot map F-Droid versionCode (self-build only for current stable)" | tee -a "$SUMMARY"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
expand_variants() {
|
||||
local raw="$1"
|
||||
local -a out=()
|
||||
local v tag vn age sev line t target
|
||||
|
||||
IFS=',' read -r -a toks <<<"$raw"
|
||||
for v in "${toks[@]}"; do
|
||||
v=$(echo "$v" | tr -d '[:space:]')
|
||||
[ -n "$v" ] || continue
|
||||
if [ "$v" = "older" ] || [ "$v" = "older-releases" ] || [ "$v" = "milestones" ]; then
|
||||
[ "$INCLUDE_OLDER" = "1" ] || continue
|
||||
# 3/6/9/12-month samples: published APK only, always WARN.
|
||||
local found=0 queued=0
|
||||
while read -r t age target; do
|
||||
[ -n "$t" ] || continue
|
||||
[ "$t" = "$STABLE_TAG" ] && continue
|
||||
found=1
|
||||
if append_older_warn "$t" "$age" "$target"; then
|
||||
queued=$((queued + 1))
|
||||
fi
|
||||
done < <(select_milestone_tags "$STABLE_TAG" 2>/dev/null || true)
|
||||
if [ "$found" = "0" ]; then
|
||||
echo " (no milestone tags within ±${MILESTONE_TOLERANCE_DAYS}d of $MILESTONE_DAYS)" | tee -a "$SUMMARY"
|
||||
elif [ "$queued" = "0" ]; then
|
||||
echo " (milestone tags found but none still on F-Droid — no older APK smokes)" | tee -a "$SUMMARY"
|
||||
fi
|
||||
else
|
||||
out+=("$v")
|
||||
fi
|
||||
done
|
||||
printf '%s\n' "${out[@]}"
|
||||
}
|
||||
|
||||
mapfile -t VLIST < <(expand_variants "$VARIANTS")
|
||||
echo "expanded variants: ${VLIST[*]}" | tee -a "$SUMMARY"
|
||||
|
||||
declare -a RESULTS=()
|
||||
declare -a WARNINGS=()
|
||||
overall=0
|
||||
|
||||
severity_for_published_current() {
|
||||
if [ "${PUBLISHED_FORCE_BLOCKER}" = "1" ]; then
|
||||
echo blocker
|
||||
return
|
||||
fi
|
||||
local days
|
||||
days=$(release_age_days "$STABLE_TAG" 2>/dev/null || true)
|
||||
severity_from_age_days "$days"
|
||||
}
|
||||
|
||||
run_published_apk() {
|
||||
local label="$1"
|
||||
local apk_url="$2"
|
||||
local apk_name="$3"
|
||||
local sev="$4"
|
||||
local out="$MATRIX_OUT/$label"
|
||||
mkdir -p "$out"
|
||||
echo "" | tee -a "$SUMMARY"
|
||||
echo "-------- variant: $label (F-Droid APK) [$(echo "$sev" | tr 'a-z' 'A-Z')] --------" | tee -a "$SUMMARY"
|
||||
echo " url=$apk_url" | tee -a "$SUMMARY"
|
||||
local smoke="$ROOT/run-android-pay-smoke.sh"
|
||||
[ "$GUI" = "1" ] && smoke="$ROOT/run-android-gui-smoke.sh"
|
||||
local strict=1
|
||||
[ "$sev" = "warn" ] && strict="${SMOKE_STRICT_WARN:-0}"
|
||||
[ "$sev" = "blocker" ] && strict="${SMOKE_STRICT:-1}"
|
||||
set +e
|
||||
env STACK="$STACK" \
|
||||
APK_URL="$apk_url" \
|
||||
APK_NAME="$apk_name" \
|
||||
APK_PATH="" \
|
||||
PKG=net.taler.wallet.fdroid \
|
||||
OUT_DIR="$out" \
|
||||
SERIAL="$SERIAL" \
|
||||
AUTO_START_EMULATOR=0 \
|
||||
SMOKE_STRICT="$strict" \
|
||||
"$smoke"
|
||||
local rc=$?
|
||||
set -e
|
||||
echo "$label exit=$rc severity=$sev" | tee -a "$SUMMARY"
|
||||
echo "$label $rc $sev $apk_url" >>"$MATRIX_OUT/results.tsv"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
run_selfbuild() {
|
||||
local label="$1"
|
||||
local ref="$2"
|
||||
local sev="${3:-warn}"
|
||||
local out="$MATRIX_OUT/$label"
|
||||
mkdir -p "$out"
|
||||
echo "" | tee -a "$SUMMARY"
|
||||
echo "-------- variant: $label (self-build ref=$ref) [$(echo "$sev" | tr 'a-z' 'A-Z')] --------" | tee -a "$SUMMARY"
|
||||
local strict="${SMOKE_STRICT:-1}"
|
||||
[ "$sev" = "warn" ] && strict="${SMOKE_STRICT_SELF:-0}"
|
||||
[ "$sev" = "blocker" ] && strict="${SMOKE_STRICT:-1}"
|
||||
set +e
|
||||
env STACK="$STACK" \
|
||||
BRANCH="$ref" \
|
||||
LABEL="$label" \
|
||||
GUI="$GUI" \
|
||||
OUT_DIR="$out" \
|
||||
SERIAL="$SERIAL" \
|
||||
ANDROID_SERIAL="$SERIAL" \
|
||||
AUTO_START_EMULATOR=0 \
|
||||
PKG=net.taler.wallet.fdroid.debug \
|
||||
SMOKE_STRICT="$strict" \
|
||||
TALER_ANDROID_SRC="$TALER_ANDROID_SRC" \
|
||||
"$ROOT/run-android-build-and-smoke.sh"
|
||||
local rc=$?
|
||||
set -e
|
||||
echo "$label (ref=$ref) exit=$rc severity=$sev" | tee -a "$SUMMARY"
|
||||
echo "$label $rc $ref $sev" >>"$MATRIX_OUT/results.tsv"
|
||||
if [ -f "$out/build-meta.txt" ]; then
|
||||
echo "build-meta:" | tee -a "$SUMMARY"
|
||||
cat "$out/build-meta.txt" | tee -a "$SUMMARY"
|
||||
fi
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
record_result() {
|
||||
local v="$1" rc="$2" sev="$3"
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
RESULTS+=("$v:OK($sev)")
|
||||
return
|
||||
fi
|
||||
case "$sev" in
|
||||
warn)
|
||||
echo "WARN: $v failed rc=$rc — not a blocker; continuing" | tee -a "$SUMMARY"
|
||||
WARNINGS+=("$v:$rc")
|
||||
RESULTS+=("$v:WARN($rc)")
|
||||
;;
|
||||
blocker)
|
||||
echo "BLOCKER: $v failed rc=$rc (release within ${BLOCKER_MAX_AGE_DAYS}d window)" | tee -a "$SUMMARY"
|
||||
RESULTS+=("$v:BLOCKER($rc)")
|
||||
overall=1
|
||||
if [ "$FAIL_FAST" = "1" ]; then
|
||||
echo "FAIL_FAST: stopping after blocker $v" | tee -a "$SUMMARY"
|
||||
return 2
|
||||
fi
|
||||
;;
|
||||
soft)
|
||||
echo "FAIL: $v rc=$rc" | tee -a "$SUMMARY"
|
||||
RESULTS+=("$v:FAIL($rc)")
|
||||
overall=1
|
||||
if [ "$FAIL_FAST" = "1" ]; then
|
||||
echo "FAIL_FAST: stopping after $v" | tee -a "$SUMMARY"
|
||||
return 2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
for raw in "${VLIST[@]}"; do
|
||||
v=$(echo "$raw" | tr -d '[:space:]')
|
||||
[ -n "$v" ] || continue
|
||||
rc=0
|
||||
sev=warn
|
||||
stop=0
|
||||
|
||||
case "$v" in
|
||||
published|fdroid|stable-published)
|
||||
sev=$(severity_for_published_current)
|
||||
age=$(release_age_days "$STABLE_TAG" 2>/dev/null || echo "?")
|
||||
echo "published current: tag=$STABLE_TAG age_days=$age → severity=$sev" | tee -a "$SUMMARY"
|
||||
run_published_apk "published" "$STABLE_APK_URL" "$STABLE_APK_NAME" "$sev" || rc=$?
|
||||
;;
|
||||
stable-self|stable|self-stable|release-self)
|
||||
# rebuild of stable: always warn (toolchain), age does not make self-build a blocker
|
||||
sev=warn
|
||||
run_selfbuild "stable-self" "$STABLE_TAG" warn || rc=$?
|
||||
;;
|
||||
master|main)
|
||||
sev=warn
|
||||
run_selfbuild "master" "$MASTER_BRANCH" warn || rc=$?
|
||||
;;
|
||||
fix|fix-branch|inference)
|
||||
sev=soft
|
||||
run_selfbuild "fix" "$FIX_BRANCH" soft || rc=$?
|
||||
;;
|
||||
older-pub:*)
|
||||
# older-pub:VERSION:VC:SEV
|
||||
IFS=':' read -r _ vn apk_vc sev <<<"$v"
|
||||
: "${sev:=warn}"
|
||||
label="older-pub-${vn}"
|
||||
url="https://f-droid.org/repo/net.taler.wallet.fdroid_${apk_vc}.apk"
|
||||
name="net.taler.wallet.fdroid_${apk_vc}.apk"
|
||||
age=$(release_age_days "wallet-${vn}" 2>/dev/null || echo "?")
|
||||
echo "older published $vn age_days=$age severity=$sev" | tee -a "$SUMMARY"
|
||||
run_published_apk "$label" "$url" "$name" "$sev" || rc=$?
|
||||
v="$label"
|
||||
;;
|
||||
older-self:*)
|
||||
# Explicit older-self is disabled by policy (self-build = current stable only).
|
||||
echo "SKIP $v — self-build only for current stable ($STABLE_TAG); use older-pub or published" | tee -a "$SUMMARY"
|
||||
RESULTS+=("$v:SKIP(no-selfbuild-older)")
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
# raw git ref: only master-like / fix via named tokens; bare wallet-* tags → no self-build
|
||||
if [[ "$v" =~ ^wallet-[0-9] ]]; then
|
||||
echo "SKIP self-build $v — only current stable self-build ($STABLE_TAG) is enabled" | tee -a "$SUMMARY"
|
||||
RESULTS+=("$v:SKIP(no-selfbuild-older)")
|
||||
continue
|
||||
fi
|
||||
sev=warn
|
||||
run_selfbuild "ref-${v//\//-}" "$v" warn || rc=$?
|
||||
;;
|
||||
esac
|
||||
|
||||
record_result "$v" "$rc" "$sev" || stop=$?
|
||||
[ "$stop" = "2" ] && break
|
||||
done
|
||||
|
||||
echo "" | tee -a "$SUMMARY"
|
||||
echo "=== matrix done (overall=$overall) ===" | tee -a "$SUMMARY"
|
||||
echo "rule: age ≤ ${BLOCKER_MAX_AGE_DAYS}d → BLOCKER; 3/6/9/12mo milestones + older → WARN only" | tee -a "$SUMMARY"
|
||||
for r in "${RESULTS[@]}"; do
|
||||
echo " $r" | tee -a "$SUMMARY"
|
||||
done
|
||||
if [ "${#WARNINGS[@]}" -gt 0 ]; then
|
||||
echo "warnings (non-blocking):" | tee -a "$SUMMARY"
|
||||
for w in "${WARNINGS[@]}"; do
|
||||
echo " WARN $w" | tee -a "$SUMMARY"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
echo "Artifacts: $MATRIX_OUT"
|
||||
exit "$overall"
|
||||
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)"
|
||||
90
android-test/start-android-emulator.sh
Executable file
90
android-test/start-android-emulator.sh
Executable file
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env bash
|
||||
# Start the Taler Android AVD. **Default = headless** (no host window).
|
||||
# Guest GLES still runs via SwiftShader so uiautomator / screenshots work.
|
||||
#
|
||||
# Usage:
|
||||
# ./start-android-emulator.sh # headless, background
|
||||
# ./start-android-emulator.sh --wait # headless + wait for boot
|
||||
# ./start-android-emulator.sh --windowed # show emulator window
|
||||
# EMULATOR_HEADLESS=0 ./start-android-emulator.sh
|
||||
# EMULATOR_GPU=host WINDOWED=1 ./start-android-emulator.sh
|
||||
#
|
||||
# Env: EMULATOR_AVD EMULATOR_MEMORY EMULATOR_CORES EMULATOR_GPU
|
||||
# EMULATOR_HEADLESS EMULATOR_LOG EMULATOR_BOOT_TIMEOUT EMULATOR_EXTRA_ARGS
|
||||
#
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib_android_env.sh
|
||||
. "$ROOT/lib_android_env.sh"
|
||||
|
||||
WAIT=0
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--wait) WAIT=1 ;;
|
||||
--windowed| --gui) EMULATOR_HEADLESS=0; export EMULATOR_HEADLESS ;;
|
||||
--headless) EMULATOR_HEADLESS=1; export EMULATOR_HEADLESS ;;
|
||||
-h|--help)
|
||||
sed -n '2,18p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
command -v emulator >/dev/null || {
|
||||
echo "emulator not found (ANDROID_HOME=$ANDROID_HOME)" >&2
|
||||
exit 2
|
||||
}
|
||||
command -v adb >/dev/null || {
|
||||
echo "adb not found" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Already booted?
|
||||
if serial=$(android_adb_serial); [ -n "$serial" ]; then
|
||||
boot=$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true)
|
||||
if [ "$boot" = "1" ]; then
|
||||
echo "already running: $serial (boot_completed=1) — headless default still applies to *new* starts"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Avoid pkill patterns that match this wrapper's argv (self-kill).
|
||||
mapfile -t EMU_ARGS < <(android_emulator_args "$EMULATOR_AVD")
|
||||
echo "starting: emulator ${EMU_ARGS[*]}"
|
||||
echo " headless=$EMULATOR_HEADLESS gpu=$EMULATOR_GPU log=$EMULATOR_LOG"
|
||||
|
||||
if [ "${EMULATOR_HEADLESS}" = "1" ]; then
|
||||
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-offscreen}"
|
||||
# Do not require DISPLAY
|
||||
unset DISPLAY || true
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$EMULATOR_LOG")"
|
||||
# Background; do not use pkill -f emulator from callers matching full cmdline
|
||||
nohup emulator "${EMU_ARGS[@]}" >"$EMULATOR_LOG" 2>&1 &
|
||||
echo "emulator pid=$! log=$EMULATOR_LOG"
|
||||
|
||||
if [ "$WAIT" != "1" ]; then
|
||||
echo "not waiting (pass --wait for boot). adb devices when ready."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "waiting for boot (timeout ${EMULATOR_BOOT_TIMEOUT}s)…"
|
||||
deadline=$((SECONDS + EMULATOR_BOOT_TIMEOUT))
|
||||
serial=""
|
||||
while (( SECONDS < deadline )); do
|
||||
adb wait-for-device 2>/dev/null || true
|
||||
serial=$(android_adb_serial || true)
|
||||
if [ -n "$serial" ]; then
|
||||
boot=$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true)
|
||||
if [ "$boot" = "1" ]; then
|
||||
echo "BOOT_OK serial=$serial headless=$EMULATOR_HEADLESS gpu=$EMULATOR_GPU"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "boot timeout — last log:" >&2
|
||||
tail -30 "$EMULATOR_LOG" >&2 || true
|
||||
exit 3
|
||||
Loading…
Add table
Add a link
Reference in a new issue