From f1f11268e2ed3e516a644894b7b0ac44c4c0db25 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Sun, 20 Sep 2026 00:49:57 +0200 Subject: [PATCH 1/2] ci(flutter): cold-boot the e2e emulator instead of restoring a snapshot Five of the last thirty e2e-android runs failed before the test script started, all with the same trace from the emulator-runner action: adb shell input keyevent 82 IllegalStateException: ServiceNotFoundException: No service published for: input The test step restored the cached AVD snapshot. A restored guest already has sys.boot_completed=1, so the action's boot wait returns as soon as adb reconnects, two seconds after the load, and its unconditional keyevent races the system services coming back up. The action has no option to skip or retry that command, and upstream main still issues it. Boot cold with -no-snapshot and drop the snapshot cache: the property then flips only after the services are published. Costs about a minute of boot time with KVM. --- .github/workflows/test_flutter.yaml | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test_flutter.yaml b/.github/workflows/test_flutter.yaml index 29c4d479..11b20a96 100644 --- a/.github/workflows/test_flutter.yaml +++ b/.github/workflows/test_flutter.yaml @@ -99,35 +99,20 @@ jobs: fi - name: Build images and start the mesh (no emulator needed) run: ./mobile/mobile_e2e.sh setup - - name: AVD cache - id: avd-cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-30-google_apis-x86_64 - - name: Create AVD snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 - with: - api-level: 30 - target: google_apis - arch: x86_64 - force-avd-creation: false - disable-animations: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - emulator-boot-timeout: 300 - script: echo "Generated AVD snapshot for caching." + # Cold boot on purpose (-no-snapshot), so no cached AVD snapshot. Restoring + # a snapshot resumes a guest whose sys.boot_completed is already 1, so the + # action's boot wait returns as soon as adb reconnects and its + # unconditional `input keyevent 82` races the system services coming back + # ("No service published for: input"). A cold boot sets the property only + # after the services are up. Costs about a minute of boot time with KVM. - name: Run E2E Integration Test in Android Emulator uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 with: api-level: 30 target: google_apis arch: x86_64 - force-avd-creation: false disable-animations: true - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + emulator-options: -no-snapshot -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none emulator-boot-timeout: 300 script: ./mobile/mobile_e2e.sh test From 0efdcf872ea56b67295de9ded3a03ed3298d2acd Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Sun, 20 Sep 2026 00:55:14 +0200 Subject: [PATCH 2/2] ci(flutter): keep the snapshot, boot it ourselves The cold boot in the previous commit fixes the race but gives back the five minutes the cached snapshot saves. Keep the snapshot and stop using the emulator-runner action for the test step: the action only creates the AVD on a cache miss. A run step restores the snapshot and treats a successful `input keyevent 82` as the readiness signal, which is exactly the command that failed, instead of trusting a sys.boot_completed that a restored guest reports before its services are back. --- .github/workflows/test_flutter.yaml | 58 ++++++++++++++++++++++++----- mobile/mobile_e2e.sh | 4 +- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test_flutter.yaml b/.github/workflows/test_flutter.yaml index 11b20a96..b9f830ae 100644 --- a/.github/workflows/test_flutter.yaml +++ b/.github/workflows/test_flutter.yaml @@ -99,20 +99,58 @@ jobs: fi - name: Build images and start the mesh (no emulator needed) run: ./mobile/mobile_e2e.sh setup - # Cold boot on purpose (-no-snapshot), so no cached AVD snapshot. Restoring - # a snapshot resumes a guest whose sys.boot_completed is already 1, so the - # action's boot wait returns as soon as adb reconnects and its - # unconditional `input keyevent 82` races the system services coming back - # ("No service published for: input"). A cold boot sets the property only - # after the services are up. Costs about a minute of boot time with KVM. - - name: Run E2E Integration Test in Android Emulator + - name: AVD cache + id: avd-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-30-google_apis-x86_64 + - name: Create AVD snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 with: api-level: 30 target: google_apis arch: x86_64 - disable-animations: true - emulator-options: -no-snapshot -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + avd-name: test + force-avd-creation: false + disable-animations: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none emulator-boot-timeout: 300 - script: ./mobile/mobile_e2e.sh test + script: echo "Generated AVD snapshot for caching." + # The action above only creates the AVD. Restoring its snapshot resumes a + # guest whose sys.boot_completed is already 1, so the action's boot wait + # returns as soon as adb reconnects and its unconditional `input keyevent + # 82` races the system services coming back ("No service published for: + # input"), failing one run in six before the test starts. Booting here lets + # the readiness check wait for the thing that actually failed. + - name: Boot the emulator from the cached snapshot + run: | + set -o errexit -o nounset -o pipefail + export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH" + echo "$ANDROID_HOME/platform-tools" >> "$GITHUB_PATH" + echo "ANDROID_AVD_HOME=$HOME/.android/avd" >> "$GITHUB_ENV" + export ANDROID_AVD_HOME="$HOME/.android/avd" + yes | sdkmanager --licenses >/dev/null + sdkmanager --install platform-tools emulator 'system-images;android-30;google_apis;x86_64' >/dev/null + nohup emulator -avd test -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none >emulator.log 2>&1 & + timeout 300 adb wait-for-device + ready=0 + for _ in $(seq 1 150); do + if [[ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" == "1" ]] && adb shell input keyevent 82 2>/dev/null; then + ready=1; break + fi + sleep 2 + done + if [[ "$ready" != 1 ]]; then echo "emulator did not become ready"; tail -n 50 emulator.log; exit 1; fi + adb shell settings put global window_animation_scale 0.0 + adb shell settings put global transition_animation_scale 0.0 + adb shell settings put global animator_duration_scale 0.0 + - name: Run E2E Integration Test in Android Emulator + run: ./mobile/mobile_e2e.sh test + - name: Stop the emulator + if: always() + run: adb emu kill || true diff --git a/mobile/mobile_e2e.sh b/mobile/mobile_e2e.sh index 6d4f7b30..2963f39f 100755 --- a/mobile/mobile_e2e.sh +++ b/mobile/mobile_e2e.sh @@ -270,8 +270,8 @@ adb reverse tcp:37001 tcp:37001 adb reverse tcp:37002 tcp:37002 # 7. Run the Flutter integration test against the running emulator. -# On CI this phase runs inside ReactiveCircus/android-emulator-runner, -# so the emulator is already started and adb is fully connected to it. +# On CI the workflow boots the emulator and waits for it to answer adb before +# this phase runs. cd mobile/sam-node-app # Run Flutter integration test