skiptools / skiptools/skip

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

Open
#723 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Swift
Stars
3.2k
Forks
106
Avg merge
6d 13h
Merged PRs (30d)
1

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with SwiftTestRunner and SwiftTest handling of --apk and --testing-library, then inspect SwiftJNI's jniContext requirement and AndroidBridgeBootstrap's setupCACerts and setupMainLooper paths. Reproduce the minimal contextLookup @Test with --testing-library all --apk and verify whether it completes without hanging or crashing; done is a documented working path or confirmed limitation.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, swift
Domain
mobile-dev, testing-qa
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.