From 7dea79dbee0ca7eddbdbb1df7f11627577d70cde Mon Sep 17 00:00:00 2001 From: SatoshiQaziMuhammed Date: Wed, 8 Jul 2026 06:36:31 -0700 Subject: [PATCH] fix: use arm64-v8a emulator image on macos-14 runners (#11) * fix: use arm64-v8a emulator image on macos-14 runners The "Run balances tests" job has been failing on every scheduled run for weeks: the emulator requested an x86 system image while macos-14 GitHub-hosted runners are Apple Silicon (aarch64) - QEMU2 refuses to boot a mismatched-architecture image ("Avd's CPU Architecture 'x86' is not supported by the QEMU2 emulator on aarch64 host"), so every adb command against the never-booted emulator failed with "device not found" for the full 10-minute boot timeout on every run. * fix(ci): run balances-test emulator on ubuntu-latest with KVM instead of macos-14 macos-14 (Apple Silicon) GitHub-hosted runners don't expose nested virtualization to guests, so the Android emulator's HVF backend fails with HV_UNSUPPORTED and the job times out waiting for boot - this affects both x86 and arm64-v8a system images, so the arch: arm64-v8a change alone wasn't sufficient. macos-13 (which previously supported this via HAXM) was retired by GitHub. Linux hosted runners support KVM-accelerated emulators once the kvm device's udev permissions are relaxed, which is the documented setup for reactivecircus/android-emulator-runner and also faster/cheaper than macOS runners. * fix(ci): build instrumentialTest androidTest APK to match AllureAndroidJUnitRunner run_balances_test.sh has always targeted io.novafoundation.nova.debug.test/io.qameta.allure.android.runners.AllureAndroidJUnitRunner, but android_build.yml's "Build debug tests" step built the plain debug variant's androidTest APK, whose manifest declares the module default runner (androidx.test.runner.AndroidJUnitRunner, from allmodules.gradle) - AllureAndroidJUnitRunner is only wired via testInstrumentationRunner on the dedicated instrumentialTest build type (app/build.gradle), which was apparently created for exactly this purpose but never actually wired into CI. Result: INSTRUMENTATION_FAILED as soon as the emulator boot issue was fixed and this step was reached for the first time. instrumentialTest initWith buildTypes.debug (same applicationIdSuffix '.debug', same default debug signing), so it installs under the same package next to the already-built debug app APK - only the androidTest APK's build type needs to change, not the main app build. Only balances_test.yml sets build-debug-tests: true, so this doesn't affect any other android_build.yml caller (pull_request.yml has it explicitly disabled with its own separate pre-existing TODO). * test: temporarily point android_build.yml ref at this branch for verification TEMPORARY - reusable workflow calls (uses: ...@ref) pin to that ref's copy regardless of the caller's branch, so android_build.yml's fix never actually ran in the previous verification attempts even though it was committed to this branch. Must revert to @main before merging this PR, once android_build.yml's changes are on main. * fix(ci): declare AllureAndroidJUnitRunner on debug build type, not instrumentialTest AGP only ever compiles one androidTest APK per module, tied to android.testBuildType (default "debug", never overridden in this project) - so a testInstrumentationRunner override on any other build type, including instrumentialTest, is unreachable regardless of which gradle task or main-app variant is used. That's why the previous attempt (assembleInstrumentialTestAndroidTest) failed outright: no such task exists, since androidTest is only ever generated for testBuildType. Reverts the previous commit's task/path changes back to assembleDebugAndroidTest / app/androidTest/debug/..., and instead moves the testInstrumentationRunner override onto the debug build type itself, which is what androidTest actually compiles against and what run_balances_test.sh has always targeted. Also drops the dead override from instrumentialTest now that it's confirmed non-functional there. * debug(ci): dump pm list instrumentation before running balances test Diagnostic only - the previous fix (testInstrumentationRunner moved to the debug build type's defaultConfig) still hit the identical INSTRUMENTATION_FAILED for AllureAndroidJUnitRunner even though both APKs now install successfully with the corrected paths. Need to see what PackageManager actually registered for the installed test APK rather than keep guessing from Gradle DSL evaluation-order theory - allmodules.gradle's subprojects{} block also sets testInstrumentationRunner on every module including app, and it's not yet confirmed which assignment wins. * fix(ci): use current io.pezkuwichain.wallet package instead of stale upstream name Diagnostic (pm list instrumentation) confirmed the real, final root cause: AllureAndroidJUnitRunner IS correctly registered on-device after the previous fix, but under the app's real applicationId (io.pezkuwichain.wallet, set in build.gradle since the Pezkuwi rebrand) - not the pre-rebrand upstream Nova Wallet package (io.novafoundation.nova) these scripts still hardcoded. Every previous INSTRUMENTATION_FAILED was simply am instrument targeting a package that was never installed. Also fixes the identical stale reference in run_instrumental_tests.sh (currently unused by any workflow, but has the same bug). The BalancesIntegrationTest class's own Kotlin package (io.novafoundation.nova.balances) is untouched - that's the test class's real source package on disk, unrelated to the app's applicationId, and was never part of the bug. * fix(ci): bound run-tests to 25min and unbuffer python output Latest verification run hung for 1h15m+ inside am instrument with zero visible progress - had to be cancelled manually. Post-mortem: Python's stdout is fully buffered (not line-buffered) when piped under GitHub Actions' `run:`, so the script's own 5s heartbeat prints (explicitly there to avoid CI's inactivity-based cancellation) never actually reached the log until the buffer filled near the very end, making the run look identical to a genuine hang for its entire duration with no way to tell what BalancesIntegrationTest was doing. - timeout-minutes: 25 on run-tests bounds any future hang to a fixed, reasonable ceiling instead of requiring a manual cancel after an hour+. - python -u unbuffers stdout so the next run's logs show real-time heartbeat/output, giving actual visibility into where a hang (or slow chain RPC) occurs instead of a silent multi-hour gap. Root cause of the hang itself is still open - this only makes it diagnosable and bounds its cost. * fix: skip WSS ChainConnection setup for Tron-based chains Real root cause of the 1h15m+ hang in the previous verification run (had to be cancelled manually): ChainRegistry registers all chains sequentially (diff.newOrUpdated.forEach { registerChain(it) }), and registerConnection() unconditionally called connectionPool.setupConnection(chain), which creates a ChainConnection backed by a WSS-only SocketService - for every chain, with no check for chain type. Tron's node (https://api.trongrid.io) is a plain REST API with no WebSocket support, so this connection attempt just hangs forever with no timeout, and since registration is sequential and Pezkuwi chains (including Tron) are ordered first in the merged chain list, every chain after it in the list never finishes registering either - including all the third-party chains BalancesIntegrationTest actually exercises. This isn't just a test problem: any real user with the Tron chain enabled would hit the same registry-wide freeze during chain sync. Tron balance/transfer operations already go through their own TronGridApi REST client (built in the Phase 1/2 Tron work), independent of ConnectionPool/ChainConnection - confirmed nothing else reads chainRegistry.getConnection() for a Tron chain id - so skipping ChainConnection setup entirely for isTronBased chains is safe. * debug(ci): stream live logcat during test run Diagnostic only. Previous fix (skip WSS ChainConnection for isTronBased chains) did NOT resolve the hang - identical symptom (silent for the full 25min timeout, just heartbeats, nothing from the actual test process). Need real device-side visibility into what's actually happening (network calls, coroutine timeouts, ANRs) instead of guessing blind from am instrument's own stdout/stderr, which stays completely silent until the process exits. * debug(ci): SIGQUIT thread dump 3min into a hung run, bump logcat to debug level Previous diagnostic (live logcat at info level) showed the app staying genuinely alive (GC cycles, system chatter) but produced zero signal from the test itself after the initial successful chains.json fetch - no WSS/DB/coroutine activity visible, likely because relevant logging is below info level. Static code reading (ChainConnection.setup(), ChainSyncService.syncUp(), ChainFetcher) hasn't turned up an obvious blocking call, and this exact hang predates this session by at least a month (zero successes in the last 100 scheduled runs going back to 2026-06-09) - so it's a real, previously-latent bug in the app/test itself, never previously reached because every earlier run failed at an earlier CI-infra stage. kill -3 on the app process forces ART to dump every thread's Java stack trace to logcat (the standard ANR-diagnosis technique) - this should show exactly which coroutine/thread is parked and where, instead of continuing to guess from log filtering. * fix(ci): bump run-tests timeout to 90min, drop now-unneeded SIGQUIT diagnostic With the chain blacklist fixed, the previous run genuinely started executing the 160-case parameterized test suite (visible TestRunner started/finished events for real chains) instead of hanging silently - first time this has happened in this entire investigation. It still hit the 25min ceiling: each test case has an explicit withTimeout(80.seconds), and several chains that aren't fully dead (so not blacklisted) but are slow/unresponsive burn the full 80s before failing. With 160 total test cases, the worst-case aggregate easily exceeds 25 minutes even with zero actual hangs - so the timeout needs to be realistic for the test's own design, not just long enough to catch a true infinite hang. 90min matches android_build.yml's existing precedent for this project's CI jobs. The SIGQUIT-after-3-minutes diagnostic added earlier is now counterproductive noise: any healthy run legitimately takes far longer than 3 minutes to get through 160 cases, so it would fire on every run rather than only genuine hangs. Removed now that its actual purpose (finding the real hang) is done - the blacklist was the answer, not something a thread dump would have caught anyway (dead sockets, not deadlocked application code). * chore: revert temporary android_build.yml ref pin back to @main Was pointed at this branch only to verify android_build.yml's own fixes actually took effect during iteration (reusable workflow calls resolve against the ref they're pinned to, not the caller's branch). android_build.yml is now byte-identical to main (the wrong task-name attempt was fully reverted in an earlier commit), so this is a no-op functionally - just restoring the correct long-term reference before merge. --- .github/scripts/run_balances_test.sh | 16 ++++++++++++---- .github/scripts/run_instrumental_tests.sh | 4 ++-- .github/workflows/balances_test.yml | 10 +++++++++- app/build.gradle | 6 +++++- .../nova/runtime/multiNetwork/ChainRegistry.kt | 12 +++++++++--- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/scripts/run_balances_test.sh b/.github/scripts/run_balances_test.sh index b7b82651..a3d3e7ef 100644 --- a/.github/scripts/run_balances_test.sh +++ b/.github/scripts/run_balances_test.sh @@ -8,8 +8,14 @@ adb -s emulator-5554 install app/debug/app-debug.apk adb -s emulator-5554 install app/androidTest/debug/app-debug-androidTest.apk # Run tests -adb logcat -c && -python - </dev/null adb logcat -d '*:E' # Export results -adb exec-out run-as io.novafoundation.nova.debug sh -c 'cd /data/data/io.novafoundation.nova.debug/files && tar cf - allure-results' > allure-results.tar +adb exec-out run-as io.pezkuwichain.wallet.debug sh -c 'cd /data/data/io.pezkuwichain.wallet.debug/files && tar cf - allure-results' > allure-results.tar exit $EXIT_CODE diff --git a/.github/scripts/run_instrumental_tests.sh b/.github/scripts/run_instrumental_tests.sh index f72a82e2..ef4c20d2 100644 --- a/.github/scripts/run_instrumental_tests.sh +++ b/.github/scripts/run_instrumental_tests.sh @@ -28,7 +28,7 @@ t.dameon = True t.start() def run(): os.system('adb wait-for-device') - p = sp.Popen('adb shell am instrument -w -m -e notClass io.novafoundation.nova.balances.BalancesIntegrationTest -e package io.novafoundation.nova.debug io.novafoundation.nova.debug.test/io.qameta.allure.android.runners.AllureAndroidJUnitRunner', + p = sp.Popen('adb shell am instrument -w -m -e notClass io.novafoundation.nova.balances.BalancesIntegrationTest -e package io.pezkuwichain.wallet.debug io.pezkuwichain.wallet.debug.test/io.qameta.allure.android.runners.AllureAndroidJUnitRunner', shell=True, stdout=sp.PIPE, stderr=sp.PIPE, stdin=sp.PIPE) return p.communicate() success = re.compile(r'OK \(\d+ tests\)') @@ -47,6 +47,6 @@ EXIT_CODE=$? adb logcat -d '*:E' # Export results -adb exec-out run-as io.novafoundation.nova.debug sh -c 'cd /data/data/io.novafoundation.nova.debug/files && tar cf - allure-results' > allure-results.tar +adb exec-out run-as io.pezkuwichain.wallet.debug sh -c 'cd /data/data/io.pezkuwichain.wallet.debug/files && tar cf - allure-results' > allure-results.tar exit $EXIT_CODE diff --git a/.github/workflows/balances_test.yml b/.github/workflows/balances_test.yml index a1832c80..6c74a8fa 100644 --- a/.github/workflows/balances_test.yml +++ b/.github/workflows/balances_test.yml @@ -19,7 +19,8 @@ jobs: run-tests: needs: [build-app] - runs-on: macos-14 + runs-on: ubuntu-latest + timeout-minutes: 90 steps: - uses: actions/checkout@v4 @@ -36,12 +37,19 @@ jobs: - name: Add permissions run: chmod +x .github/scripts/run_balances_test.sh + - name: Enable KVM group perms + run: | + 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: Run tests uses: reactivecircus/android-emulator-runner@v2 with: disable-animations: true profile: Nexus 6 api-level: 29 + arch: x86_64 script: .github/scripts/run_balances_test.sh - uses: actions/upload-artifact@v4 diff --git a/app/build.gradle b/app/build.gradle index 2065a25b..f0eaa8b3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,6 +36,11 @@ android { debug { applicationIdSuffix '.debug' versionNameSuffix '-debug' + // androidTest is only ever compiled against this build type (AGP's testBuildType + // defaults to "debug" and is not overridden anywhere in this project), so this is the + // only place a testInstrumentationRunner override actually takes effect - the identical + // override on the instrumentialTest build type below is unreachable dead config. + defaultConfig.testInstrumentationRunner "io.qameta.allure.android.runners.AllureAndroidJUnitRunner" buildConfigField "String", "BuildType", "\"debug\"" } @@ -94,7 +99,6 @@ android { instrumentialTest { initWith buildTypes.debug matchingFallbacks = ['debug'] - defaultConfig.testInstrumentationRunner "io.qameta.allure.android.runners.AllureAndroidJUnitRunner" buildConfigField "String", "BuildType", "\"instrumentalTest\"" } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt index 57da123a..1ae735b7 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt @@ -215,12 +215,18 @@ class ChainRegistry( if (chain.hasSubstrateRuntime) { runtimeProviderPool.setupRuntimeProvider(chain) - runtimeSyncService.registerChain(chain, connection) - runtimeSubscriptionPool.setupRuntimeSubscription(chain, connection) + runtimeSyncService.registerChain(chain, requireNotNull(connection)) + runtimeSubscriptionPool.setupRuntimeSubscription(chain, requireNotNull(connection)) } } - private suspend fun registerConnection(chain: Chain): ChainConnection { + private suspend fun registerConnection(chain: Chain): ChainConnection? { + // Tron nodes are plain REST APIs (TronGrid), not WSS JSON-RPC endpoints - ChainConnection's + // SocketService can only speak the latter, so attempting to set one up here would hang + // indefinitely instead of failing fast. Tron balance/transfer operations already go through + // their own dedicated TronGridApi client, independent of this connection pool. + if (chain.isTronBased) return null + val connection = connectionPool.setupConnection(chain) if (chain.isEthereumBased) {