Skip to content

Add --apk flag to android test command to package the tests into an APK for full Android API access#199

Merged
marcprux merged 12 commits intomainfrom
test-apk
Mar 1, 2026
Merged

Add --apk flag to android test command to package the tests into an APK for full Android API access#199
marcprux merged 12 commits intomainfrom
test-apk

Conversation

@marcprux
Copy link
Member

@marcprux marcprux commented Mar 1, 2026

This PR adds --apk mode to skip android test, providing two distinct ways to run Swift tests on Android. The default mode pushes a test executable to the device and runs it via adb shell. The new --apk mode packages the tests into a real Android APK with a NativeActivity harness, giving them access to the full Android framework through JNI.

APK mode: skip android test --apk

Packages the tests into a real Android APK and runs them as an installed app. Steps:

  1. swift build --build-tests --swift-sdk <triple> -Xlinker -shared -Xlinker -no-pie cross-compiles the test target as a shared library (.so) instead of an executable
  2. Collects .so dependencies (same three sources as default mode)
  3. Copies the test .so (renamed to lib<Package>Test.so) and all dependency .so files into a local staging lib/<abi>/ directory
  4. Generates a test harness SwiftPM package in a temp directory with two targets:
  • CAndroid (test_harness.c): implements ANativeActivity_onCreate which stores the activity pointer and spawns a test_runner pthread. The runner calls redirect_stdio() to pipe stdout/stderr through reader threads that forward each line to logcat via __android_log_print. It then calls dlopen on the test library, resolves swt_abiv0_getEntryPoint via dlsym, calls the getter to obtain the entry point, opens the event stream file if configured, and calls the Swift run_swift_tests() function. A handle_test_record() function writes JSON records to both stdout (which goes to logcat) and the event stream fd.
  • TestHarness (TestRunner.swift): exports @_cdecl("run_swift_tests") which unsafeBitCasts the raw pointer to the ST-0002 EntryPoint type, creates a record handler closure that delegates to the C handle_test_record, and calls the entry point in an async Task
  1. swift build --swift-sdk <triple> --package-path <harness> cross-compiles the harness, producing libtest_harness.so
  2. Copies libtest_harness.so into the APK's lib/<abi>/ alongside the test library
  3. Generates an AndroidManifest.xml declaring a NativeActivity with android.app.lib_name set to test_harness
  4. aapt2 link creates the unsigned APK from the manifest and android.jar
  5. zip -r -0 adds the lib/ tree (all native libraries) into the APK
  6. zipalign aligns the APK for efficient memory mapping
  7. keytool -genkeypair generates ~/.android/debug.keystore if it doesn't already exist
  8. apksigner sign signs the APK with the debug key
  9. adb uninstall removes any previous version of the test package
  10. adb install -t installs the signed APK
  11. adb logcat -c clears logcat
  12. adb shell am start -n <package>/android.app.NativeActivity launches the test activity
  13. adb logcat -s SwiftTest:I -v raw streams test output, forwarding each line to the host's stdout and watching for the SWIFT_TEST_EXIT_CODE=<n> sentinel
  14. If --event-stream-output-path was specified: adb pull copies /data/local/tmp/swift-test-events.jsonl to the host, then adb shell rm -f cleans it up
  15. adb uninstall removes the test APK (unless --no-cleanup)

Because the tests run inside a real Android app process with NativeActivity, they get a full JNI environment with access to the entire Android framework (Context, AssetManager, content providers, system services, etc.). The tradeoff is that resource bundles don't work: the test .so is loaded from the APK's lib/ directory by the Android runtime, and Foundation has no support for resolving Bundle.module resources from an APK's native library path. Tests that load bundled resources at runtime will fail to find them.

Default mode: skip android test

As a refresher, the pre-existing default test mode will compile the test target as an XCTest CLI executable and runs it directly on a shell on the Android emulator or device with the following steps:

  1. swift build --build-tests --swift-sdk <triple> cross-compiles the test target as an executable (<Package>PackageTests.xctest)
  2. Collects .so dependencies from three sources: build output artifacts, Swift runtime libraries from the SDK's dynamic lib path, and libc++_shared.so from the NDK sysroot
  3. Discovers any .resources sidecar directories in the build output (e.g. Module_TestModule.resources)
  4. adb shell mkdir -p /data/local/tmp/swift-android/<package>-<uuid>/ creates a staging directory
  5. adb push copies the executable, all .so files, .resources directories, and any --copy files to the staging directory
  6. adb shell cd '<staging>' && ./<Package>PackageTests.xctest runs the tests. If the ELF binary's needed section includes libTesting.so, the executable is run a second time with --testing-library swift-testing, and exit code 69 (EX_UNAVAILABLE = no tests found) is treated as success
  7. adb shell rm -r cleans up the staging directory (unless --no-cleanup)

Because the executable runs from a flat directory on the filesystem, Bundle.module resource lookup works normally: the .resources bundles are right there alongside the binary. The tradeoff is that there is no Android application context, no JVM, and no JNI environment, so tests that need Android framework APIs will not work in this mode.

Comparison

Default mode APK mode (--apk)
Test binary XCTest CLI executable Shared library in APK
Execution adb shell command line NativeActivity in a real Android app
Test frameworks XCTest + Swift Testing (two passes) Swift Testing only
Android APIs / JNI Not available Full access
Resource bundles Work (.resources dirs pushed alongside) Not available (Foundation Bundle limitation)
Output stdout/stderr over adb shell stdout/stderr piped to logcat

@cla-bot cla-bot bot added the cla-signed label Mar 1, 2026
@marcprux marcprux changed the title Add --apk flag to skip android test to package the tests into an APK for full Android API access Add --apk flag to android test command to package the tests into an APK for full Android API access Mar 1, 2026
@piercifani
Copy link

👏

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants