Testing on Android

Testing currently works on Android by cross-compiling the test runner executable with the Swift SDK for Android, copying the binary along with any dependent shared object files (plus any resources the tests require) over to an Android emulator or device, and then just running the executable as a command-line tool using adb. This is how testing is performed by the GitHub swift-android-action, the swiftlang/github-workflows, and with the skip android test command. They all do the same thing in slightly different ways. There are some quirks (like having to re-run the test executable twice, once for XCTest and once for Testing), but overall it works fine.

But a command-line executable isn't a real "app", which means it doesn't have any ambient Java environment. This means that you can't access any of the Android Java SDK, or even much of the NDK (because many NDK libraries, despite being C or C++, still need to be initialized from an Android Java Context, like AAssetManager_fromJava and ANativeWindow_fromSurface). This severely limits what can be tested when it comes to integration with the Android platform.

So we want a way to package up the tests such that they can be executed in the context of an actual .apk that is launched on an emulator or device. Gradle has support for this through their "instrumentation testing", and it is how skip test (not to be confused with skip android test) works with transpiled test cases. What is missing is a way to actually invoke the XCTest/Swift Testing tests from an .apk, and that will require building and bundling the tests differently.

I raised this last May at Building tests as a dynamic library rather than an executable. What we need is a way to build the tests as a shared library rather than as a command-line executable, and then host the tests within an app, just like iOS testing is done from Xcode. I think what we need is the ability to output TestProductStyle.loadableBundle for non-Darwin platforms. Maybe @dschaefer2 or @grynspan knows? Then we need to figure out how to call into the tests from Java and process the output, something that @madsodgaard and @ktoso might help with. Then we need a nice way to build, package, and run them, either with a separate harness like skip android test, or via a SwiftPM Command Plugin, or by baking support for it right into SwiftPM itself.

This project cuts across different areas and workgroups: Testing, Java, SwiftPM, and Android. Is there precedent for an ephemeral "task force" workgroup to tackle interdisciplinary projects like this? Might there be interest in forming one and having a call in a couple of weeks?

6 Likes

If what you're aiming for is a loadable executable (a .so file, I assume) you probably need a new test product style .loadableExecutable. .loadableBundle produces a Darwin/Mach-O bundle with a directory structure specific to Xcode/XCTest, which isn't going to be useful on Android.

It isn't hard to add a new test product style, just a bit tedious. I think you'd need to make changes in the swift-package-manager and swift-build repos to plumb everything through.

Then you'd need something executable that calls dlopen() to load the executable and dlsym() to find and invoke its entry point function (an externally-visible main function). I'd want us to have something in the Swift toolchain for this purpose when targetting Android, but I can also understand if Skip specifically has its own equivalent tool.

I wonder if we might get away with something like:

$ swift build -Xlinker -shared -Xlinker -no-pie --swift-sdk aarch64-unknown-linux-android28 --build-tests --verbose

$ file ./.build/aarch64-unknown-linux-android28/debug/DemoPackageTests.xctest
./.build/aarch64-unknown-linux-android28/debug/DemoPackageTests.xctest: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=5c1e470056a9010831ce58bfff6b47602ccf5be4, with debug_info, not stripped

And then dlopen() it…

In your previous thread I replied indicating how it might be even better to transition SwiftPM to producing dynamically-loadable test products for all platforms, and I still think that could be useful.

The Testing Workgroup is meeting tomorrow (Monday, January 26) at 1pm PT. I think as a start towards this effort you could come talk about this topic with the group. Feel free to request an invite if you’d like to attend!

1 Like

That'd be great! I'll follow up with @testing-workgroup.

1 Like

A follow up to this topic from a different angle: it would be great if we could get at least test runner executables working initially for all wanted cross-compilation target platforms, by baking in some SwiftPM support for connecting to emulators, containers, and external devices. I raised the matter in the latest Build and Packaging workgroup call and was told we'd need to run any new CLI commands through Swift Evolution, but that some of this, like configuration of the various targets, could probably be done in SwiftPM plugins.

For now, it would be good if people started prototyping connecting SwiftPM to your target of choice locally- eg I will be connecting SwiftPM to Android's adb tool- then we can compare notes on what will be needed to turn this into a real feature going forward.

I'm happy to report that after an illuminating meeting with the @testing-workgroup I was able to implement support for this in the skip android test front-end:

The whole process is pretty involved and useful only for Android development, so I'm skeptical about whether it makes sense to try to roll this into SwiftPM itself. But I have some vague half-baked ideas around how something like this might be included as part of a cross-compilation SDK itself, something like:

swift test --sdk aarch64-unknown-linux-android28 --sdk-testing-plugin-flags package-as-apk

How might this work? Would the cross-compilation SDK have a plugin-style shared library or command executable that swift test would delegate to at some point? Perhaps it would work like a macro, where the tooling would be built on-demand (to avoid needing to ship host-specific binaries with the Swift SDK)? I'll ponder this, along with other useful scenarios and applications like general emulator/container/external device support.

Update: I've added my thoughts to the related thread about this at Expand `swift test` to enable testing with cross-compilation target triples · Issue #9740 · swiftlang/swift-package-manager · GitHub

1 Like

As we discussed on the Build and Packaging call, it would be much better if we had a plugin architecture that let you plug this specialized behaviour into SwiftPM. We could then build up a package ecosystem of such things that anyone can create and publish as we bring swift test to new platforms.

It is an exciting area I’d like to invest in and these are great examples that help feed into design.

I would anticipate the same architecture could be used for Wasm and, in theory at least, for Apple platforms where testing is dependent on Xcode (iOS, simulators, etc.)

1 Like

Definitely. I’m also thinking about embedded, at least on such systems where we can get a communication channel.

And, of course this should work for both swift run and swift test.

I think the mechanics would be pretty simple. A way to let the plugin know what it needs about the package graph and build artifacts and maybe the tests to run as well as a description of the device it needs to deploy to and communicate with. It would then do the necessary deployment and set up a stream SwiftPM could use to manage the session in a device independent way.

And then a clean set of CLI options to set that all up.

I’m sure we’ll run into things as we try different device architectures out but we can make it experimental for a while to get people to try it and learn from that.

I am also working on some ideas for putting SDKs in packages, so having these plugins in the same package makes a lot of sense. Then you have everything you need to build, run and test packages for a given platform all in one spot that SwiftPM can grab when it needs it.

2 Likes