aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLena <lena@omega>2026-07-01 00:00:00 +0000
committerLena <lena@omega>2026-07-01 00:00:00 +0000
commitf6668454daaa0b5b559723b3bb685c244456247b (patch)
tree174280a1542fa4d1f5c9427b00f853458ca32022
parent30ee8be82e44d1a8250d1f6e27e71f7d5f24aec5 (diff)
downloadscrcpy-android-f6668454daaa0b5b559723b3bb685c244456247b.tar.gz
test: add a screenshot capture tier
The store screenshots were made by hand, so nothing kept them in step with the UI. Add ./test screenshots, which boots a pixel_6 AVD on the same image the e2e tier uses and overwrites the tracked PNGs. The AVD is its own so a capture run cannot disturb the device state e2e asserts against, and the panel comes from the device profile: forcing geometry with wm size on a booted software-GPU emulator ANRs SystemUI and the ANR dialog lands in the capture. SystemUI demo mode does the same, so the status bar is left live. Each shot is rejected unless the app owns the focused window and the PNG has the expected geometry.
-rw-r--r--README9
-rwxr-xr-xtest11
-rwxr-xr-xtest-rig/run.sh15
-rwxr-xr-xtest-rig/screenshots.sh109
4 files changed, 133 insertions, 11 deletions
diff --git a/README b/README
index b6618cf..11de229 100644
--- a/README
+++ b/README
@@ -82,6 +82,15 @@ use and cached.
# taps/keys, and stitches a screen recording into
# ./output.mp4. Needs /dev/kvm.
+ ./test screenshots
+ # Boots a pixel_6 AVD (natively 1080x2400), captures
+ # Main and Settings, and overwrites the tracked PNGs
+ # under fastlane/metadata/android/en-US/images/
+ # phoneScreenshots. Refuses to write a shot unless the
+ # app owns the focused window and the PNG has the
+ # expected geometry. Review the diff before
+ # committing. Needs /dev/kvm.
+
./test apk # Smoke build: generates a throwaway PKCS12 keystore
# under .tools/, runs scripts/build-apk against it,
# verifies the resulting APK is signed. No KVM.
diff --git a/test b/test
index ec31236..d5bf317 100755
--- a/test
+++ b/test
@@ -7,6 +7,9 @@
# ./test all unit + e2e
# ./test record boot emulator, run a Mirror session, inject events,
# capture screen recording to ./output.mp4 (needs /dev/kvm)
+# ./test screenshots
+# boot emulator, capture the fastlane store screenshots
+# into fastlane/metadata/.../phoneScreenshots (needs /dev/kvm)
# ./test apk build a signed release APK with a throwaway keystore
# (exercises scripts/build-apk end-to-end, no KVM)
#
@@ -20,7 +23,7 @@ cmd="${1:-unit}"
target=unit
case "$cmd" in
- e2e|all|record) target=e2e ;;
+ e2e|all|record|screenshots) target=e2e ;;
esac
# The image tag is a hash of the rig inputs, so editing the Dockerfile
@@ -30,14 +33,14 @@ IMG="scrcpy-android-test-$target:$(cat "$ROOT/test-rig/Dockerfile" \
| sha256sum | cut -c1-12)"
case "$cmd" in
- unit|e2e|all|record|apk) ;;
+ unit|e2e|all|record|screenshots|apk) ;;
-h|--help)
# Usage is the header comment block: print from line 2 down to
# the first non-comment line.
awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0" >&2
exit 0 ;;
*)
- echo "usage: $0 [unit|e2e|all|record|apk]" >&2
+ echo "usage: $0 [unit|e2e|all|record|screenshots|apk]" >&2
exit 2 ;;
esac
@@ -62,7 +65,7 @@ fi
# KVM check for tiers that boot the emulator.
needs_kvm=
case "$cmd" in
- e2e|all|record) needs_kvm=1 ;;
+ e2e|all|record|screenshots) needs_kvm=1 ;;
esac
kvm_args=
diff --git a/test-rig/run.sh b/test-rig/run.sh
index ff63c21..b55ce1e 100755
--- a/test-rig/run.sh
+++ b/test-rig/run.sh
@@ -1,6 +1,6 @@
#!/bin/sh
# In-container orchestrator. Called by ./test from outside.
-# /work/test-rig/run.sh unit|e2e|all
+# /work/test-rig/run.sh unit|e2e|all|record|screenshots|apk
#
# Expects to run inside the scrcpy-android-test docker image with
# /work mounted at the project root.
@@ -24,10 +24,11 @@ e2e() {
}
case "$cmd" in
- unit) unit ;;
- e2e) e2e ;;
- all) unit && e2e ;;
- record) /work/test-rig/record.sh ;;
- apk) /work/test-rig/build-apk.sh ;;
- *) echo "run.sh: bad cmd $cmd" >&2; exit 2 ;;
+ unit) unit ;;
+ e2e) e2e ;;
+ all) unit && e2e ;;
+ record) /work/test-rig/record.sh ;;
+ screenshots) /work/test-rig/screenshots.sh ;;
+ apk) /work/test-rig/build-apk.sh ;;
+ *) echo "run.sh: bad cmd $cmd" >&2; exit 2 ;;
esac
diff --git a/test-rig/screenshots.sh b/test-rig/screenshots.sh
new file mode 100755
index 0000000..5da6349
--- /dev/null
+++ b/test-rig/screenshots.sh
@@ -0,0 +1,109 @@
+#!/bin/sh
+# Regenerate the F-Droid listing screenshots.
+#
+# Boots a pixel_6 AVD on the same AOSP API-35 image the e2e tier uses -
+# natively 1080x2400, the geometry the tracked PNGs already have - and
+# screencaps Main and Settings into
+# fastlane/metadata/android/en-US/images/phoneScreenshots/.
+#
+# The AVD is its own (scrcpy-shots, not scrcpy-test) so a screenshot run
+# never perturbs the device state ./test e2e asserts against. Panel size
+# comes from the device profile rather than `wm size`: overriding
+# geometry on a booted software-GPU emulator ANRs SystemUI, and the ANR
+# dialog lands in the capture.
+#
+# Runs inside the test image; orchestrated by ../test.
+
+set -eu
+
+ROOT=/work
+# Debug applicationId (note the .debug suffix). Activity classes live in
+# the invalid.lena.scrcpy namespace, which carries no suffix.
+PKG=invalid.lena.scrcpy.debug
+AVD=scrcpy-shots
+SERIAL=emulator-5554
+APK=$ROOT/app/build/outputs/apk/debug/app-debug.apk
+SHOTS=$ROOT/fastlane/metadata/android/en-US/images/phoneScreenshots
+TMPDIR=$ROOT/.tools/shots
+WIDTH=1080
+HEIGHT=2400
+mkdir -p "$TMPDIR" "$SHOTS"
+
+log() { printf 'screenshots: %s\n' "$*" >&2; }
+
+# ---- 0. server jar ----
+[ -f "$ROOT/app/src/main/assets/scrcpy-server.jar" ] || "$ROOT/scripts/update-server"
+
+# ---- 1. build APK ----
+log 'gradle :app:assembleDebug'
+"$ROOT/gradlew" --no-daemon -q :app:assembleDebug
+
+# ---- 2. AVD + boot ----
+if ! avdmanager list avd | grep -q "Name: $AVD$"; then
+ log "creating avd $AVD (pixel_6, ${WIDTH}x${HEIGHT})"
+ echo no | avdmanager create avd -n "$AVD" -k 'system-images;android-35;default;x86_64' -d pixel_6 >/dev/null
+fi
+
+# Clean stale lock files from a previously aborted run; snapshot.lock.lock
+# and read-snapshot.txt in particular make the next boot die with
+# "a snapshot operation is pending and timeout has expired".
+AVD_DIR="$ANDROID_AVD_HOME/$AVD.avd"
+rm -f "$AVD_DIR"/multiinstance.lock "$AVD_DIR"/hardware-qemu.ini.lock \
+ "$AVD_DIR"/snapshot.lock.lock "$AVD_DIR"/read-snapshot.txt 2>/dev/null || true
+rm -rf "$HOME/.android/avd/running" 2>/dev/null || true
+
+if ! pgrep -f "emulator.*-avd $AVD" >/dev/null; then
+ log 'booting emulator (no window, no snapshot)'
+ emulator -avd "$AVD" -no-window -no-audio -no-snapshot -gpu swiftshader_indirect \
+ -no-boot-anim -accel on >"$TMPDIR/emulator.log" 2>&1 &
+fi
+adb -s "$SERIAL" wait-for-device
+until [ "$(adb -s "$SERIAL" shell getprop sys.boot_completed | tr -d '\r')" = 1 ]; do
+ sleep 2
+done
+
+trap 'adb -s "$SERIAL" emu kill >/dev/null 2>&1 || true' EXIT INT TERM
+
+# ---- 3. install ----
+log "install $APK"
+adb -s "$SERIAL" install -r "$APK" >/dev/null
+# Screenshot 1 is the empty-list state; a re-run must not inherit the
+# devices.json a previous run left behind.
+adb -s "$SERIAL" shell pm clear "$PKG" >/dev/null
+
+# ---- 4. capture ----
+# One activity per shot: start it, let the window settle, screencap.
+# SettingsActivity is not exported, so this needs adbd running as root.
+log 'adb root'
+adb -s "$SERIAL" root
+adb -s "$SERIAL" wait-for-device
+
+shot() {
+ activity=$1
+ out=$2
+ log "capture $activity -> $out"
+ adb -s "$SERIAL" shell am force-stop "$PKG"
+ adb -s "$SERIAL" shell am start -W -n "$PKG/invalid.lena.scrcpy.$activity" >/dev/null
+ sleep 4
+ # Anything else on top - a system dialog, an ANR, a failed launch -
+ # means the shot is not the screen we asked for. Fail, do not ship it.
+ focus=$(adb -s "$SERIAL" shell dumpsys window \
+ | sed -n 's/.*mCurrentFocus=[^ ]* [^ ]* \([^ }]*\).*/\1/p' | head -1)
+ case "$focus" in
+ "$PKG/"*) ;;
+ *) log "focused window is '$focus', want $PKG/*"; exit 1 ;;
+ esac
+ adb -s "$SERIAL" exec-out screencap -p > "$SHOTS/$out"
+ [ -s "$SHOTS/$out" ] || { log "$out is empty"; exit 1; }
+ # Reject anything that is not the panel we expect: a wrong-sized PNG
+ # means the device profile changed and the shot is unusable.
+ got=$(ffprobe -v error -select_streams v:0 \
+ -show_entries stream=width,height -of csv=p=0:s=x "$SHOTS/$out")
+ [ "$got" = "${WIDTH}x${HEIGHT}" ] || { log "$out is $got, want ${WIDTH}x${HEIGHT}"; exit 1; }
+}
+
+shot Main 1.png
+shot SettingsActivity 2.png
+
+log "wrote $SHOTS/1.png $SHOTS/2.png"
+log 'done'