From 60df86cbb7c199bd0682bc8823a8f82179c8b8df Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Wed, 7 Oct 2026 13:51:48 +0300 Subject: [PATCH 1/3] Cold boot API 26 to avoid restored snapshot startup failures --- .github/workflows/android-device.yml | 8 +++++--- AGENTS.md | 3 ++- docs/en/fastlane.md | 5 +++-- docs/ru/fastlane.md | 5 +++-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/android-device.yml b/.github/workflows/android-device.yml index 31a375d..cc6f74e 100644 --- a/.github/workflows/android-device.yml +++ b/.github/workflows/android-device.yml @@ -58,6 +58,7 @@ jobs: snapshot_hash=$(cat "$ANDROID_HOME/emulator/source.properties" "$ANDROID_HOME/system-images/android-$DEVICE_API/google_apis/x86_64/source.properties" | sha256sum | cut -d ' ' -f 1) echo "snapshot-hash=$snapshot_hash" >> "$GITHUB_OUTPUT" - name: Restore clean emulator snapshot + if: inputs.api != 26 id: avd uses: actions/cache/restore@v4 with: @@ -66,7 +67,7 @@ jobs: ~/.android/avd/megaproxy-device.avd/ key: android-device-v1-${{ runner.os }}-api${{ inputs.api }}-google-apis-x86_64-${{ steps.sdk.outputs.snapshot-hash }}-${{ hashFiles('.github/workflows/android-device.yml') }} - name: Generate clean snapshot on cache miss - if: steps.avd.outputs.cache-hit != 'true' + if: inputs.api != 26 && steps.avd.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2.38.0 with: api-level: ${{ inputs.api }} @@ -79,7 +80,7 @@ jobs: disable-animations: false script: adb shell input keyevent 82 - name: Save clean snapshot before installing MegaProxy - if: steps.avd.outputs.cache-hit != 'true' + if: inputs.api != 26 && steps.avd.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: | @@ -99,7 +100,8 @@ jobs: avd-name: megaproxy-device profile: pixel_5 force-avd-creation: false - emulator-options: -accel on -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none + # API 26 restored snapshots can kill the action's pre-test input command (137). + emulator-options: -accel on ${{ inputs.api == 26 && '-no-snapshot' || '-no-snapshot-save' }} -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none disable-animations: true script: | mkdir -p test-results/android-api${{ inputs.api }} diff --git a/AGENTS.md b/AGENTS.md index def5d56..80df6ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,8 @@ branch names, credentials, signing material, or other secrets. - Pull requests must run native tests, Android JVM unit/lint/build checks and independent Android emulator API 26/API 35 integration scenarios. Use Ubuntu 24.04 with explicit KVM - permissions, acceleration checks and clean snapshot caches, following BrowserMegaProxy. + permissions and acceleration checks, following BrowserMegaProxy. API 26 cold boots to avoid + pre-test input failures after snapshot restore; API 35 uses clean snapshot caches. - Compare each suite against its last successful ancestor check in the same PR and base. Failed/skipped/cancelled jobs do not advance coverage. Fall back to the full PR diff when history is unavailable; unknown paths and shared build/CI inputs enable all suites. Require `Change scope`, `Python tests and style` and both emulator scenarios diff --git a/docs/en/fastlane.md b/docs/en/fastlane.md index 8fc9ad8..04d5d54 100644 --- a/docs/en/fastlane.md +++ b/docs/en/fastlane.md @@ -249,8 +249,9 @@ Release branches `release/vX.Y.Z` always run all CI suites; skipped jobs cannot ## Android emulator integration tests CI runs API 26 and API 35 as independent jobs with independent successful-history baselines. -Both use Ubuntu 24.04, explicit KVM permissions, mandatory hardware acceleration and clean AVD -snapshots keyed by API, emulator/system-image versions and workflow content. No software fallback +Both use Ubuntu 24.04, explicit KVM permissions and mandatory hardware acceleration. API 26 +cold boots because restored snapshots can kill the pre-test input command (exit 137). API 35 uses +clean AVD snapshots keyed by API, emulator/system-image versions and workflow content. No software fallback or automatic test retry is used. Emulator startup gets one retry only if the test runner has not started; a runner-started marker prevents retrying any test/setup failure inside the runner. SDK installation also gets one retry for transient download failures. diff --git a/docs/ru/fastlane.md b/docs/ru/fastlane.md index a963653..6959330 100644 --- a/docs/ru/fastlane.md +++ b/docs/ru/fastlane.md @@ -253,8 +253,9 @@ Release-ветки `release/vX.Y.Z` всегда запускают все на ## Интеграционные тесты на Android-эмуляторах CI запускает API 26 и API 35 отдельными заданиями с независимой историей успешных проверок. -Оба используют Ubuntu 24.04, явные разрешения KVM, обязательное аппаратное ускорение и чистые -снимки AVD. Ключ кеша включает API, версии эмулятора/system image и содержимое workflow. +Оба используют Ubuntu 24.04, явные разрешения KVM и обязательное аппаратное ускорение. API 26 +загружается с нуля: восстановление снимка может завершить команду ввода перед тестами с кодом 137. +API 35 использует чистые снимки AVD. Ключ кеша включает API, версии эмулятора/system image и содержимое workflow. Программного fallback и автоматических повторов тестов нет. Запуск эмулятора повторяется один раз, только если тестовый runner ещё не стартовал. Маркер runner-started запрещает повторять ошибки тестов и подготовки внутри runner. Две ошибки запуска оставляют required check From 866805f297f57dbc537b32c226e153098170d1f0 Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Wed, 7 Oct 2026 14:03:48 +0300 Subject: [PATCH 2/3] Preserve device diagnostics and failed JUnit results on adb errors --- scripts/android_device_tests.py | 45 +++++++++++++++------- scripts/tests/test_android_device_tests.py | 30 +++++++++++++++ 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/scripts/android_device_tests.py b/scripts/android_device_tests.py index 52fb394..4939bd0 100644 --- a/scripts/android_device_tests.py +++ b/scripts/android_device_tests.py @@ -48,6 +48,33 @@ def require_success(output): raise RuntimeError("Instrumentation did not report exactly one successful test") +def finish_case(shell, results, label, case, suite, log_start): + error_message = None + for name, words in ( + ("logcat", ("logcat", "-d", "-v", "threadtime", "-T", log_start)), + ("force-stop", ("am", "force-stop", PACKAGE)), + ): + try: + output = shell(*words) + except subprocess.SubprocessError as error: + output = getattr(error, "output", None) or "" + if isinstance(output, bytes): + output = output.decode(errors="replace") + message = f"{name} failed ({type(error).__name__}, exit {getattr(error, 'returncode', 'unknown')})" + ET.SubElement(case, "failure", message=message) + error_message = error_message or message + if name == "logcat" or error_message: + (results / (label + f"-{name}.txt")).write_text(output) + suite.set("tests", str(len(suite))) + suite.set("failures", str(sum(c.find("failure") is not None for c in suite))) + suite.set("time", f"{sum(float(c.get('time', '0')) for c in suite):.3f}") + ET.ElementTree(suite).write( + results / "junit.xml", encoding="utf-8", xml_declaration=True + ) + if error_message: + raise RuntimeError(error_message + "; see collected evidence") + + @contextlib.contextmanager def fixture(): name = "megaproxy-device-" + uuid.uuid4().hex[:12] @@ -193,7 +220,9 @@ def handle(self): release.set() server.shutdown() worker.join() - shell("rm", "-f", accepted_path, release_path) + # The disposable emulator is discarded on failure; preserve the cause. + if sys.exc_info()[0] is None: + shell("rm", "-f", accepted_path, release_path) def main(): @@ -324,19 +353,7 @@ def shell(*words): print(f"Failure {extension} evidence unavailable", flush=True) finally: case.set("time", f"{time.monotonic() - started:.3f}") - (results / (label + "-logcat.txt")).write_text( - shell("logcat", "-d", "-v", "threadtime", "-T", log_start) - ) - shell("am", "force-stop", PACKAGE) - suite.set("tests", str(len(suite))) - suite.set("failures", str(failures)) - suite.set( - "time", - f"{sum(float(item.get('time', '0')) for item in suite):.3f}", - ) - ET.ElementTree(suite).write( - results / "junit.xml", encoding="utf-8", xml_declaration=True - ) + finish_case(shell, results, label, case, suite, log_start) return bool(failures) diff --git a/scripts/tests/test_android_device_tests.py b/scripts/tests/test_android_device_tests.py index b3b8dcc..62553ac 100644 --- a/scripts/tests/test_android_device_tests.py +++ b/scripts/tests/test_android_device_tests.py @@ -1,12 +1,42 @@ +import subprocess import sys +import tempfile import unittest +import xml.etree.ElementTree as ET from pathlib import Path +from unittest.mock import Mock sys.path.insert(0, str(Path(__file__).parents[1])) import android_device_tests as m class InstrumentationResultTest(unittest.TestCase): + def test_logcat_failure_preserves_evidence_and_failed_report(self): + suite = ET.Element("testsuite") + case = ET.SubElement(suite, "testcase", time="1.0") + shell = Mock( + side_effect=[ + subprocess.CalledProcessError( + 255, "logcat", output="read: unexpected EOF!" + ), + subprocess.CalledProcessError(1, "force-stop", output="device offline"), + ] + ) + with tempfile.TemporaryDirectory() as directory: + results = Path(directory) + with self.assertRaisesRegex(RuntimeError, "logcat failed"): + m.finish_case(shell, results, "test", case, suite, "10-07 10:58:32.000") + report = ET.parse(results / "junit.xml").getroot() + self.assertEqual(report.get("tests"), "1") + self.assertEqual(report.get("failures"), "1") + self.assertEqual(len(report[0].findall("failure")), 2) + self.assertEqual( + (results / "test-logcat.txt").read_text(), "read: unexpected EOF!" + ) + self.assertEqual( + (results / "test-force-stop.txt").read_text(), "device offline" + ) + def test_adb_success_is_not_enough(self): for output in ( "INSTRUMENTATION_CODE: 0", From 3d0392b32e6ee319f969c0b1846fad5c4fbe8c62 Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Wed, 7 Oct 2026 14:11:49 +0300 Subject: [PATCH 3/3] Cold boot both emulator APIs instead of restoring unstable snapshots --- .github/workflows/android-device.yml | 38 ++-------------------------- AGENTS.md | 4 +-- docs/en/fastlane.md | 6 ++--- docs/ru/fastlane.md | 6 ++--- 4 files changed, 10 insertions(+), 44 deletions(-) diff --git a/.github/workflows/android-device.yml b/.github/workflows/android-device.yml index cc6f74e..697f583 100644 --- a/.github/workflows/android-device.yml +++ b/.github/workflows/android-device.yml @@ -43,8 +43,7 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Install SDK and fingerprint snapshot dependencies - id: sdk + - name: Install SDK and check acceleration env: DEVICE_API: ${{ inputs.api }} run: | @@ -55,38 +54,6 @@ jobs: fi echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/29.0.14206865" >> "$GITHUB_ENV" "$ANDROID_HOME/emulator/emulator" -accel-check - snapshot_hash=$(cat "$ANDROID_HOME/emulator/source.properties" "$ANDROID_HOME/system-images/android-$DEVICE_API/google_apis/x86_64/source.properties" | sha256sum | cut -d ' ' -f 1) - echo "snapshot-hash=$snapshot_hash" >> "$GITHUB_OUTPUT" - - name: Restore clean emulator snapshot - if: inputs.api != 26 - id: avd - uses: actions/cache/restore@v4 - with: - path: | - ~/.android/avd/megaproxy-device.ini - ~/.android/avd/megaproxy-device.avd/ - key: android-device-v1-${{ runner.os }}-api${{ inputs.api }}-google-apis-x86_64-${{ steps.sdk.outputs.snapshot-hash }}-${{ hashFiles('.github/workflows/android-device.yml') }} - - name: Generate clean snapshot on cache miss - if: inputs.api != 26 && steps.avd.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2.38.0 - with: - api-level: ${{ inputs.api }} - target: google_apis - arch: x86_64 - avd-name: megaproxy-device - profile: pixel_5 - force-avd-creation: false - emulator-options: -accel on -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none - disable-animations: false - script: adb shell input keyevent 82 - - name: Save clean snapshot before installing MegaProxy - if: inputs.api != 26 && steps.avd.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: | - ~/.android/avd/megaproxy-device.ini - ~/.android/avd/megaproxy-device.avd/ - key: ${{ steps.avd.outputs.cache-primary-key }} - name: Build debug and instrumentation APKs run: bundle exec fastlane android device_test_build - name: Run real Android tests @@ -100,8 +67,7 @@ jobs: avd-name: megaproxy-device profile: pixel_5 force-avd-creation: false - # API 26 restored snapshots can kill the action's pre-test input command (137). - emulator-options: -accel on ${{ inputs.api == 26 && '-no-snapshot' || '-no-snapshot-save' }} -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none + emulator-options: -accel on -no-snapshot -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none disable-animations: true script: | mkdir -p test-results/android-api${{ inputs.api }} diff --git a/AGENTS.md b/AGENTS.md index 80df6ef..d66b0cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,8 +23,8 @@ branch names, credentials, signing material, or other secrets. - Pull requests must run native tests, Android JVM unit/lint/build checks and independent Android emulator API 26/API 35 integration scenarios. Use Ubuntu 24.04 with explicit KVM - permissions and acceleration checks, following BrowserMegaProxy. API 26 cold boots to avoid - pre-test input failures after snapshot restore; API 35 uses clean snapshot caches. + permissions and acceleration checks, following BrowserMegaProxy. Both APIs cold boot without + snapshot caches to avoid restored-state startup failures and ADB disconnects. - Compare each suite against its last successful ancestor check in the same PR and base. Failed/skipped/cancelled jobs do not advance coverage. Fall back to the full PR diff when history is unavailable; unknown paths and shared build/CI inputs enable all suites. Require `Change scope`, `Python tests and style` and both emulator scenarios diff --git a/docs/en/fastlane.md b/docs/en/fastlane.md index 04d5d54..1a37531 100644 --- a/docs/en/fastlane.md +++ b/docs/en/fastlane.md @@ -249,9 +249,9 @@ Release branches `release/vX.Y.Z` always run all CI suites; skipped jobs cannot ## Android emulator integration tests CI runs API 26 and API 35 as independent jobs with independent successful-history baselines. -Both use Ubuntu 24.04, explicit KVM permissions and mandatory hardware acceleration. API 26 -cold boots because restored snapshots can kill the pre-test input command (exit 137). API 35 uses -clean AVD snapshots keyed by API, emulator/system-image versions and workflow content. No software fallback +Both use Ubuntu 24.04, explicit KVM permissions and mandatory hardware acceleration. Both APIs +cold boot without snapshot caches to avoid the failures observed after snapshot restore: +pre-test input failures on API 26 and ADB disconnects during diagnostics on API 35. No software fallback or automatic test retry is used. Emulator startup gets one retry only if the test runner has not started; a runner-started marker prevents retrying any test/setup failure inside the runner. SDK installation also gets one retry for transient download failures. diff --git a/docs/ru/fastlane.md b/docs/ru/fastlane.md index 6959330..2436d39 100644 --- a/docs/ru/fastlane.md +++ b/docs/ru/fastlane.md @@ -253,9 +253,9 @@ Release-ветки `release/vX.Y.Z` всегда запускают все на ## Интеграционные тесты на Android-эмуляторах CI запускает API 26 и API 35 отдельными заданиями с независимой историей успешных проверок. -Оба используют Ubuntu 24.04, явные разрешения KVM и обязательное аппаратное ускорение. API 26 -загружается с нуля: восстановление снимка может завершить команду ввода перед тестами с кодом 137. -API 35 использует чистые снимки AVD. Ключ кеша включает API, версии эмулятора/system image и содержимое workflow. +Оба используют Ubuntu 24.04, явные разрешения KVM и обязательное аппаратное ускорение. Оба API +загружаются с нуля без кеша снимков, чтобы избежать сбоев, наблюдавшихся после восстановления: +ошибки команды ввода перед тестами на API 26 и обрывов ADB при сборе диагностики на API 35. Программного fallback и автоматических повторов тестов нет. Запуск эмулятора повторяется один раз, только если тестовый runner ещё не стартовал. Маркер runner-started запрещает повторять ошибки тестов и подготовки внутри runner. Две ошибки запуска оставляют required check