Files
simplex-chat/apps/multiplatform/scripts/uitest-android.sh
T
Narasimha-sc 69b3cc4f0e ui tests: about 300 more tests across all areas, server control, relays, remote desktop, CI workflow
- Tests for connection plans, prepared chats, edit/delete/selection, forwarding, drafts, group preferences and admission, group lifecycle, compose, live messages, item info, reports and moderation, privacy and SimpleX Lock, chat data and database, address and contact preferences, profiles, contact info, unread and search, files and reactions, group info and events, chat shell and statuses, joining, support and business chats, settings and appearance, network and servers, channels, remote desktop, Android intents and notifications.
- Harness: server stop/start, smp2/xftp2, SOCKS stub, CLI chat relays, CLI remote host/controller, chat scroll and drag helpers, file chooser answers, clipboard in popups.
- ui-tests workflow (manual dispatch only) running both suites on GitHub runners.
- App findings recorded in plan section 15.5 (items 7-23), each covered by a test marked @KnownAppIssue.
2026-10-22 19:45:11 +00:00

155 lines
8.5 KiB
Bash
Executable File

#!/bin/sh
# Usage: scripts/uitest-android.sh <app.apk> <androidTest.apk> [am instrument -e args...]
# Needs scripts/uitest.sh to have run once, or `scripts/uitest.sh --prepare-only` (it builds the CLI and test servers and writes build/uitest/env.properties).
# Env:
# UITEST_SERIAL adb serial of the device or emulator to use; when unset, a Waydroid instance is claimed with scripts/waydroid/wd.sh
# WD_SH another wd.sh to use (default scripts/waydroid/wd.sh; see scripts/waydroid/README.md)
# WD_INSTANCE Waydroid instance (default 2), WD_OWNER claim owner (default uitest-$USER)
# UITEST_APP_ID application id of the app APK (default chat.simplex.app.debug)
# ADB, JAVA_HOME tool locations (default: the adb wd.sh uses, `wd.sh adb-path`; java from JAVA_HOME or PATH)
set -eu
mp="$(dirname "$(readlink -f "$0")")/.."
wd="${WD_SH:-$mp/scripts/waydroid/wd.sh}"
ADB="${ADB:-$("$wd" adb-path)}"
adb() { "$ADB" "$@"; }
app_apk=$1; test_apk=$2; shift 2
export WD_INSTANCE="${WD_INSTANCE:-2}" WD_OWNER="${WD_OWNER:-uitest-$(id -un)}"
pkg="${UITEST_APP_ID:-chat.simplex.app.debug}"
runner=$pkg.test/androidx.test.runner.AndroidJUnitRunner
out="$(dirname "$(readlink -f "$0")")/../build/uitest-android/run-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$out"
t0=$(date +%s)
step() { echo "uitest-android: [$(( $(date +%s) - t0 )) s] $*"; }
bridge_log="$out/bridge.log"
avail_mb=$(awk '/^MemAvailable:/ {print int($2 / 1024)}' /proc/meminfo)
[ "$avail_mb" -ge 4500 ] || { echo "uitest-android: only $avail_mb MB available, need 4500 (bridge + Waydroid); free memory first" >&2; exit 1; }
java="${JAVA_HOME:+$JAVA_HOME/bin/}java"
[ -s "$mp/build/uitest/env.properties" ] || { echo "uitest-android: run scripts/uitest.sh (or scripts/uitest.sh --prepare-only) once first; it builds the CLI and test servers and writes build/uitest/env.properties" >&2; exit 1; }
cp_file="$mp/build/uitest/bridge.classpath"
h="$mp/common/src/desktopTest/kotlin/chat/simplex/app/ui/harness"
bridge_srcs="$h/UiBridge.kt $h/CliPeer.kt $h/VtScreen.kt $h/TestServers.kt $h/Socks5Stub.kt $h/UiEnv.kt $h/UiJvm.kt $mp/common/src/uiTestShared/kotlin/chat/simplex/app/ui/harness/ServersConfig.kt $mp/common/src/uiTestShared/kotlin/chat/simplex/app/ui/harness/PeerLines.kt"
wd_pid=
cleanup() {
[ -z "${bridge_pid:-}" ] || kill -TERM "$bridge_pid" 2>/dev/null || true
[ -z "$wd_pid" ] || wait "$wd_pid" 2>/dev/null || true
[ -n "${UITEST_SERIAL:-}" ] || "$wd" release >/dev/null 2>&1 || true
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if [ ! -s "$cp_file" ] || [ -n "$(find $bridge_srcs -newer "$cp_file" 2>/dev/null)" ]; then
step "compiling the bridge"
(cd "$mp" && ./gradlew -g "$mp/.gradle/uitest-home" --no-daemon -q -Pkotlin.compiler.execution.strategy=in-process :common:uitestBridgeClasspath) > "$out/bridge-build.log" 2>&1 \
|| { echo "uitest-android: bridge build failed; see $out/bridge-build.log" >&2; exit 1; }
fi
if [ -z "${UITEST_SERIAL:-}" ]; then
"$wd" claim "$WD_OWNER"
if ! "$wd" status | grep -q "Session:.*RUNNING"; then
"$wd" up > "$out/waydroid-up.log" 2>&1 &
wd_pid=$!
fi
fi
setsid "$java" -Djava.awt.headless=true -Dsimplex.uitest.env="$mp/build/uitest/env.properties" -cp "$(cat "$cp_file")" chat.simplex.app.ui.harness.UiBridge 7777 > "$bridge_log" 2>&1 &
bridge_pid=$!
i=0
until grep -q "^uitest-bridge: smp" "$bridge_log" 2>/dev/null; do
i=$((i + 1)); [ "$i" -lt 1200 ] || { echo "uitest-android: bridge did not start; see $bridge_log" >&2; exit 1; }
kill -0 "$bridge_pid" 2>/dev/null || { echo "uitest-android: bridge exited; see $bridge_log" >&2; exit 1; }
sleep 0.1
done
base=$(sed -n 's/^uitest-bridge: .*port base \([0-9]*\),.*/\1/p' "$bridge_log")
step "bridge up, server port base $base"
if [ -n "$wd_pid" ]; then
wait "$wd_pid" || { echo "uitest-android: wd.sh up failed; see $out/waydroid-up.log" >&2; wd_pid=; exit 1; }
wd_pid=
fi
serial="${UITEST_SERIAL:-$("$wd" serial)}"
step "device $serial ready"
echo "uitest-android: note: pm clear removes the app's data on $serial"
app_sum=$(sha256sum "$app_apk" | cut -c1-16)
stamp="$mp/build/uitest-android/installed-app-$(printf %s "$serial" | tr -c "A-Za-z0-9" _)"
installed_ver=$(adb -s "$serial" shell dumpsys package "$pkg" 2>/dev/null | tr -d '\r' | sed -n 's/^ *lastUpdateTime=//p' | head -1)
if [ -n "$installed_ver" ] && [ "$(cat "$stamp" 2>/dev/null)" = "$app_sum $installed_ver" ]; then
echo "uitest-android: app APK unchanged on $serial, skipping its install"
else
if [ -n "${UITEST_SERIAL:-}" ]; then adb -s "$serial" install -r -g "$app_apk" >/dev/null; else "$wd" install "$app_apk"; fi
installed_ver=$(adb -s "$serial" shell dumpsys package "$pkg" | tr -d '\r' | sed -n 's/^ *lastUpdateTime=//p' | head -1)
echo "$app_sum $installed_ver" > "$stamp"
fi
adb -s "$serial" install -r -t "$test_apk"
step "APKs installed"
for p in 7777 $((base + 1)) $((base + 2)) $((base + 11)) $((base + 12)) $((base + 20)) $((base + 90)); do adb -s "$serial" reverse "tcp:$p" "tcp:$p" >/dev/null; done
prepare_app() {
adb -s "$serial" shell pm clear "$pkg" >/dev/null
adb -s "$serial" shell settings put global heads_up_notifications_enabled 0
for perm in android.permission.POST_NOTIFICATIONS android.permission.CAMERA android.permission.RECORD_AUDIO; do
adb -s "$serial" shell pm grant "$pkg" "$perm" 2>/dev/null || true
done
}
# Runs `am instrument` into $1. If its output does not change for UITEST_STALL_S seconds (default 420, longer than the
# in-process 300 s watchdog), the app process is force-stopped so the pass ends as "Process crashed" and the run resumes:
# under arm64 translation the native allocator can deadlock (scudo::HybridMutex::lockSlow in several threads), which
# blocks every thread of the process, the in-process watchdog included (scripts/waydroid/README.md).
instrument() {
file=$1; shift
adb -s "$serial" shell am instrument -w -r "$@" "$runner" > "$file" 2>&1 &
ipid=$!
last=-1; idle=0; stall="${UITEST_STALL_S:-420}"
while kill -0 "$ipid" 2>/dev/null; do
sleep 15
size=$(wc -c < "$file")
if [ "$size" = "$last" ]; then idle=$((idle + 15)); else idle=0; last=$size; fi
if [ "$idle" -ge "$stall" ]; then
step "no instrumentation output for $idle s; force-stopping $pkg (stalled process)"
adb -s "$serial" shell am force-stop "$pkg" >/dev/null 2>&1
idle=0
fi
done
wait "$ipid"
}
# Prints one line per test in an `am instrument -r` output: "<class>#<test> <code>" for finished tests (0 pass, -2 fail, -3/-4 skipped)
# and "<class>#<test> crashed" for a test that started without finishing.
results() {
tr -d '\r' < "$1" | awk '
/^INSTRUMENTATION_STATUS: class=/ { c = substr($0, 31) }
/^INSTRUMENTATION_STATUS: test=/ { t = substr($0, 30) }
/^INSTRUMENTATION_STATUS_CODE: / { code = $2; if (code == 1) { running = c "#" t } else { print c "#" t, code; running = "" } }
END { if (running != "") print running, "crashed" }'
}
shots=/sdcard/Android/data/$pkg/files/failures
adb -s "$serial" shell rm -rf "$shots" || true
adb -s "$serial" logcat -G 16M >/dev/null 2>&1 || true
adb -s "$serial" logcat -c || true
: > "$out/results.txt"
pass=1
gave_up=
while :; do
prepare_app
skip=$(cut -d' ' -f1 "$out/results.txt" | paste -sd, -)
set +e
if [ -n "$skip" ]; then
instrument "$out/instrument-$pass.txt" "$@" -e notClass "$skip"
else
instrument "$out/instrument-$pass.txt" "$@"
fi
rc=$?
set -e
results "$out/instrument-$pass.txt" >> "$out/results.txt"
adb -s "$serial" pull "$shots" "$out/" >/dev/null 2>&1 || true
step "instrumentation pass $pass finished (am instrument exit $rc)"
tr -d '\r' < "$out/instrument-$pass.txt" | grep -q "^INSTRUMENTATION_RESULT: shortMsg=" || break
[ "$pass" -lt 4 ] || { echo "uitest-android: process crashed in 4 passes, giving up" >&2; gave_up=1; break; }
pass=$((pass + 1))
echo "uitest-android: the app process crashed; resuming with the remaining tests (pass $pass)"
done
cat "$out"/instrument-*.txt > "$out/instrument.txt"
adb -s "$serial" logcat -d > "$out/logcat.txt" 2>&1 || true
grep -E "INSTRUMENTATION_STATUS: stack=" "$out/instrument.txt" | cut -c1-1500 || true
grep -E "UITEST|TestRunner: (started|finished|failed)" "$out/logcat.txt" | sed "s/^[0-9-]* //" > "$out/timings.txt" || true
echo "uitest-android: results ($out/results.txt; 0 pass, -2 fail, -3/-4 skipped):"
sed 's/^chat\.simplex\.app\.ui\.//' "$out/results.txt"
echo "uitest-android: $(grep -c ' 0$' "$out/results.txt") passed, $(grep -c ' -2$' "$out/results.txt") failed, $(grep -c ' crashed$' "$out/results.txt") crashed; results in $out"
[ -z "$gave_up" ] && ! grep -qE ' (-2|crashed)$' "$out/results.txt" && [ -s "$out/results.txt" ]