Skip to content

Commit a1f1976

Browse files
authored
Merge pull request #1714 from SwiftAndroid/tests
2 parents 5bea187 + ee51b7a commit a1f1976

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
@@ -162,6 +162,8 @@ set(SWIFT_ANDROID_ICU_I18N "" CACHE STRING
162162
"Path to a directory containing libicui18n.so")
163163
set(SWIFT_ANDROID_ICU_I18N_INCLUDE "" CACHE STRING
164164
"Path to a directory containing headers libicui18n")
165+
set(SWIFT_ANDROID_DEPLOY_DEVICE_PATH "" CACHE STRING
166+
"Path on an Android device where build products will be pushed. These are used when running the test suite against the device")
165167

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

748811
else:
749812
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)