summaryrefslogtreecommitdiff
path: root/bin/android-launch-check
diff options
context:
space:
mode:
Diffstat (limited to 'bin/android-launch-check')
-rwxr-xr-xbin/android-launch-check159
1 files changed, 159 insertions, 0 deletions
diff --git a/bin/android-launch-check b/bin/android-launch-check
new file mode 100755
index 0000000..00dfcfd
--- /dev/null
+++ b/bin/android-launch-check
@@ -0,0 +1,159 @@
+#!/usr/bin/env bash
+#
+# Determine whether an Android app actually launches and stays up, and surface
+# the exception if it does not.
+#
+# Why this exists: `adb shell monkey -p <pkg> -c android.intent.category.LAUNCHER 1`
+# followed by polling `pidof` is not a reliable check. In practice it reported a
+# healthy app as dead and a dead app as healthy, in the same session, on the same
+# emulator — monkey's launch can land on whatever activity happens to be in the
+# foreground, and a single `pidof` sample races app startup and restart.
+#
+# This script instead:
+# * resolves the launcher activity from the package manager rather than guessing
+# * starts a continuous logcat capture BEFORE launching, so the crash window is
+# never missed (the `-b crash` buffer was empty even for real FATAL crashes)
+# * launches with `am start -W` and checks its reported status
+# * samples the pid repeatedly over a settle period, so a process that starts
+# and then dies is reported as a failure, not a success
+# * greps the captured log for FATAL EXCEPTION and prints the stack
+#
+# Usage:
+# android-launch-check <package> [--serial <adb-serial>] [--settle <seconds>]
+#
+# Exit: 0 = launched and stayed up with no fatal exception, 1 = did not.
+
+set -euo pipefail
+
+die() { printf '%s: %s\n' "$(basename "$0")" "$*" >&2; exit 2; }
+
+[ $# -ge 1 ] || die "usage: $(basename "$0") <package> [--serial <s>] [--settle <secs>]"
+
+pkg=$1; shift
+serial=""
+settle=20
+apk=""
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --serial) serial=${2:-}; shift 2 ;;
+ --settle) settle=${2:-20}; shift 2 ;;
+ --apk) apk=${2:-}; shift 2 ;;
+ *) die "unknown argument: $1" ;;
+ esac
+done
+
+adb_bin=$(command -v adb || echo /opt/android-sdk/platform-tools/adb)
+[ -x "$adb_bin" ] || die "adb not found"
+ADB=("$adb_bin")
+[ -n "$serial" ] && ADB+=(-s "$serial")
+
+devices=$("${ADB[@]}" devices | awk 'NR>1 && $2=="device" {print $1}')
+[ -n "$devices" ] || die "no adb device in 'device' state"
+[ "$(printf '%s\n' "$devices" | wc -l)" -eq 1 ] || [ -n "$serial" ] \
+ || die "multiple devices attached; pass --serial. Found: $(echo $devices)"
+
+printf '=== android-launch-check ===\n'
+printf 'package : %s\n' "$pkg"
+printf 'device : %s\n' "$(echo $devices | tr '\n' ' ')"
+
+if [ -n "$apk" ]; then
+ [ -f "$apk" ] || die "apk not found: $apk"
+ # adb rejects any filename not ending .apk/.apex, and prints that refusal to
+ # stdout while still exiting 0 in some versions — so check the text as well as
+ # the status. Discarding this output is how a "reinstall" can silently not
+ # happen and leave the previous build under test.
+ case "$apk" in
+ *.apk|*.apex) : ;;
+ *) die "adb will refuse this filename (must end .apk/.apex): $apk" ;;
+ esac
+ printf 'apk : %s\n' "$apk"
+ install_out=$("${ADB[@]}" install -r "$apk" 2>&1 | tr -d '\r' || true)
+ if ! printf '%s' "$install_out" | grep -q '^Success$'; then
+ printf 'result : FAIL — install did not succeed\n'
+ printf '%s\n' "$install_out" | sed 's/^/ /'
+ exit 1
+ fi
+ printf 'install : Success\n'
+fi
+
+# Membership test done in pure shell, not `... | grep -q`. Under `set -o pipefail`
+# a `grep -q` that exits on its first match can SIGPIPE the upstream command, and
+# the pipeline then reports failure even though the match succeeded — which made
+# this check report an installed package as missing.
+pkg_list=$("${ADB[@]}" shell pm list packages 2>/dev/null | tr -d '\r' || true)
+case $'\n'"$pkg_list"$'\n' in
+ *$'\n'"package:$pkg"$'\n'*) : ;;
+ *) die "package not installed: $pkg" ;;
+esac
+
+# Resolve rather than assume. Guessing ".MainActivity" when the manifest says
+# ".ui.MainActivity" produces "Activity class does not exist", which is an
+# operator error that can easily be misread as an app crash.
+activity=$("${ADB[@]}" shell cmd package resolve-activity --brief "$pkg" 2>/dev/null \
+ | tr -d '\r' | tail -1)
+case "$activity" in
+ "$pkg"/*) : ;;
+ *) die "could not resolve launcher activity for $pkg (got: '$activity')" ;;
+esac
+printf 'activity: %s\n' "$activity"
+
+logfile=$(mktemp); trap 'rm -f "$logfile"' EXIT
+
+"${ADB[@]}" shell am force-stop "$pkg" >/dev/null 2>&1 || true
+sleep 2
+"${ADB[@]}" logcat -c >/dev/null 2>&1 || true
+
+# Capture starts first and runs across the whole window.
+"${ADB[@]}" logcat -v threadtime > "$logfile" 2>&1 &
+logcat_pid=$!
+trap 'kill "$logcat_pid" 2>/dev/null || true; rm -f "$logfile"' EXIT
+sleep 2
+
+start_out=$("${ADB[@]}" shell am start -W -n "$activity" 2>&1 | tr -d '\r')
+start_status=$(printf '%s\n' "$start_out" | awk -F': *' '/^Status:/ {print $2}')
+printf 'am start: %s\n' "${start_status:-<none>}"
+
+# Sample repeatedly. One sample cannot distinguish "not started yet" from
+# "started and already died".
+alive_samples=0; total_samples=0; last_pid=""
+interval=5
+elapsed=0
+while [ "$elapsed" -lt "$settle" ]; do
+ sleep "$interval"
+ elapsed=$((elapsed + interval))
+ # `|| true` is load-bearing: pidof exits non-zero when the process is gone,
+ # which under `set -e` would abort this script at exactly the moment it is
+ # supposed to be detecting a crash.
+ pid=$("${ADB[@]}" shell pidof "$pkg" 2>/dev/null | tr -d '\r' | awk '{print $1}' || true)
+ total_samples=$((total_samples + 1))
+ if [ -n "$pid" ]; then alive_samples=$((alive_samples + 1)); last_pid=$pid; fi
+ printf ' t=%-3ss pid=%s\n' "$elapsed" "${pid:-<none>}"
+done
+
+sleep 1
+kill "$logcat_pid" 2>/dev/null || true
+wait "$logcat_pid" 2>/dev/null || true
+
+fatal_count=$(grep -c "FATAL EXCEPTION" "$logfile" 2>/dev/null || true)
+fatal_count=${fatal_count:-0}
+
+printf 'samples : %d/%d alive\n' "$alive_samples" "$total_samples"
+printf 'fatal : %s\n' "$fatal_count"
+
+if [ "$fatal_count" -gt 0 ]; then
+ printf '\n--- first fatal exception ---\n'
+ grep -A 25 -m 1 "FATAL EXCEPTION" "$logfile" | sed 's/^/ /'
+ printf '\nresult : FAIL — app crashed\n'
+ exit 1
+fi
+
+if [ "$alive_samples" -ne "$total_samples" ]; then
+ printf '\nresult : FAIL — process was not alive for the whole settle window\n'
+ printf 'hint : started but died, or never started. No FATAL in logcat, so\n'
+ printf ' check for a native crash or an early System.exit.\n'
+ exit 1
+fi
+
+printf 'pid : %s\n' "$last_pid"
+printf 'result : PASS — launched and stayed up %ss with no fatal exception\n' "$settle"
+exit 0