Skip to content

Commit ee51b7a

Browse files
committed
[Android] Add testing support
This adds support for running tests for the stdlib built for Android, both on the host machine and on an Android device. The Android variant of Swift may be built and tested using the following `build-script` invocation: ``` $ utils/build-script \ -R \ # Build in ReleaseAssert mode. -T \ # Run all tests. --android \ # Build for Android. --android-deploy-device-path /data/local/tmp \ # Temporary directory on the device where Android tests are run. --android-ndk ~/android-ndk-r10e \ # Path to an Android NDK. --android-ndk-version 21 \ --android-icu-uc ~/libicu-android/armeabi-v7a/libicuuc.so \ --android-icu-uc-include ~/libicu-android/armeabi-v7a/icu/source/common \ --android-icu-i18n ~/libicu-android/armeabi-v7a/libicui18n.so \ --android-icu-i18n-include ~/libicu-android/armeabi-v7a/icu/source/i18n/ ``` See docs/Testing.rst for more details.
1 parent 344d6e2 commit ee51b7a

File tree

18 files changed

+511
-12
lines changed

18 files changed

+511
-12
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ set(SWIFT_ANDROID_ICU_I18N "" CACHE STRING
160160
"Path to a directory containing libicui18n.so")
161161
set(SWIFT_ANDROID_ICU_I18N_INCLUDE "" CACHE STRING
162162
"Path to a directory containing headers libicui18n")
163+
set(SWIFT_ANDROID_DEPLOY_DEVICE_PATH "" CACHE STRING
164+
"Path on an Android device where build products will be pushed. These are used when running the test suite against the device")
163165

164166
#
165167
# User-configurable Darwin-specific options.

docs/Android.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
The Swift stdlib can be compiled for Android armv7 targets, which makes it
44
possible to execute Swift code on a mobile device running Android. This guide
5-
explains how to run a simple "Hello, world" program on your Android device.
5+
explains:
6+
7+
1. How to run a simple "Hello, world" program on your Android device.
8+
2. How to run the Swift test suite, targeting Android, and on an Android device.
69

710
If you encounter any problems following the instructions below, please file a
811
bug using https://bugs.swift.org/.
@@ -39,7 +42,7 @@ To follow along with this guide, you'll need:
3942
turn on remote debugging by following the official instructions:
4043
https://developer.chrome.com/devtools/docs/remote-debugging.
4144

42-
## "Hello, world" on Android
45+
## Part One: "Hello, world" on Android
4346

4447
### 1. Downloading (or building) the Swift Android stdlib dependencies
4548

@@ -171,3 +174,28 @@ Hello, Android
171174

172175
Congratulations! You've just run your first Swift program on Android.
173176

177+
## Part Two: Running the Swift test suite hosted on an Android device
178+
179+
When running the test suite, build products are automatically pushed to your
180+
device. As in part one, you'll need to connect your Android device via USB:
181+
182+
1. Connect your Android device to your computer via USB. Ensure that remote
183+
debugging is enabled for that device by following the official instructions:
184+
https://developer.chrome.com/devtools/docs/remote-debugging.
185+
2. Confirm the device is connected by running `adb devices`. You should see
186+
your device listed.
187+
3. Run the tests using the build script:
188+
189+
```
190+
$ utils/build-script \
191+
-R \ # Build in ReleaseAssert mode.
192+
-T \ # Run all tests.
193+
--android \ # Build for Android.
194+
--android-deploy-device-path /data/local/tmp \ # Temporary directory on the device where Android tests are run.
195+
--android-ndk ~/android-ndk-r10e \ # Path to an Android NDK.
196+
--android-ndk-version 21 \
197+
--android-icu-uc ~/libicu-android/armeabi-v7a/libicuuc.so \
198+
--android-icu-uc-include ~/libicu-android/armeabi-v7a/icu/source/common \
199+
--android-icu-i18n ~/libicu-android/armeabi-v7a/libicui18n.so \
200+
--android-icu-i18n-include ~/libicu-android/armeabi-v7a/icu/source/i18n/
201+
```

test/1_stdlib/InputStream.swift.gyb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
// RUN: %S/../../utils/line-directive %t/InputStream.swift -- %target-run %t/a.out
1717
// REQUIRES: executable_test
1818

19+
// FIXME: The Android test runner is incapable of running this test, which
20+
// relies on stdin input.
21+
// UNSUPPORTED: OS=linux-androideabi
22+
1923
import StdlibUnittest
2024

2125

test/CMakeLists.txt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,29 @@ if(PYTHONINTERP_FOUND)
212212

