Skip to content

JNI calls hang or crash when made from a Swift Testing @Test body under --apk instrumented mode #723

Description

@koustavdg

Summary

skip android test --apk is the only mode with a real Android application Context
(per its own --help text: "required for tests that need an Android application
context"), but its SwiftTestRunner/SwiftTest harness only ever runs Swift
Testing, never XCTest, regardless of --testing-library. And any JNI call made
from inside a @Test body under that harness either hangs indefinitely or crashes,
depending on how it's structured — never succeeds.

Minimal repro

import Testing
import SkipAndroidBridge

@Test func contextLookup() throws {
    let context = jniContext { ProcessInfo.processInfo.dynamicAndroidContext() }
}

Run with:

skip android test --android-serial <emulator-or-device> --testing-library all --apk

against any Fuse (skip init --native-app) package with this test in its SPM test
target.

What happens

Attempt 1 — plain call, wrapped in jniContext { } as required.
Hangs indefinitely. Confirmed via adb shell ps that the process stays alive
(state S, sleeping) rather than crashing or completing — a genuine deadlock, not
an error. testStarted is logged; no testEnded, no crash, no further log output
observed for 20+ minutes before external kill.

Attempt 2 — bootstrap first.
Added try? AndroidBridgeBootstrap.initAndroidBridge(filesDir:cacheDir:) (with
placeholder paths — it only needs real ones for the FileManager/XDG env-var
setup) as the first statement in the test, before any context access, mirroring
skip-android-bridge's own AndroidBridgeTests.swift pattern.

Result: crashes immediately —
SwiftJNI/SwiftJNI.swift:166: Fatal error: SwiftJNI: you must perform JNI operations within a jniContext { ... } block — but this time the crash is inside
initAndroidBridge's own internals (AndroidBootstrap.setupCACerts() and/or
AndroidLooper.setupMainLooper()), which don't wrap their own JNI calls in
jniContext { }.

Attempt 3 — @MainActor, both structural forms.
Tried both a bare @Test @MainActor func and a @Suite @MainActor struct wrapping
the same call. Both crash identically, with this backtrace:

#00 _dispatch_main_queue_callback_4CF
#01 TestHarness.drainMainQueueCallback(...)
#02 (JNI trampoline back into drainMainQueueCallback)
#03 android::Looper::pollOnce
#04 android::android_os_MessageQueue_nativePollOnce
#05 android.os.MessageQueue.next
#06 android.os.Looper.loopOnce
#07 android.os.Looper.loop
#08 android.app.ActivityThread.main

SwiftTest's own harness (libtest_harness.so) already owns the main thread and
drains libdispatch's main queue itself via drainMainQueueCallback, invoked
reentrantly from inside Looper.pollOnce. Scheduling @MainActor-isolated test
code onto the main dispatch queue makes it run inside that same reentrant call,
colliding with the harness's own control flow. The @Suite-level variant crashes
even before testStarted is logged — during Swift Testing's own suite-dispatch
machinery, before the test body runs at all.

Attempt 4 — @Suite(.serialized).
Confirmed via adb shell ps: reproduces attempt 1's hang exactly (process alive,
state S, no crash, no completion). Avoids attempt 3's crash simply by staying on
the worker thread — which is exactly where attempt 1's wedge happens.

Expected

Some documented, working way to make a JNI/Context-touching call from a Swift
Testing @Test body under --apk mode — or confirmation this is a known
limitation and XCTest-under---apk (or some other path) is the intended way to get
Context-dependent Android tests running.

Environment

  • Skip 1.9.5
  • Swift 6.3.3 (Android SDK swift-6.3.3-RELEASE_android)
  • Android API 34, x86_64 emulator (Pixel_API_34)
  • macOS 26.5, Intel (x86_64) host
  • skip checkup clean

Why this matters

Any test needing a real Android Context — credential storage via
EncryptedSharedPreferences, TLS/TrustManager work, anything touching
android.content.Context-scoped APIs — currently has no working path to on-device
automated verification under Fuse. Happy to provide the full local package/test
setup if useful; kept this report to the minimal repro rather than an app-specific
one.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions