diff --git a/scripts/taler-monitoring/GIT-AUTOMATION-NOTES.md b/scripts/taler-monitoring/GIT-AUTOMATION-NOTES.md index 73e654a..f954cfd 100644 --- a/scripts/taler-monitoring/GIT-AUTOMATION-NOTES.md +++ b/scripts/taler-monitoring/GIT-AUTOMATION-NOTES.md @@ -156,12 +156,12 @@ STACK=stage ./android-test/run-android-pay-smoke.sh **User-local emulator (no apt root if SDK already under `$HOME/Android/Sdk`):** ```bash -export ANDROID_HOME=$HOME/Android/Sdk -export PATH=$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH -# AVD e.g. TalerWallet34 (google_apis x86_64) -emulator -avd TalerWallet34 -no-window -gpu swiftshader_indirect -memory 1536 & -adb wait-for-device +# Default = headless (no host window); guest GLES via SwiftShader. +# Prefer the helper — same flags smokes use: +./android-test/start-android-emulator.sh --wait ./android-test/run-android-pay-smoke.sh +# Or let smoke auto-start the AVD (AUTO_START_EMULATOR=1). +# Windowed laptop: WINDOWED=1 ./android-test/start-android-emulator.sh --wait ``` **Limits (clementine 2026-07-17):** host RAM ~3.7 GiB → emulator often ANRs System UI; diff --git a/scripts/taler-monitoring/android-test/GUI-AUTOMATION-NOTES.md b/scripts/taler-monitoring/android-test/GUI-AUTOMATION-NOTES.md index fce915e..27330ca 100644 --- a/scripts/taler-monitoring/android-test/GUI-AUTOMATION-NOTES.md +++ b/scripts/taler-monitoring/android-test/GUI-AUTOMATION-NOTES.md @@ -120,6 +120,39 @@ Automation can run on hosts that support **Android only**, **GUI tooling only** | `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:** @@ -203,6 +236,8 @@ Scripts **today** implement Android only; they should honour: | `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 inference fix branch + smoke (`GUI=1` optional) | +| `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 | --- @@ -233,6 +268,7 @@ Scripts **today** implement Android only; they should honour: - [x] Minimalvariante: was geht / was nicht (Beschränkungen) - [x] Two stacks (goa / stage) - [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) diff --git a/scripts/taler-monitoring/android-test/README.md b/scripts/taler-monitoring/android-test/README.md index e1ab975..4c2e5f2 100644 --- a/scripts/taler-monitoring/android-test/README.md +++ b/scripts/taler-monitoring/android-test/README.md @@ -59,5 +59,16 @@ Canonical notes (minimal variant, **flags**, Linux defaults): | `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 [GIT-AUTOMATION-NOTES.md](../GIT-AUTOMATION-NOTES.md). diff --git a/scripts/taler-monitoring/android-test/lib_android_env.sh b/scripts/taler-monitoring/android-test/lib_android_env.sh new file mode 100644 index 0000000..ad209de --- /dev/null +++ b/scripts/taler-monitoring/android-test/lib_android_env.sh @@ -0,0 +1,153 @@ +# 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:=180}" +: "${EMULATOR_LOG:=/tmp/taler-android-emulator.log}" + +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 + 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 +} diff --git a/scripts/taler-monitoring/android-test/run-android-gui-smoke.sh b/scripts/taler-monitoring/android-test/run-android-gui-smoke.sh index de280f5..f52c783 100755 --- a/scripts/taler-monitoring/android-test/run-android-gui-smoke.sh +++ b/scripts/taler-monitoring/android-test/run-android-gui-smoke.sh @@ -12,14 +12,9 @@ # set -euo pipefail ROOT=$(cd "$(dirname "$0")" && pwd) +# shellcheck source=lib_android_env.sh +. "$ROOT/lib_android_env.sh" -# Platform flags (GUI-AUTOMATION-NOTES.md). Linux → Android only. -os=$(uname -s) -case "${AUTO_PLATFORM:-auto}" in - auto) case "$os" in Linux|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 [ "${AUTO_ANDROID}" = "1" ] || { echo "skipped: AUTO_ANDROID=${AUTO_ANDROID}"; exit 0; } if [ "${AUTO_GUI}" != "1" ]; then echo "AUTO_GUI=0 → deep-link smoke only" @@ -46,17 +41,12 @@ STACK="${STACK:-auto}" SERIAL="${SERIAL:-}" mkdir -p "$OUT_DIR" -if ! command -v adb >/dev/null 2>&1; then - [ -x "$HOME/Android/Sdk/platform-tools/adb" ] && export PATH="$HOME/Android/Sdk/platform-tools:$PATH" -fi command -v adb >/dev/null || { echo "adb missing" >&2; exit 2; } -if [ -z "$SERIAL" ]; then - SERIAL=$(adb devices | awk '/\tdevice$/{print $1; exit}') -fi -[ -n "$SERIAL" ] || { echo "no adb device" >&2; exit 3; } +# 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)" +echo "device: $SERIAL mode: GUI (uiautomator) headless=${EMULATOR_HEADLESS} gpu=${EMULATOR_GPU}" case "$STACK" in auto) diff --git a/scripts/taler-monitoring/android-test/run-android-pay-smoke.sh b/scripts/taler-monitoring/android-test/run-android-pay-smoke.sh index 690c222..ef2ca8e 100755 --- a/scripts/taler-monitoring/android-test/run-android-pay-smoke.sh +++ b/scripts/taler-monitoring/android-test/run-android-pay-smoke.sh @@ -20,15 +20,9 @@ # set -euo pipefail ROOT=$(cd "$(dirname "$0")" && pwd) +# shellcheck source=lib_android_env.sh +. "$ROOT/lib_android_env.sh" -# Platform flags (see GUI-AUTOMATION-NOTES.md). Linux default: Android only. -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) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;; - macos) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;; - *) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;; -esac if [ "${AUTO_ANDROID}" != "1" ]; then echo "skipped: AUTO_ANDROID=${AUTO_ANDROID} (platform flags — see GUI-AUTOMATION-NOTES.md)" exit 0 @@ -46,28 +40,23 @@ OUT_DIR="${OUT_DIR:-$ROOT/out}" mkdir -p "$APK_DIR" "$OUT_DIR" if ! command -v adb >/dev/null 2>&1; then - if [ -x "$HOME/Android/Sdk/platform-tools/adb" ]; then - export PATH="$HOME/Android/Sdk/platform-tools:$PATH" - else - echo "adb missing — install android-tools-adb or user SDK platform-tools" >&2 - exit 2 - fi + echo "adb missing — install android-tools-adb or user SDK platform-tools" >&2 + exit 2 fi -if [ -z "$SERIAL" ]; then - SERIAL=$(adb devices | awk '/\tdevice$/{print $1; exit}') -fi -if [ -z "$SERIAL" ]; then - cat >&2 <<'EOF' -No adb device. Options (no root for user-local emulator if /dev/kvm is usable): +# Default: headless AVD (no host window). WINDOWED=1 / EMULATOR_HEADLESS=0 for UI. +if ! android_ensure_device; then + cat >&2 </dev/null 2>&1; then - [ -x "$HOME/Android/Sdk/platform-tools/adb" ] && export PATH="$HOME/Android/Sdk/platform-tools:$PATH" -fi command -v adb >/dev/null || die "adb not found" command -v python3 >/dev/null || die "python3 required" @@ -112,12 +105,11 @@ if [[ ! -f "${EXP_PW_FILE:-}" && "$STACK" = "stage" ]]; then fi [[ -n "${EXP_PW_FILE:-}" && -f "$EXP_PW_FILE" ]] || die "set EXP_PW_FILE (explorer password)" -if [[ -z "${ANDROID_SERIAL:-}" ]]; then - n=$(adb devices | awk 'NR>1 && $2=="device"{c++} END{print c+0}') - [[ "$n" -ge 1 ]] || die "no adb device (start emulator or plug phone)" - export ANDROID_SERIAL=$(adb devices | awk 'NR>1 && $2=="device"{print $1; exit}') -fi -info "ANDROID_SERIAL=$ANDROID_SERIAL STACK=$STACK PKG=$PKG SHOTDIR=$SHOTDIR" +# 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 "$@"; } diff --git a/scripts/taler-monitoring/android-test/start-android-emulator.sh b/scripts/taler-monitoring/android-test/start-android-emulator.sh new file mode 100755 index 0000000..e62b5f4 --- /dev/null +++ b/scripts/taler-monitoring/android-test/start-android-emulator.sh @@ -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