213213
set(command_upload_stdlib)
214214
if("${SDK}" STREQUAL "IOS" OR "${SDK}" STREQUAL "TVOS" OR "${SDK}" STREQUAL "WATCHOS")
215-
# These are supported testing SDKs.
215+
# These are supported testing SDKs, but their implementation of
216+
# `command_upload_stdlib` is hidden.
217+
elseif("${SDK}" STREQUAL "ANDROID")
218+
# Warning: This step will fail if you do not have an Android device
219+
# connected via USB. See docs/Android.md for details on
220+
# how to run the test suite for Android.
221+
set(command_upload_stdlib
222+
COMMAND
223+
# Reboot the device and remove everything in its tmp
224+
# directory. Build products and test executables are pushed
225+
# to that directory when running the test suite.
226+
${PYTHON_EXECUTABLE} "${SWIFT_SOURCE_DIR}/utils/android/adb_clean.py"
227+
COMMAND
228+
${PYTHON_EXECUTABLE} "${SWIFT_SOURCE_DIR}/utils/android/adb_push_built_products.py"
229+
--ndk "${SWIFT_ANDROID_NDK_PATH}"
230+
--destination "${SWIFT_ANDROID_DEPLOY_DEVICE_PATH}"
231+
# Build products like libswiftCore.so.
232+
"${SWIFTLIB_DIR}/android"
233+
# These two directories may contain the same libraries,
234+
# but upload both to device just in case. Duplicates will be
235+
# overwritten, and uploading doesn't take very long anyway.
236+
"${SWIFT_ANDROID_ICU_UC}"
237+
"${SWIFT_ANDROID_ICU_I18N}")
216238
endif()
217239

218240
foreach(test_subset ${TEST_SUBSETS})

test/lit.cfg

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,69 @@ elif run_os == 'linux-gnu' or run_os == 'linux-gnueabihf' or run_os == 'freebsd'
741741
config.target_ld = (
742742
"ld -L%s" %
743743
(os.path.join(test_resource_dir, config.target_sdk_name)))
744+
elif run_os == 'linux-androideabi':
745+
lit_config.note("Testing Android " + config.variant_triple)
746+
config.target_object_format = "elf"
747+
config.target_dylib_extension = "so"
748+
config.target_runtime = "native"
749+
config.target_swift_autolink_extract = inferSwiftBinary("swift-autolink-extract")
750+
config.target_sdk_name = "android"
751+
android_linker_opt = "-L {libcxx} -L {libgcc}".format(
752+
libcxx=os.path.join(config.android_ndk_path,
753+
"sources", "cxx-stl", "llvm-libc++", "libs",
754+
"armeabi-v7a"),
755+
libgcc=os.path.join(config.android_ndk_path,
756+
"toolchains",
757+
"arm-linux-androideabi-{}".format(
758+
config.android_ndk_gcc_version),
759+
"prebuilt", "linux-x86_64", "lib", "gcc",
760+
"arm-linux-androideabi",
761+
config.android_ndk_gcc_version))
762+
config.target_build_swift = (
763+
'%s -target %s -sdk %s %s -Xlinker -pie %s %s %s %s'
764+
% (config.swiftc, config.variant_triple, config.variant_sdk,
765+
android_linker_opt, resource_dir_opt, mcp_opt,
766+
config.swift_test_options, swift_execution_tests_extra_flags))
767+
config.target_swift_frontend = (
768+
'%s -frontend -target %s -sdk %s %s %s'
769+
% (config.swift, config.variant_triple, config.variant_sdk,
770+
android_linker_opt, resource_dir_opt))
771+
subst_target_swift_frontend_mock_sdk = config.target_swift_frontend
772+
subst_target_swift_frontend_mock_sdk_after = ""
773+
config.target_run = os.path.join(
774+
config.swift_src_root, 'utils', 'android', 'adb_test_runner.py')
775+
# FIXME: Include -sdk in this invocation.
776+
config.target_sil_opt = (
777+
'%s -target %s %s %s' %
778+
(config.sil_opt, config.variant_triple, resource_dir_opt, mcp_opt))
779+
config.target_swift_ide_test = (
780+
'%s -target %s %s %s %s' %
781+
(config.swift_ide_test, config.variant_triple, resource_dir_opt,
782+
mcp_opt, ccp_opt))
783+
subst_target_swift_ide_test_mock_sdk = config.target_swift_ide_test
784+
subst_target_swift_ide_test_mock_sdk_after = ""
785+
config.target_swiftc_driver = (
786+
"%s -target %s -sdk %s %s %s %s" %
787+
(config.swiftc, config.variant_triple, config.variant_sdk,
788+
android_linker_opt, resource_dir_opt, mcp_opt))
789+
config.target_swift_modulewrap = (
790+
'%s -modulewrap -target %s' %
791+
(config.swiftc, config.variant_triple))
792+
config.target_clang = (
793+
"clang++ -target %s %s" %
794+
(config.variant_triple, clang_mcp_opt))
795+
config.target_ld = "{} -L{}".format(
796+
os.path.join(
797+
config.android_ndk_path,
798+
'toolchains',
799+
'arm-linux-androideabi-{}'.format(config.android_ndk_gcc_version),
800+
'prebuilt',
801+
'linux-x86_64',
802+
'arm-linux-androideabi',
803+
'bin'),
804+
os.path.join(test_resource_dir, config.target_sdk_name))
805+
# The Swift interpreter is not available when targeting Android.
806+
config.available_features.remove('swift_interpreter')
744807

