-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Android] Add testing support #1714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,8 @@ config.variant_sdk = "@VARIANT_SDK@" | |
config.variant_suffix = "@VARIANT_SUFFIX@" | ||
config.swiftlib_dir = "@LIT_SWIFTLIB_DIR@" | ||
config.darwin_xcrun_toolchain = "@SWIFT_DARWIN_XCRUN_TOOLCHAIN@" | ||
config.android_ndk_path = "@SWIFT_ANDROID_NDK_PATH@" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to also add this to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! Thanks :) |
||
config.android_ndk_gcc_version = "@SWIFT_ANDROID_NDK_GCC_VERSION@" | ||
|
||
config.coverage_mode = "@SWIFT_ANALYZE_CODE_COVERAGE@" | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
# adb/commands.py - Run executables on an Android device -*- python -*- | ||
# | ||
# This source file is part of the Swift.org open source project | ||
# | ||
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors | ||
# Licensed under Apache License v2.0 with Runtime Library Exception | ||
# | ||
# See http://swift.org/LICENSE.txt for license information | ||
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
# | ||
# ---------------------------------------------------------------------------- | ||
# | ||
# Push executables to an Android device and run them, capturing their output | ||
# and exit code. | ||
# | ||
# ---------------------------------------------------------------------------- | ||
|
||
from __future__ import print_function | ||
|
||
import subprocess | ||
import tempfile | ||
import uuid | ||
|
||
|
||
# A temporary directory on the Android device. | ||
DEVICE_TEMP_DIR = '/data/local/tmp' | ||
|
||
|
||
def shell(args): | ||
""" | ||
Execute 'adb shell' with the given arguments. | ||
|
||
Raise an exception if 'adb shell' returns a non-zero exit code. | ||
Note that this only occurs if communication with the connected device | ||
fails, not if the command run on the device fails. | ||
""" | ||
return subprocess.check_output(['adb', 'shell'] + args) | ||
|
||
|
||
def rmdir(path): | ||
"""Remove all files in the device directory at `path`.""" | ||
shell(['rm', '-rf', '{}/*'.format(path)]) | ||
|
||
|
||
def push(local_path, device_path): | ||
"""Move the file at the given local path to the path on the device.""" | ||
return subprocess.check_output(['adb', 'push', local_path, device_path], | ||
stderr=subprocess.STDOUT).strip() | ||
|
||
|
||
def reboot(): | ||
"""Reboot the connected Android device, waiting for it to return online.""" | ||
subprocess.check_call(['adb', 'reboot']) | ||
subprocess.check_call(['adb', 'wait-for-device']) | ||
|
||
|
||
def _create_executable_on_device(device_path, contents): | ||
_, tmp = tempfile.mkstemp() | ||
with open(tmp, 'w') as f: | ||
f.write(contents) | ||
push(tmp, device_path) | ||
shell(['chmod', '755', device_path]) | ||
|
||
|
||
def execute_on_device(executable_path, executable_arguments): | ||
""" | ||
Run an executable on an Android device. | ||
|
||
Push an executable at the given 'executable_path' to an Android device, | ||
then execute that executable on the device, passing any additional | ||
'executable_arguments'. Return 0 if the executable succeeded when run on | ||
device, and 1 otherwise. | ||
|
||
This function is not as simple as calling 'adb shell', for two reasons: | ||
|
||
1. 'adb shell' can only take input up to a certain length, so it fails for | ||
long executable names or when a large amount of arguments are passed to | ||
the executable. This function attempts to limit the size of any string | ||
passed to 'adb shell'. | ||
2. 'adb shell' ignores the exit code of any command it runs. This function | ||
therefore uses its own mechanisms to determine whether the executable | ||
had a successful exit code when run on device. | ||
""" | ||
# We'll be running the executable in a temporary directory in | ||
# /data/local/tmp. `adb shell` has trouble with commands that | ||
# exceed a certain length, so to err on the safe side we only | ||
# use the first 10 characters of the UUID. | ||
uuid_dir = '{}/{}'.format(DEVICE_TEMP_DIR, str(uuid.uuid4())[:10]) | ||
shell(['mkdir', '-p', uuid_dir]) | ||
|
||
# `adb` can only handle commands under a certain length. No matter what the | ||
# original executable's name, on device we call it `__executable`. | ||
executable = '{}/__executable'.format(uuid_dir) | ||
push(executable_path, executable) | ||
|
||
# When running the executable on the device, we need to pass it the same | ||
# arguments, as well as specify the correct LD_LIBRARY_PATH. Save these | ||
# to a file we can easily call multiple times. | ||
executable_with_args = '{}/__executable_with_args'.format(uuid_dir) | ||
_create_executable_on_device( | ||
executable_with_args, | ||
'LD_LIBRARY_PATH={uuid_dir}:{tmp_dir} ' | ||
'{executable} {executable_arguments}'.format( | ||
uuid_dir=uuid_dir, | ||
tmp_dir=DEVICE_TEMP_DIR, | ||
executable=executable, | ||
executable_arguments=' '.join(executable_arguments))) | ||
|
||
# Write the output from the test executable to a file named '__stdout', and | ||
# if the test executable succeeds, write 'SUCCEEDED' to a file | ||
# named '__succeeded'. We do this because `adb shell` does not report | ||
# the exit code of the command it executes on the device, so instead we | ||
# check the '__succeeded' file for our string. | ||
executable_stdout = '{}/__stdout'.format(uuid_dir) | ||
succeeded_token = 'SUCCEEDED' | ||
executable_succeeded = '{}/__succeeded'.format(uuid_dir) | ||
executable_piped = '{}/__executable_piped'.format(uuid_dir) | ||
_create_executable_on_device( | ||
executable_piped, | ||
'{executable_with_args} > {executable_stdout} && ' | ||
'echo "{succeeded_token}" > {executable_succeeded}'.format( | ||
executable_with_args=executable_with_args, | ||
executable_stdout=executable_stdout, | ||
succeeded_token=succeeded_token, | ||
executable_succeeded=executable_succeeded)) | ||
|
||
# We've pushed everything we need to the device. | ||
# Now execute the wrapper script. | ||
shell([executable_piped]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if the host detects a timeout and kills the python script? Would we clean up the device? We want to kill the process so that in a CI environment the next job will get to use a clean device. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This script does a few things:
On top of these steps, I agree that we want to use a clean device for each test run. A few questions, though:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This would make sense.
I think so. In fact, you can remove all of them in But there also needs to be logic that will clean up leftover processes, which might be stuck consuming 100% CPU. One simple way to do that would be to reboot the device from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! Rebooting the phone adds something like 45 seconds to the time it takes to run the test suite, which seems tolerable. |
||
|
||
# Grab the results of running the executable on device. | ||
stdout = shell(['cat', executable_stdout]) | ||
exitcode = shell(['cat', executable_succeeded]) | ||
if not exitcode.startswith(succeeded_token): | ||
debug_command = '$ adb shell {}'.format(executable_with_args) | ||
print('Executable exited with a non-zero code on the Android device.\n' | ||
'Device stdout:\n' | ||
'{stdout}\n' | ||
'To debug, run:\n' | ||
'{debug_command}\n'.format( | ||
stdout=stdout, | ||
debug_command=debug_command)) | ||
|
||
# Exit early so that the output isn't passed to FileCheck, nor are any | ||
# temporary directories removed; this allows the user to re-run | ||
# the executable on the device. | ||
return 1 | ||
|
||
print(stdout) | ||
|
||
shell(['rm', '-rf', uuid_dir]) | ||
return 0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
#!/usr/bin/env python | ||
# adb_reboot.py - Reboots and cleans an Android device. -*- python -*- | ||
# | ||
# This source file is part of the Swift.org open source project | ||
# | ||
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors | ||
# Licensed under Apache License v2.0 with Runtime Library Exception | ||
# | ||
# See http://swift.org/LICENSE.txt for license information | ||
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
|
||
from adb.commands import DEVICE_TEMP_DIR, reboot, rmdir | ||
|
||
|
||
if __name__ == '__main__': | ||
reboot() | ||
rmdir(DEVICE_TEMP_DIR) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
#!/usr/bin/env python | ||
# adb_push_build_products.py - Push libraries to Android device -*- python -*- | ||
# | ||
# This source file is part of the Swift.org open source project | ||
# | ||
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors | ||
# Licensed under Apache License v2.0 with Runtime Library Exception | ||
# | ||
# See http://swift.org/LICENSE.txt for license information | ||
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
|
||
import sys | ||
|
||
from adb_push_built_products.main import main | ||
|
||
|
||
if __name__ == '__main__': | ||
sys.exit(main()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it just hangs instead. It uses stdin.