Skip to content

Commit da6d0e2

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 420c8ff commit da6d0e2

24 files changed

+484
-9
lines changed

docs/Testing.rst

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

118+
Running tests hosted on an Android device
119+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
120+
121+
You may run tests targeting android-armv7 by connecting a device to run the
122+
tests on, then pushing the necessary dependencies to that device.
123+
124+
1. Connect your Android device to your computer via USB. Ensure that remote
125+
debugging is enabled for that device by following the official instructions:
126+
https://developer.chrome.com/devtools/docs/remote-debugging.
127+
2. Confirm the device is connected by running ``adb devices``. You should see
128+
your device listed.
129+
3. Push the built products for android-armv7 to your device, using the
130+
``utils/android/adb_push_built_products.py`` script. For example, to push
131+
the built products at a build directory
132+
``~/swift/Ninja-ReleaseAssert/swift-linux-x86_64``, with an Android NDK
133+
placed at ``~/android-ndk-r10e``, run the following:
134+
135+
$ utils/android/adb_push_built_products.py \
136+
~/swift/build/Ninja-ReleaseAssert/swift-linux-x86_64/lib/swift/android/ \
137+
--ndk ~/android-ndk-r10e
138+
4. Run the test suite for the ``android-armv7`` target.
139+
140+
You may run the tests using the build script as well. Specifying a
141+
``--android-deploy-device-path`` pushes the built Android products to your
142+
device as part of the build:
143+
144+
$ utils/build-script \
145+
-R \ # Build in ReleaseAssert mode.
146+
-T \ # Run all tests.
147+
--android \ # Build for Android.
148+
--android-deploy-device-path /data/local/tmp \ # Temporary directory on the device where Android tests are run.
149+
--android-ndk ~/android-ndk-r10e \ # Path to an Android NDK.
150+
--android-ndk-version 21 \
151+
--android-icu-uc ~/libicu-android/armeabi-v7a/libicuuc.so \
152+
--android-icu-uc-include ~/libicu-android/armeabi-v7a/icu/source/common \
153+
--android-icu-i18n ~/libicu-android/armeabi-v7a/libicui18n.so \
154+
--android-icu-i18n-include ~/libicu-android/armeabi-v7a/icu/source/i18n/
155+
156+
You must run the Linux tests once in order to build the Android test suite.
157+
After that, you may run the above command with the ``--skip-test-linux`` option
158+
to only run Android tests.
159+
118160
Writing tests
119161
-------------
120162

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/IRGen/report_dead_method_call.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
// RUN: %target-run %t/a.out p1 p2 2> %t/err3.log; FileCheck %s < %t/err3.log
66
// REQUIRES: executable_test
77

8+
// This test correctly outputs "fatal error" on an Android device, but
9+
// the Android test runner interprets the abort as a test failure.
10+
// XFAIL: OS=linux-androideabi
811

912
// The -disable-access-control option let us "call" methods, which are removed
1013
// by dead method elimination.

test/Prototypes/FloatingPoint.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension UInt32 : FloatingPointRepresentation {
2727
// Ewwww? <rdar://problem/20060017>
2828
#if os(OSX) || os(iOS) || os(watchOS) || os(tvOS)
2929
import Darwin
30-
#elseif os(Linux)
30+
#elseif os(Linux) || os(Android)
3131
import Glibc
3232
#endif
3333

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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,65 @@ elif run_os == 'linux-gnu' or run_os == 'linux-gnueabihf' or run_os == 'freebsd'
723723
config.target_ld = (
724724
"ld -L%s" %
725725
(os.path.join(test_resource_dir, config.target_sdk_name)))
726+
elif run_os == 'linux-androideabi':
727+
# Android
728+
lit_config.note("Testing Android " + config.variant_triple)
729+
# FIXME: This value should be read off of config.android_ndk_path,
730+
# but I can't figure out how. The value is set via CMake, and the
731+
# build directory contains a test-android-armv7/lit.site.cfg with
732+
# its @SWIFT_ANDROID_NDK_PATH@ correctly replaced. So what's wrong?
733+
android_ndk_path = "/home/modocache/android-ndk-r10e"
734+
735+
config.target_object_format = "elf"
736+
config.target_runtime = "native"
737+
config.target_swift_autolink_extract = inferSwiftBinary("swift-autolink-extract")
738+
config.target_sdk_name = "android"
739+
android_linker_opt = "-L {libcxx} -L {libgcc}".format(
740+
libcxx=os.path.join(android_ndk_path,
741+
"sources", "cxx-stl", "llvm-libc++", "libs",
742+
"armeabi-v7a"),
743+
libgcc=os.path.join(android_ndk_path,
744+
"toolchains", "arm-linux-androideabi-4.8",
745+
"prebuilt", "linux-x86_64", "lib", "gcc",
746+
"arm-linux-androideabi", "4.8"))
747+
config.target_build_swift = (
748+
'%s -target %s -sdk %s %s -Xlinker -pie %s %s %s %s'
749+
% (config.swiftc, config.variant_triple, config.variant_sdk,
750+
android_linker_opt, resource_dir_opt, mcp_opt,
751+
config.swift_test_options, swift_execution_tests_extra_flags))
752+
config.target_swift_frontend = (
753+
'%s -frontend -target %s -sdk %s %s %s'
754+
% (config.swift, config.variant_triple, config.variant_sdk,
755+
android_linker_opt, resource_dir_opt))
756+
subst_target_swift_frontend_mock_sdk = config.target_swift_frontend
757+
subst_target_swift_frontend_mock_sdk_after = ""
758+
config.target_run = os.path.join(
759+
config.swift_src_root, 'utils', 'android', 'adb_test_runner.py')
760+
config.target_sil_opt = (
761+
'%s -target %s %s %s' %
762+
(config.sil_opt, config.variant_triple, resource_dir_opt, mcp_opt))
763+
config.target_swift_ide_test = (
764+
'%s -target %s %s %s %s' %
765+
(config.swift_ide_test, config.variant_triple, resource_dir_opt,
766+
mcp_opt, ccp_opt))
767+
subst_target_swift_ide_test_mock_sdk = config.target_swift_ide_test
768+
subst_target_swift_ide_test_mock_sdk_after = ""
769+
config.target_swiftc_driver = (
770+
"%s -target %s -sdk %s %s %s %s" %
771+
(config.swiftc, config.variant_triple, config.variant_sdk,
772+
android_linker_opt, resource_dir_opt, mcp_opt))
773+
config.target_swift_modulewrap = (
774+
'%s -modulewrap -target %s' %
775+
(config.swiftc, config.variant_triple))
776+
config.target_clang = (
777+
"clang++ -target %s %s" %
778+
(config.variant_triple, clang_mcp_opt))
779+
config.target_ld = (
780+
"ld -L%s" %
781+
(os.path.join(test_resource_dir, config.target_sdk_name)))
782+
# config.environment['ANDROID_NDK_HOME'] = os.getenv("ANDROID_NDK_HOME")
783+
# The Swift interpreter is not available when targeting Android.
784+
config.available_features.remove('swift_interpreter')
726785

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

test/lit.site.cfg.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ 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@"
2122

2223
config.coverage_mode = "@SWIFT_ANALYZE_CODE_COVERAGE@"
2324

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)