745808
else:
746809
lit_config.fatal("Don't know how to define target_run and "

test/lit.site.cfg.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ config.variant_sdk = "@VARIANT_SDK@"
1818
config.variant_suffix = "@VARIANT_SUFFIX@"
1919
config.swiftlib_dir = "@LIT_SWIFTLIB_DIR@"
2020
config.darwin_xcrun_toolchain = "@SWIFT_DARWIN_XCRUN_TOOLCHAIN@"
21+
config.android_ndk_path = "@SWIFT_ANDROID_NDK_PATH@"
22+
config.android_ndk_gcc_version = "@SWIFT_ANDROID_NDK_GCC_VERSION@"
2123

2224
config.coverage_mode = "@SWIFT_ANALYZE_CODE_COVERAGE@"
2325

utils/android/adb/__init__.py

Whitespace-only changes.

utils/android/adb/commands.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# adb/commands.py - Run executables on an Android device -*- python -*-
2+
#
3+
# This source file is part of the Swift.org open source project
4+
#
5+
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
6+
# Licensed under Apache License v2.0 with Runtime Library Exception
7+
#
8+
# See http://swift.org/LICENSE.txt for license information
9+
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
#
11+
# ----------------------------------------------------------------------------
12+
#
13+
# Push executables to an Android device and run them, capturing their output
14+
# and exit code.
15+
#
16+
# ----------------------------------------------------------------------------
17+
18+
from __future__ import print_function
19+
20+
import subprocess
21+
import tempfile
22+
import uuid
23+
24+
25+
# A temporary directory on the Android device.
26+
DEVICE_TEMP_DIR = '/data/local/tmp'
27+
28+
29+
def shell(args):
30+
"""
31+
Execute 'adb shell' with the given arguments.
32+
33+
Raise an exception if 'adb shell' returns a non-zero exit code.
34+
Note that this only occurs if communication with the connected device
35+
fails, not if the command run on the device fails.
36+
"""
37+
return subprocess.check_output(['adb', 'shell'] + args)
38+
39+
40+
def rmdir(path):
41+
"""Remove all files in the device directory at `path`."""
42+
shell(['rm', '-rf', '{}/*'.format(path)])
43+
44+
45+
def push(local_path, device_path):
46+
"""Move the file at the given local path to the path on the device."""
47+
return subprocess.check_output(['adb', 'push', local_path, device_path],
48+
stderr=subprocess.STDOUT).strip()
49+
50+
51+
def reboot():
52+
"""Reboot the connected Android device, waiting for it to return online."""
53+
subprocess.check_call(['adb', 'reboot'])
54+
subprocess.check_call(['adb', 'wait-for-device'])
55+
56+
57+
def _create_executable_on_device(device_path, contents):
58+
_, tmp = tempfile.mkstemp()
59+
with open(tmp, 'w') as f:
60+
f.write(contents)
61+
push(tmp, device_path)
62+
shell(['chmod', '755', device_path])
63+
64+
65+
def execute_on_device(executable_path, executable_arguments):
66+
"""
67+
Run an executable on an Android device.
68+
69+
Push an executable at the given 'executable_path' to an Android device,
70+
then execute that executable on the device, passing any additional
71+
'executable_arguments'. Return 0 if the executable succeeded when run on
72+
device, and 1 otherwise.
73+
74+
This function is not as simple as calling 'adb shell', for two reasons:
75+
76+
1. 'adb shell' can only take input up to a certain length, so it fails for
77+
long executable names or when a large amount of arguments are passed to
78+
the executable. This function attempts to limit the size of any string
79+
passed to 'adb shell'.
80+
2. 'adb shell' ignores the exit code of any command it runs. This function
81+
therefore uses its own mechanisms to determine whether the executable
82+
had a successful exit code when run on device.
83+
"""
84+
# We'll be running the executable in a temporary directory in
85+
# /data/local/tmp. `adb shell` has trouble with commands that
86+
# exceed a certain length, so to err on the safe side we only
87+
# use the first 10 characters of the UUID.
88+
uuid_dir = '{}/{}'.format(DEVICE_TEMP_DIR, str(uuid.uuid4())[:10])
89+
shell(['mkdir', '-p', uuid_dir])
90+
91+
# `adb` can only handle commands under a certain length. No matter what the
92+
# original executable's name, on device we call it `__executable`.
93+
executable = '{}/__executable'.format(uuid_dir)
94+
push(executable_path, executable)
95+
96+
# When running the executable on the device, we need to pass it the same
97+
# arguments, as well as specify the correct LD_LIBRARY_PATH. Save these
98+
# to a file we can easily call multiple times.
99+
executable_with_args = '{}/__executable_with_args'.format(uuid_dir)
100+
_create_executable_on_device(
101+
executable_with_args,
102+
'LD_LIBRARY_PATH={uuid_dir}:{tmp_dir} '
103+
'{executable} {executable_arguments}'.format(
104+
uuid_dir=uuid_dir,
105+
tmp_dir=DEVICE_TEMP_DIR,
106+
executable=executable,
107+
executable_arguments=' '.join(executable_arguments)))
108+
109+
# Write the output from the test executable to a file named '__stdout', and
110+
# if the test executable succeeds, write 'SUCCEEDED' to a file
111+
# named '__succeeded'. We do this because `adb shell` does not report
112+
# the exit code of the command it executes on the device, so instead we
113+
# check the '__succeeded' file for our string.
114+
executable_stdout = '{}/__stdout'.format(uuid_dir)
115+
succeeded_token = 'SUCCEEDED'
116+
executable_succeeded = '{}/__succeeded'.format(uuid_dir)
117+
executable_piped = '{}/__executable_piped'.format(uuid_dir)
118+
_create_executable_on_device(
119+
executable_piped,
120+
'{executable_with_args} > {executable_stdout} && '
121+
'echo "{succeeded_token}" > {executable_succeeded}'.format(
122+
executable_with_args=executable_with_args,
123+
executable_stdout=executable_stdout,
124+
succeeded_token=succeeded_token,
125+
executable_succeeded=executable_succeeded))
126+
127+
# We've pushed everything we need to the device.
128+
# Now execute the wrapper script.
129+
shell([executable_piped])
130+
131+
# Grab the results of running the executable on device.
132+
stdout = shell(['cat', executable_stdout])
133+
exitcode = shell(['cat', executable_succeeded])
134+
if not exitcode.startswith(succeeded_token):
135+
debug_command = '$ adb shell {}'.format(executable_with_args)
136+
print('Executable exited with a non-zero code on the Android device.\n'
137+
'Device stdout:\n'
138+
'{stdout}\n'
139+
'To debug, run:\n'
140+
'{debug_command}\n'.format(
141+
stdout=stdout,
142+
debug_command=debug_command))
143+
144+
# Exit early so that the output isn't passed to FileCheck, nor are any
145+
# temporary directories removed; this allows the user to re-run
146+
# the executable on the device.
147+
return 1
148+
149+
print(stdout)
150+
151+
shell(['rm', '-rf', uuid_dir])
152+
return 0

utils/android/adb_clean.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env python
2+
# adb_reboot.py - Reboots and cleans an Android device. -*- python -*-
3+
#
4+
# This source file is part of the Swift.org open source project
5+
#
6+
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
7+
# Licensed under Apache License v2.0 with Runtime Library Exception
8+
#
9+
# See http://swift.org/LICENSE.txt for license information
10+
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
11+
12+
from adb.commands import DEVICE_TEMP_DIR, reboot, rmdir
13+
14+
15+
if __name__ == '__main__':
16+
reboot()
17+
rmdir(DEVICE_TEMP_DIR)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python
2+
# adb_push_build_products.py - Push libraries to Android device -*- python -*-
3+
#
4+
# This source file is part of the Swift.org open source project
5+
#
6+
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
7+
# Licensed under Apache License v2.0 with Runtime Library Exception
8+
#
9+
# See http://swift.org/LICENSE.txt for license information
10+
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
11+
12+
import sys
13+
14+
from adb_push_built_products.main import main
15+
16+
17+
if __name__ == '__main__':
18+
sys.exit(main())

utils/android/adb_push_built_products/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)