Skip to content

Commit 587f448

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 66c4c87 commit 587f448

File tree

23 files changed

+480
-4
lines changed

23 files changed

+480
-4
lines changed

docs/Testing.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,48 @@ targets mentioned above and modify it as necessary. lit.py also has several
134134
useful features, like timing tests and providing a timeout. Check these features
135135
out with ``lit.py -h``.
136136

137+
Running tests hosted on an Android device
138+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
139+
140+
You may run tests targeting android-armv7 by connecting a device to run the
141+
tests on, then pushing the necessary dependencies to that device.
142+
143+
1. Connect your Android device to your computer via USB. Ensure that remote
144+
debugging is enabled for that device by following the official instructions:
145+
https://developer.chrome.com/devtools/docs/remote-debugging.
146+
2. Confirm the device is connected by running ``adb devices``. You should see
147+
your device listed.
148+
3. Push the built products for android-armv7 to your device, using the
149+
``utils/android/adb_push_built_products.py`` script. For example, to push
150+
the built products at a build directory
151+
``~/swift/Ninja-ReleaseAssert/swift-linux-x86_64``, with an Android NDK
152+
placed at ``~/android-ndk-r10e``, run the following:
153+
154+
$ utils/android/adb_push_built_products.py \
155+
~/swift/build/Ninja-ReleaseAssert/swift-linux-x86_64/lib/swift/android/ \
156+
--ndk ~/android-ndk-r10e
157+
4. Run the test suite for the ``android-armv7`` target.
158+
159+
You may run the tests using the build script as well. Specifying a
160+
``--android-deploy-device-path`` pushes the built Android products to your
161+
device as part of the build:
162+
163+
$ utils/build-script \
164+
-R \ # Build in ReleaseAssert mode.
165+
-T \ # Run all tests.
166+
--android \ # Build for Android.
167+
--android-deploy-device-path /data/local/tmp \ # Temporary directory on the device where Android tests are run.
168+
--android-ndk ~/android-ndk-r10e \ # Path to an Android NDK.
169+
--android-ndk-version 21 \
170+
--android-icu-uc ~/libicu-android/armeabi-v7a/libicuuc.so \
171+
--android-icu-uc-include ~/libicu-android/armeabi-v7a/icu/source/common \
172+
--android-icu-i18n ~/libicu-android/armeabi-v7a/libicui18n.so \
173+
--android-icu-i18n-include ~/libicu-android/armeabi-v7a/icu/source/i18n/
174+
175+
You must run the Linux tests once in order to build the Android test suite.
176+
After that, you may run the above command with the ``--skip-test-linux`` option
177+
to only run Android tests.
178+
137179
Writing tests
138180
-------------
139181

stdlib/private/SwiftPrivateLibcExtras/Subprocess.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ public func spawnChild(_ args: [String])
136136

137137
// Close the pipe when we're done writing the error.
138138
close(childToParentPipe.writeFD)
139+
} else if pid > 0 {
140+
close(childToParentPipe.writeFD)
139141
}
140142
#else
141143
var fileActions = _make_posix_spawn_file_actions_t()

test/1_stdlib/InputStream.swift.gyb

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

19+
// FIXME: This test takes too long to complete on Android.
20+
// UNSUPPORTED: OS=linux-androideabi
21+
1922
import StdlibUnittest
2023

2124

test/1_stdlib/POSIX.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// RUN: %target-run-simple-swift
22
// REQUIRES: executable_test
33

4+
// Android Bionic does not provide a working implementation of
5+
// <semaphore.h>.
6+
// XFAIL: OS=linux-androideabi
7+
48
import StdlibUnittest
59
#if os(Linux)
610
import Glibc

test/ClangModules/autolinking.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// UNSUPPORTED: OS=linux-gnu
1717
// UNSUPPORTED: OS=linux-gnueabihf
1818
// UNSUPPORTED: OS=freebsd
19+
// UNSUPPORTED: OS=linux-androideabi
1920

2021
import LinkMusket
2122
import LinkFramework

test/IRGen/c_layout.sil

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
// RUN: %target-swift-frontend -I %S/Inputs/abi %s -emit-ir | FileCheck %s --check-prefix=CHECK-%target-cpu
1+
// FIXME: Rather than passing `-fsigned-char`, this should test each platform's expected char
2+
// signedness. For Android, this means implicitly unsigned. See: https://github.com/apple/swift/pull/1103
3+
// RUN: %target-swift-frontend -Xcc -fsigned-char -I %S/Inputs/abi %s -emit-ir | FileCheck %s --check-prefix=CHECK-%target-cpu
24

35
sil_stage canonical
46
import c_layout

test/Serialization/autolinking.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
// UNSUPPORTED: OS=linux-gnu
2626
// UNSUPPORTED: OS=linux-gnueabihf
2727
// UNSUPPORTED: OS=freebsd
28+
// UNSUPPORTED: OS=linux-androideabi
2829

2930
import someModule
3031

