android: Make run_tests.py work on Android versions before 7.0 (N)

I took yesterday’s work and tried using it to run tests on a Nexus 4
running 5.1.1 (L), and absolutely nothing worked. The highlights:

 - There’s no /system/bin/mktemp.
 - There’s no /system/bin/env.
 - “adb shell” doesn’t know what the command’s exit status was.

While I’m in here, I’ll also make colored gtest output work, although it
won’t work on the normal Windows console which doesn’t understand ANSI
color codes. (It might work in Cygwin?)

Plus some bonus bloopers:
 - I was trying to catch an exception that isn’t even defined in Python
   2!
 - The part of the script that tells you what test it’s about to run had
   fallen into a conditional block, preventing it from being shown
   except when running end_to_end_test.py.

Bug: crashpad:30
Change-Id: I98fc410f90a2b4e91cb3cacb6a8decf2a8c2252b
Reviewed-on: https://chromium-review.googlesource.com/818125
Commit-Queue: Mark Mentovai <mark@chromium.org>
Reviewed-by: Scott Graham <scottmg@chromium.org>
This commit is contained in:
Mark Mentovai 2017-12-08 18:47:12 -05:00 committed by Commit Bot
parent 92e8bc4713
commit 0cc63d4a45

View File

@ -46,17 +46,15 @@ def _BinaryDirTargetOS(binary_dir):
# 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_path = os.path.join(binary_dir, 'build.ninja')
if os.path.exists(build_ninja_path):
with open(build_ninja_path) 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
@ -70,13 +68,95 @@ def _RunOnAndroidTarget(binary_dir, test, android_device):
'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')
print('This 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()
def _adb(*args):
# Flush all of this scripts 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)
def _adb_push(sources, destination):
args = list(sources)
args.append(destination)
_adb('push', *args)
def _adb_shell(command_args, env={}):
# Build a command to execute via “sh -c” instead of invoking it directly.
# Heres why:
#
# /system/bin/env isnt normally present prior to Android 6.0 (M), where
# toybox was introduced (Android platform/manifest 9a2c01e8450b). Instead,
# set environment variables by using the shells internal “export” command.
#
# adbd prior to Android 7.0 (N), and the adb client prior to SDK
# platform-tools version 24, dont know how to communicate a shell commands
# exit status. This was added in Android platform/system/core 606835ae5c4b).
# With older adb servers and clients, adb will “exit 0” indicating success
# even if the command failed on the device. This makes
# subprocess.check_call() semantics difficult to implement directly. As a
# workaround, have the device send the commands exit status over stdout and
# pick it back up in this function.
#
# Both workarounds are implemented by giving the device a simple script,
# which adbd will run as an “sh -c” argument.
adb_command = ['adb', '-s', android_device, 'shell']
script_commands = []
for k, v in env.items():
script_commands.append('export %s=%s' % (pipes.quote(k), pipes.quote(v)))
script_commands.extend([
' '.join(pipes.quote(x) for x in command_args),
'status=${?}',
'echo "status=${status}"',
'exit ${status}'])
adb_command.append('; '.join(script_commands))
child = subprocess.Popen(adb_command,
shell=IS_WINDOWS_HOST,
stdin=open(os.devnull),
stdout=subprocess.PIPE)
FINAL_LINE_RE = re.compile('status=(\d+)$')
final_line = None
while True:
# Use readline so that the test output appears “live” when running.
data = child.stdout.readline().decode('utf-8')
if data == '':
break
if final_line is not None:
# It wasnt really the final line.
print(final_line, end='')
final_line = None
if FINAL_LINE_RE.match(data.rstrip()):
final_line = data
else:
print(data, end='')
if final_line is None:
# Maybe there was some stderr output after the end of stdout. Old versions
# of adb, prior to when the exit status could be communicated, smush the
# two together.
raise subprocess.CalledProcessError(-1, adb_command)
status = int(FINAL_LINE_RE.match(final_line.rstrip()).group(1))
if status != 0:
raise subprocess.CalledProcessError(status, adb_command)
child.wait()
if child.returncode != 0:
raise subprocess.CalledProcessError(subprocess.returncode, adb_command)
# /system/bin/mktemp isnt normally present prior to Android 6.0 (M), where
# toybox was introduced (Android platform/manifest 9a2c01e8450b). Fake it with
# a host-generated name. This wont retry if the name is in use, but with 122
# bits of randomness, it should be OK. This uses “mkdir” instead of “mkdir -p”
# because the latter will not indicate failure if the directory already
# exists.
device_temp_dir = '/data/local/tmp/%s.%s' % (test, uuid.uuid4().hex)
_adb_shell(['mkdir', device_temp_dir])
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
@ -91,15 +171,6 @@ def _RunOnAndroidTarget(binary_dir, test, android_device):
elif test == 'crashpad_util_test':
test_data.append('util/net/testdata/')
def _adb(*args):
# Flush all of this scripts 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]
@ -117,28 +188,44 @@ def _RunOnAndroidTarget(binary_dir, test, android_device):
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 = ['mkdir', '-p']
adb_mkdir_command.extend(device_mkdirs)
_adb(*adb_mkdir_command)
_adb_shell(adb_mkdir_command)
# Push the test binary and any other build output to the device.
adb_push_command = ['push']
local_test_build_artifacts = []
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)
local_test_build_artifacts.append(os.path.join(binary_dir, artifact))
_adb_push(local_test_build_artifacts, device_out_dir)
# Push test data to the device.
for source_path in test_data:
_adb('push',
os.path.join(CRASHPAD_DIR, source_path),
_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))
# Run the test on the device. Pass the test data root in the environment.
#
# Because the test will not run with its standard output attached to a
# pseudo-terminal device, gtest will not normally enable colored output, so
# mimic gtests own logic for deciding whether to enable color by checking
# this scripts own standard output connection. The whitelist of TERM values
# comes from gtest googletest/src/gtest.cc
# testing::internal::ShouldUseColor().
env = {'CRASHPAD_TEST_DATA_ROOT': device_temp_dir}
gtest_color = os.environ.get('GTEST_COLOR')
if gtest_color in ('auto', None):
if (sys.stdout.isatty() and
os.environ.get('TERM') in
('xterm', 'xterm-color', 'xterm-256color', 'screen',
'screen-256color', 'tmux', 'tmux-256color', 'rxvt-unicode',
'rxvt-unicode-256color', 'linux', 'cygwin')):
gtest_color = 'yes'
else:
gtest_color = 'no'
env['GTEST_COLOR'] = gtest_color
_adb_shell([posixpath.join(device_out_dir, test)], env)
finally:
_adb('shell', 'rm', '-rf', device_temp_dir)
_adb_shell(['rm', '-rf', device_temp_dir])
def _GetFuchsiaSDKRoot():
@ -330,10 +417,10 @@ def main(args):
tests = [single_test]
for test in tests:
if test.endswith('.py'):
print('-' * 80)
print(test)
print('-' * 80)
if test.endswith('.py'):
subprocess.check_call(
[sys.executable, os.path.join(CRASHPAD_DIR, test), binary_dir])
else: