Files
simplex-chat/apps/multiplatform/scripts/uitest-android.sh
T
Narasimha-sc b87dd8a86d ui tests: test-writing guide, stricter harness, reviewed and new tests
- apps/multiplatform/common/src/uiTestShared/README.md: rules for writing UI tests, from the test review;
- harness: any app crash fails the test (both platforms); known app bugs are marked with @KnownAppIssue and reported as skipped; no input retries (Android types through the input connection); peer error lines and app work still running at the end fail the test; restart re-reads settings from disk; Android stall watcher and resume after any abnormal instrumentation end; shared hideKeyboard();
- tests: ~25 merged or dropped, the rest strengthened per the review; ~25 new: files and images (local XFTP server), received replies, delivery status, disappearing messages, mentions, group links, incognito join, moderation, block for all, "show last messages", hidden profiles, Android onboarding;
- app: test keys only, on elements the new tests act on;
- plans: test review reports, findings 43-53, app findings 5-6, results.
2026-10-19 22:11:49 +00:00

155 lines
8.3 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 (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 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/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)); 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" ]