diff --git a/build/run_tests.py b/build/run_tests.py index 18cc9f53..67c3a03f 100755 --- a/build/run_tests.py +++ b/build/run_tests.py @@ -19,6 +19,8 @@ from __future__ import print_function import os import pipes +import posixpath +import re import subprocess import sys import uuid @@ -28,18 +30,120 @@ CRASHPAD_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), IS_WINDOWS_HOST = sys.platform.startswith('win') -def _GetFuchsiaSDKRoot(): - arch = 'mac-amd64' if sys.platform == 'darwin' else 'linux-amd64' - return os.path.join(CRASHPAD_DIR, 'third_party', 'fuchsia', 'sdk', arch) +def _BinaryDirTargetOS(binary_dir): + """Returns the apparent target OS of binary_dir, or None if none appear to be + explicitly specified.""" - -def _BinaryDirLooksLikeFuchsiaBuild(binary_dir): - """Checks whether the provided output directory targets Fuchsia.""" + # Look for a GN “target_os”. popen = subprocess.Popen( ['gn', 'args', binary_dir, '--list=target_os', '--short'], shell=IS_WINDOWS_HOST, stdout=subprocess.PIPE, stderr=open(os.devnull)) value = popen.communicate()[0] - return popen.returncode == 0 and 'target_os = "fuchsia"' in value + if popen.returncode == 0: + match = re.match('target_os = "(.*)"$', value.decode('utf-8')) + if match: + return match.group(1) + + # For GYP with Ninja, look for the appearance of “linux-android” in the path + # to ar. This path is configured by gyp_crashpad_android.py. + try: + with open(os.path.join(binary_dir, 'build.ninja')) as build_ninja_file: + build_ninja_content = build_ninja_file.read() + match = re.search('^ar = .+-linux-android(eabi)?-ar$', + build_ninja_content, + re.MULTILINE) + if match: + return 'android' + except FileNotFoundError: + # Ninja may not be in use. Assume the best. + pass + + return None + + +def _RunOnAndroidTarget(binary_dir, test, android_device): + local_test_path = os.path.join(binary_dir, test) + MAYBE_UNSUPPORTED_TESTS = ( + 'crashpad_client_test', + 'crashpad_handler_test', + 'crashpad_minidump_test', + 'crashpad_snapshot_test', + ) + if not os.path.exists(local_test_path) and test in MAYBE_UNSUPPORTED_TESTS: + print(test, 'is not present and may not be supported, skipping') + return + + device_temp_dir = subprocess.check_output( + ['adb', '-s', android_device, 'shell', + 'mktemp', '-d', '/data/local/tmp/%s.XXXXXXXX' % test], + shell=IS_WINDOWS_HOST).decode('utf-8').rstrip() + try: + # Specify test dependencies that must be pushed to the device. This could be + # determined automatically in a GN build, following the example used for + # Fuchsia. Since nothing like that exists for GYP, hard-code it for + # supported tests. + test_build_artifacts = [test] + test_data = ['test/test_paths_test_data_root.txt'] + + if test == 'crashpad_test_test': + test_build_artifacts.append( + 'crashpad_test_test_multiprocess_exec_test_child') + elif test == 'crashpad_util_test': + test_data.append('util/net/testdata/') + + def _adb(*args): + # Flush all of this script’s own buffered stdout output before running + # adb, which will likely produce its own output on stdout. + sys.stdout.flush() + + adb_command = ['adb', '-s', android_device] + adb_command.extend(args) + subprocess.check_call(adb_command, shell=IS_WINDOWS_HOST) + + # Establish the directory structure on the device. + device_out_dir = posixpath.join(device_temp_dir, 'out') + device_mkdirs = [device_out_dir] + for source_path in test_data: + # A trailing slash could reasonably mean to copy an entire directory, but + # will interfere with what’s needed from the path split. All parent + # directories of any source_path need to be be represented in + # device_mkdirs, but it’s important that no source_path itself wind up in + # device_mkdirs, even if source_path names a directory, because that would + # cause the “adb push” of the directory below to behave incorrectly. + if source_path.endswith(posixpath.sep): + source_path = source_path[:-1] + + device_source_path = posixpath.join(device_temp_dir, source_path) + device_mkdir = posixpath.split(device_source_path)[0] + if device_mkdir not in device_mkdirs: + device_mkdirs.append(device_mkdir) + adb_mkdir_command = ['shell', 'mkdir', '-p'] + adb_mkdir_command.extend(device_mkdirs) + _adb(*adb_mkdir_command) + + # Push the test binary and any other build output to the device. + adb_push_command = ['push'] + for artifact in test_build_artifacts: + adb_push_command.append(os.path.join(binary_dir, artifact)) + adb_push_command.append(device_out_dir) + _adb(*adb_push_command) + + # Push test data to the device. + for source_path in test_data: + _adb('push', + os.path.join(CRASHPAD_DIR, source_path), + posixpath.join(device_temp_dir, source_path)) + + # Run the test on the device. + _adb('shell', 'env', 'CRASHPAD_TEST_DATA_ROOT=' + device_temp_dir, + posixpath.join(device_out_dir, test)) + finally: + _adb('shell', 'rm', '-rf', device_temp_dir) + + +def _GetFuchsiaSDKRoot(): + arch = 'mac-amd64' if sys.platform == 'darwin' else 'linux-amd64' + return os.path.join(CRASHPAD_DIR, 'third_party', 'fuchsia', 'sdk', arch) def _GenerateFuchsiaRuntimeDepsFiles(binary_dir, tests): @@ -170,7 +274,9 @@ def main(args): if os.path.isdir(binary_dir_32): os.environ['CRASHPAD_TEST_32_BIT_OUTPUT'] = binary_dir_32 - is_fuchsia = _BinaryDirLooksLikeFuchsiaBuild(binary_dir) + target_os = _BinaryDirTargetOS(binary_dir) + is_android = target_os == 'android' + is_fuchsia = target_os == 'fuchsia' tests = [ 'crashpad_client_test', @@ -181,7 +287,26 @@ def main(args): 'crashpad_util_test', ] - if is_fuchsia: + if is_android: + android_device = os.environ.get('ANDROID_DEVICE') + if not android_device: + adb_devices = subprocess.check_output(['adb', 'devices'], + shell=IS_WINDOWS_HOST) + devices = [] + for line in adb_devices.splitlines(): + line = line.decode('utf-8') + if (line == 'List of devices attached' or + re.match('^\* daemon .+ \*$', line) or + line == ''): + continue + (device, ignore) = line.split('\t') + devices.append(device) + if len(devices) != 1: + print("Please set ANDROID_DEVICE to your device's id", file=sys.stderr) + return 2 + android_device = devices[0] + print('Using autodetected Android device:', android_device) + elif is_fuchsia: zircon_nodename = os.environ.get('ZIRCON_NODENAME') if not zircon_nodename: netls = os.path.join(_GetFuchsiaSDKRoot(), 'tools', 'netls') @@ -212,7 +337,9 @@ def main(args): subprocess.check_call( [sys.executable, os.path.join(CRASHPAD_DIR, test), binary_dir]) else: - if is_fuchsia: + if is_android: + _RunOnAndroidTarget(binary_dir, test, android_device) + elif is_fuchsia: _RunOnFuchsiaTarget(binary_dir, test, zircon_nodename) else: subprocess.check_call(os.path.join(binary_dir, test)) diff --git a/doc/developing.md b/doc/developing.md index d034b4d4..c8bb430d 100644 --- a/doc/developing.md +++ b/doc/developing.md @@ -210,35 +210,18 @@ Software Development Kit. ### Android -To test on Android, use [ADB (Android Debug -Bridge)](https://developer.android.com/studio/command-line/adb.html) to `adb -push` test executables and test data to a device or emulator, then use `adb -shell` to get a shell to run the test executables from. ADB is part of the -[Android SDK](https://developer.android.com/sdk/). Note that it is sufficient to -install just the command-line tools. The entire Android Studio IDE is not -necessary to obtain ADB. +To test on Android, [ADB (Android Debug +Bridge)](https://developer.android.com/studio/command-line/adb.html) from the +[Android SDK](https://developer.android.com/sdk/) must be in the `PATH`. Note +that it is sufficient to install just the command-line tools from the Android +SDK. The entire Android Studio IDE is not necessary to obtain ADB. -This example runs `crashpad_test_test` on a device. This test executable has a -run-time dependency on a second executable and a test data file, which are also -transferred to the device prior to running the test. - -``` -$ cd ~/crashpad/crashpad -$ adb push out/android_arm64_api21/out/Debug/crashpad_test_test /data/local/tmp/ -[100%] /data/local/tmp/crashpad_test_test -$ adb push \ - out/android_arm64_api21/out/Debug/crashpad_test_test_multiprocess_exec_test_child \ - /data/local/tmp/ -[100%] /data/local/tmp/crashpad_test_test_multiprocess_exec_test_child -$ adb shell mkdir -p /data/local/tmp/crashpad_test_data_root/test -$ adb push test/test_paths_test_data_root.txt \ - /data/local/tmp/crashpad_test_data_root/test/ -[100%] /data/local/tmp/crashpad_test_data_root/test/test_paths_test_data_root.txt -$ adb shell -device:/ $ cd /data/local/tmp -device:/data/local/tmp $ CRASHPAD_TEST_DATA_ROOT=crashpad_test_data_root \ - ./crashpad_test_test -``` +When asked to test an Android build directory, `run_tests.py` will detect a +single connected Android device (including an emulator). If multiple devices are +connected, one may be chosen explicitly with the `ANDROID_DEVICE` environment +variable. `run_tests.py` will upload test executables and data to a temporary +location on the detected or selected device, run them, and clean up after itself +when done. ## Contributing