test/lit.cfg

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ elif run_os == 'linux-gnu' or run_os == 'linux-gnueabihf' or run_os == 'freebsd'
719719
'%s -Xfrontend -disable-access-control %%s' % (target_run_base))
720720
config.available_features.add('interpret')
721721
config.environment['SWIFT_INTERPRETER'] = config.swift
722+
# FIXME: Include -sdk in this invocation.
722723
config.target_sil_opt = (
723724
'%s -target %s %s %s' %
724725
(config.sil_opt, config.variant_triple, resource_dir_opt, mcp_opt))
@@ -740,6 +741,62 @@ elif run_os == 'linux-gnu' or run_os == 'linux-gnueabihf' or run_os == 'freebsd'
740741
config.target_ld = (
741742
"ld -L%s" %
742743
(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 = (
796+
"ld -L%s" %
797+
(os.path.join(test_resource_dir, config.target_sdk_name)))
798+
# The Swift interpreter is not available when targeting Android.
799+
config.available_features.remove('swift_interpreter')
743800

744801
else:
745802
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: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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 push(local_path, device_path):
41+
"""Move the file at the given local path to the path on the device."""
42+
return subprocess.check_output(['adb', 'push', local_path, device_path],
43+
stderr=subprocess.STDOUT).strip()
44+
45+
46+
def _create_executable_on_device(device_path, contents):
47+
_, tmp = tempfile.mkstemp()
48+
with open(tmp, 'w') as f:
49+
f.write(contents)
50+
push(tmp, device_path)
51+
shell(['chmod', '755', device_path])
52+
53+
54+
def execute_on_device(executable_path,
55+
executable_arguments,
56+
rerun_failures):
57+
"""
58+
Run an executable on an Android device.
59+
60+
Push an executable at the given 'executable_path' to an Android device,
61+
then execute that executable on the device, passing any additional
62+
'executable_arguments'. Return 0 if the executable succeeded when run on
63+
device, and 1 otherwise.
64+
65+
This function is not as simple as calling 'adb shell', for two reasons:
66+
67+
1. 'adb shell' can only take input up to a certain length, so it fails for
68+
long executable names or when a large amount of arguments are passed to
69+
the executable. This function attempts to limit the size of any string
70+
passed to 'adb shell'.
71+
2. 'adb shell' ignores the exit code of any command it runs. This function
72+
therefore uses its own mechanisms to determine whether the executable
73+
had a successful exit code when run on device.
74+
75+
If 'rerun_failures' is specified, this function re-runs failing tests,
76+
printing their output to stdout.
77+
"""
78+
# We'll be running the executable in a temporary directory in
79+
# /data/local/tmp. `adb shell` has trouble with commands that
80+
# exceed a certain length, so to err on the safe side we only
81+
# use the first 10 characters of the UUID.
82+
uuid_dir = '{}/{}'.format(DEVICE_TEMP_DIR, str(uuid.uuid4())[:10])
83+
shell(['mkdir', '-p', uuid_dir])
84+
85+
# `adb` can only handle commands under a certain length. No matter what the
86+
# original executable's name, on device we call it `__executable`.
87+
executable = '{}/__executable'.format(uuid_dir)
88+
push(executable_path, executable)
89+
90+
# When running the executable on the device, we need to pass it the same
91+
# arguments, as well as specify the correct LD_LIBRARY_PATH. Save these
92+
# to a file we can easily call multiple times.
93+
executable_with_args = '{}/__executable_with_args'.format(uuid_dir)
94+
_create_executable_on_device(
95+
executable_with_args,
96+
'LD_LIBRARY_PATH={uuid_dir}:{tmp_dir} '
97+
'{executable} {executable_arguments}'.format(
98+
uuid_dir=uuid_dir,
99+
tmp_dir=DEVICE_TEMP_DIR,
100+
executable=executable,
101+
executable_arguments=' '.join(executable_arguments)))
102+
103+
# Write the output from the test executable to a file named '__stdout', and
104+
# if the test executable succeeds, write 'SUCCEEDED' to a file
105+
# named '__succeeded'. We do this because `adb shell` does not report
106+
# the exit code of the command it executes on the device, so instead we
107+
# check the '__succeeded' file for our string.
108+
executable_stdout = '{}/__stdout'.format(uuid_dir)
109+
succeeded_token = 'SUCCEEDED'
110+
executable_succeeded = '{}/__succeeded'.format(uuid_dir)
111+
executable_piped = '{}/__executable_piped'.format(uuid_dir)
112+
_create_executable_on_device(
113+
executable_piped,
114+
'{executable_with_args} > {executable_stdout} && '
115+
'echo "{succeeded_token}" > {executable_succeeded}'.format(
116+
executable_with_args=executable_with_args,
117+
executable_stdout=executable_stdout,
118+
succeeded_token=succeeded_token,
119+
executable_succeeded=executable_succeeded))
120+
121+
# We've pushed everything we need to the device.
122+
# Now execute the wrapper script.
123+
shell([executable_piped])
124+
125+
# Grab the results of running the executable on device.
126+
stdout = shell(['cat', executable_stdout])
127+
exitcode = shell(['cat', executable_succeeded])
128+
if not exitcode.startswith(succeeded_token):
129+
debug_command = '$ adb shell {}'.format(executable_with_args)
130+
print('Executable exited with a non-zero code on the Android device.\n'
131+
'Device stdout:\n'
132+
'{stdout}\n'
133+
'To debug, run:\n'
134+
'{debug_command}\n'.format(
135+
stdout=stdout,
136+
debug_command=debug_command))
137+
138+
if rerun_failures:
139+
print('Re-running executable with command:\n'
140+
'{}\n'.format(debug_command))
141+
print(shell([executable_with_args]))
142+
143+
# Exit early so that the output isn't passed to FileCheck, nor are any
144+
# temporary directories removed; this allows the user to re-run
145+
# the executable on the device.
146+
return 1
147+
148+
print(stdout)
149+
150+
shell(['rm', '-rf', uuid_dir])
151+
return 0
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 - Calls adb_push_build_products.main -*- 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)