Files
DroidDeck/tools/device-smoke.sh

298 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# Smoke test of a DroidDeck build on a real device, over plain adb (no python, no root).
#
# What it does, in order, and restores afterwards:
# 1. prints the device (model, Android, kernel, page size, SoC);
# 2. installs --apk when given and launches the app;
# 3. checks that the app starts and that nothing in the crash buffer belongs to it;
# 4. changes the display density and the window size and checks the app keeps its process
# (and does not recreate the activity that is on top);
# 5. on a foldable cycles the device state (half-opened, and with --closed also closed) through
# `cmd device_state`, then puts the original state back;
# 6. reads the newest session folder of a debug build: first frame, last frame rate, the
# memory page size, the proot that session ran, and the last process exits.
#
# Usage: tools/device-smoke.sh [--serial SERIAL] [--apk FILE] [--pkg PACKAGE]
# [--no-density] [--no-size] [--no-fold] [--closed]
# Exit status: 0 when no check failed, 1 when one did, 2 for a usage or device problem.
#
# Not a replacement for tools/droiddeck-scenario, which drives a game through the agent bridge;
# this covers what that does not: display changes and foldable states.
set -u
export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'
pkg=com.droiddeck.launcher
serial=${ANDROID_SERIAL:-}
apk=
do_density=1
do_size=1
do_fold=1
do_closed=0
adb_bin=${ADB:-adb}
while [ $# -gt 0 ]; do
case $1 in
--serial) [ $# -ge 2 ] || { echo "--serial needs a value" >&2; exit 2; }; serial=$2; shift ;;
--apk) [ $# -ge 2 ] || { echo "--apk needs a value" >&2; exit 2; }; apk=$2; shift ;;
--pkg) [ $# -ge 2 ] || { echo "--pkg needs a value" >&2; exit 2; }; pkg=$2; shift ;;
--no-density) do_density=0 ;;
--no-size) do_size=0 ;;
--no-fold) do_fold=0 ;;
--closed) do_closed=1 ;;
-h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $1" >&2; exit 2 ;;
esac
shift
done
adb_cmd=("$adb_bin")
[ -n "$serial" ] && adb_cmd+=(-s "$serial")
A() {
local out rc
out=$("${adb_cmd[@]}" "$@" 2>&1); rc=$?
[ -z "$out" ] || printf '%s\n' "$out" | tr -d '\r'
return $rc
}
sh_() {
local command= arg
for arg in "$@"; do command+="'${arg//\'/\'\\\'\'}' "; done
A shell "$command"
}
pass=0 fail=0 skip=0
ok() { echo " PASS $*"; pass=$((pass + 1)); }
bad() { echo " FAIL $*"; fail=$((fail + 1)); }
note() { echo " SKIP $*"; skip=$((skip + 1)); }
head_() { printf '\n== %s\n' "$*"; }
state=$(A get-state)
if [ "$state" != "device" ]; then
echo "no authorized device (adb get-state: $state)" >&2
exit 2
fi
density_snapshot=$(sh_ wm density) || density_snapshot=
size_snapshot=$(sh_ wm size) || size_snapshot=
orig_density=$(echo "$density_snapshot" | sed -n 's/.*Physical density: \([0-9]*\).*/\1/p' | head -1)
override_density=$(echo "$density_snapshot" | sed -n 's/.*Override density: \([0-9]*\).*/\1/p' | head -1)
override_size=$(echo "$size_snapshot" | sed -n 's/.*Override size: \([0-9]*x[0-9]*\).*/\1/p' | head -1)
ds=$(sh_ cmd device_state state 2>/dev/null)
orig_state=$(echo "$ds" | sed -n "s/.*Committed state: DeviceState{identifier=\([0-9]*\),.*/\1/p" | head -1)
pre_override=$(echo "$ds" | sed -n "s/.*Override state: DeviceState{identifier=\([0-9]*\),.*/\1/p" | head -1)
density_changed=0 size_changed=0 fold_changed=0
wm_override() { sed -n "s/.*Override $1: \\([0-9x]*\\).*/\\1/p" | head -1; }
restore_density() {
sh_ wm density "${override_density:-reset}" >/dev/null || return 1
local current
current=$(sh_ wm density) || return 1
echo "$current" | grep -q 'Physical density:' || return 1
[ "$(echo "$current" | wm_override density)" = "$override_density" ]
}
restore_size() {
sh_ wm size "${override_size:-reset}" >/dev/null || return 1
local current
current=$(sh_ wm size) || return 1
echo "$current" | grep -q 'Physical size:' || return 1
[ "$(echo "$current" | wm_override size)" = "$override_size" ]
}
# An override that was already in place (a test lab pins one) is put back; otherwise the request is cancelled.
restore_fold() { if [ -n "$pre_override" ]; then sh_ cmd device_state state "$pre_override" >/dev/null; else sh_ cmd device_state state reset >/dev/null; fi; }
restore() {
local failed=0
if [ "$density_changed" = 1 ]; then
if restore_density; then density_changed=0; else bad "could not restore display density"; failed=1; fi
fi
if [ "$size_changed" = 1 ]; then
if restore_size; then size_changed=0; else bad "could not restore window size"; failed=1; fi
fi
if [ "$fold_changed" = 1 ]; then
if restore_fold; then fold_changed=0; else bad "could not restore fold override"; failed=1; fi
fi
return "$failed"
}
finish() {
local status=$?
restore || status=1
printf '\n%d passed, %d failed, %d skipped\n' "$pass" "$fail" "$skip"
[ "$fail" = 0 ] || status=1
exit "$status"
}
trap finish EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
top_activity() {
sh_ dumpsys activity activities 2>/dev/null | sed -n 's/.*topResumedActivity=ActivityRecord{[^ ]* u[0-9]* \([^ ]*\) .*/\1/p' | head -1
}
app_pid() { sh_ pidof "$pkg" | awk '{print $1}'; }
app_crashes() {
local out pat
out=$(sh_ logcat -b crash -d) || return 1
pat=$(printf '%s' "$pkg" | sed 's/[.[\*^$]/\\&/g')
printf '%s\n' "$out" | grep -c -E "(Process: |Cmdline: |>>> )$pat([ ,<:]|$)" || true
}
relaunches_of() { # $1 = activity class fragment
sh_ logcat -b events -d 2>/dev/null | grep -E 'wm_(relaunch|destroy|create)_activity' | grep -c "$1"
}
head_ "Device"
echo " model $(sh_ getprop ro.product.manufacturer) $(sh_ getprop ro.product.model)"
echo " android $(sh_ getprop ro.build.version.release) (API $(sh_ getprop ro.build.version.sdk)), One UI $(sh_ getprop ro.build.version.oneui)"
echo " kernel $(sh_ uname -r)"
page=$(sh_ getconf PAGE_SIZE)
echo " page size $page"
echo " soc $(sh_ getprop ro.soc.manufacturer) $(sh_ getprop ro.soc.model) ($(sh_ getprop ro.board.platform))"
echo " gpu $(sh_ cat /sys/class/kgsl/kgsl-3d0/gpu_model 2>/dev/null)"
echo " display $(sh_ wm size | tr '\n' ' ') $(sh_ wm density | tr '\n' ' ')"
[ "$page" = 4096 ] || [ "$page" = 16384 ] && ok "page size is $page" || bad "page size reads '$page'"
head_ "Install and launch"
sh_ logcat -b crash -c >/dev/null
sh_ logcat -b events -c >/dev/null
if [ -n "$apk" ]; then
r=$(A install -r -t "$apk" | tail -1)
case $r in *Success*) ok "installed $(basename "$apk")" ;; *) bad "install: $r"; exit 1 ;; esac
fi
packages=$(sh_ pm list packages "$pkg") || { echo "could not query installed packages" >&2; exit 2; }
if ! printf '%s\n' "$packages" | grep -Fxq "package:$pkg"; then echo "package $pkg is not installed" >&2; exit 2; fi
if ! launch=$(sh_ am start -n "$pkg/.MainActivity") || printf '%s\n' "$launch" | grep -q '^Error'; then
bad "could not launch $pkg/.MainActivity"
fi
sleep 6
pid=$(app_pid)
[ -n "$pid" ] && ok "process running (pid $pid)" || bad "the app has no process after launch"
t=$(top_activity)
case $t in
"$pkg"/*|*permissioncontroller*|*GrantPermissions*) ok "top activity: $t" ;;
*) bad "top activity is '$t'" ;;
esac
if c=$(app_crashes); then
[ "${c:-0}" = 0 ] && ok "nothing in the crash buffer belongs to the app" || bad "$c crash entries mention the app"
else
bad "could not read the crash buffer"
fi
if [ "$do_density" = 1 ] && [ -n "$orig_density" ]; then
head_ "Density change"
target=$((orig_density * 5 / 4))
before=$(top_activity); pid0=$(app_pid)
sh_ logcat -b events -c >/dev/null
density_changed=1
wm_status=0
sh_ wm density "$target" >/dev/null || wm_status=$?
sleep 6
applied=$(sh_ wm density) || wm_status=1
after=$(top_activity); pid1=$(app_pid)
if [ "$wm_status" -ne 0 ] || [ "$(echo "$applied" | wm_override density)" != "$target" ]; then
bad "wm density failed"
else
if [ -n "$pid1" ] && [ "$pid0" = "$pid1" ]; then
ok "same process after $orig_density -> $target dpi"
else
bad "process changed ($pid0 -> $pid1)"
fi
case $before in "$pkg"/*)
n=$(relaunches_of "${before#*/}")
[ "${n:-0}" = 0 ] && ok "$before was not recreated" || bad "$before was recreated ${n}x"
[ "$after" = "$before" ] && ok "same top activity after the change" || bad "top activity changed ($before -> $after)"
;; *) note "top activity was not the app ($before)" ;; esac
fi
if restore_density; then
density_changed=0
else
bad "could not restore display density"
fi
sleep 3
elif [ "$do_density" = 1 ]; then
note "could not read the original display density"
fi
if [ "$do_size" = 1 ]; then
head_ "Window size change"
phys=$(echo "$size_snapshot" | sed -n 's/.*Physical size: \([0-9]*\)x\([0-9]*\).*/\1 \2/p' | head -1)
set -- $phys
if [ -n "${1:-}" ]; then
w=$(( ($2 * 2 / 3) / 2 * 2 )); h=$1 # a narrower, taller window, like a cover screen
pid0=$(app_pid)
size_changed=1
wm_status=0
sh_ wm size "${w}x${h}" >/dev/null || wm_status=$?
sleep 6
applied=$(sh_ wm size) || wm_status=1
pid1=$(app_pid)
if [ "$wm_status" -ne 0 ] || [ "$(echo "$applied" | wm_override size)" != "${w}x${h}" ]; then
bad "wm size failed"
elif [ -n "$pid1" ] && [ "$pid0" = "$pid1" ]; then
ok "same process at ${w}x${h}"
else
bad "process changed ($pid0 -> $pid1)"
fi
if restore_size; then
size_changed=0
else
bad "could not restore window size"
fi
sleep 3
else
note "could not read the physical size"
fi
fi
if [ "$do_fold" = 1 ]; then
head_ "Foldable states"
states=$(sh_ cmd device_state print-states 2>/dev/null)
if ! echo "$states" | grep -q "identifier"; then
note "no device_state service (not a foldable, or not permitted)"
elif [ -z "$orig_state" ]; then
note "could not read the current device_state identifier"
else
id_of() { echo "$states" | sed -n "s/.*identifier=\([0-9]*\), name='$1'.*/\1/p" | head -1; }
half=$(id_of HALF_OPENED); closed=$(id_of CLOSED)
cycle() { # $1 name $2 id
pid0=$(app_pid)
fold_changed=1
sh_ cmd device_state state "$2" >/dev/null; sleep 8
got=$(sh_ cmd device_state state | sed -n "s/.*Committed state: DeviceState{identifier=[0-9]*, name='\([A-Z_]*\)'.*/\1/p" | head -1)
pid1=$(app_pid)
[ "$got" = "$1" ] && ok "device state is $1" || note "asked for $1, committed '$got'"
[ -n "$pid1" ] && [ "$pid0" = "$pid1" ] && ok "same process in $1" || bad "process changed in $1 ($pid0 -> $pid1)"
restore_fold; sleep 6
}
if [ -n "$half" ]; then cycle HALF_OPENED "$half"; else note "no HALF_OPENED state"; fi
if [ "$do_closed" = 1 ] && [ -n "$closed" ]; then
cycle CLOSED "$closed"
note "CLOSED turns the screens off: the session pauses (Background behaviour), Resume it from the app"
fi
got=$(sh_ cmd device_state state | sed -n "s/.*Committed state: DeviceState{identifier=\([0-9]*\),.*/\1/p" | head -1)
[ "$got" = "$orig_state" ] && ok "device state restored ($orig_state)" || bad "device state is $got, was $orig_state"
fi
fi
head_ "Session log (debug builds)"
dir=$(sh_ run-as "$pkg" ls -t files/logs 2>/dev/null | head -1)
if [ -z "$dir" ] || echo "$dir" | grep -q -i "not debuggable\|unknown package"; then
note "no readable session folder (release build, or no session has run)"
else
echo " newest session $dir"
ev=$(sh_ run-as "$pkg" cat "files/logs/$dir/events.jsonl" 2>/dev/null)
echo "$ev" | grep -q '"frame.first"' && ok "first frame was drawn" || note "no frame.first in $dir"
echo "$ev" | grep -q '"session.failed"' && bad "session.failed: $(echo "$ev" | grep '"session.failed"' | tail -1 | cut -c1-160)"
report=$(sh_ run-as "$pkg" cat "files/logs/$dir/device.txt" 2>/dev/null)
proot_line=$(echo "$report" | grep 'proot binary' | head -1)
case $proot_line in
*"rootfs fallback"*|*"missing"*) bad "session proot is not the patched binary ($proot_line)" ;;
*"apk"*|*"add-on pack"*) ok "patched proot ($proot_line)" ;;
*) note "device report has no proot binary line (build from before the check)" ;;
esac
app_log=$(sh_ run-as "$pkg" cat "files/logs/$dir/app.log" 2>/dev/null)
echo "$app_log" | grep -Fq 'proot: fast path on (' && ok "proot fast path was on" \
|| note "proot fast path activation was not recorded ($(echo "$report" | grep 'proot fast path' | tr '\n' '; '))"
fps=$(sh_ run-as "$pkg" cat "files/logs/$dir/app.log" 2>/dev/null | grep 'last 10 s:' | tail -1 | sed 's/.*last 10 s: //' | cut -c1-70)
[ -n "$fps" ] && echo " last stats $fps"
sh_ run-as "$pkg" cat "files/logs/$dir/device.txt" 2>/dev/null | grep -E 'Memory page size|^Kernel' | sed 's/^/ report /'
fi
[ "$fail" = 0 ]