blob: 07c11cedf1a4788037725b1a097bc4e281fab638 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright (C) 2015 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Runs all NDK tests.
TODO: Handle default ABI case.
The old test script would build and test all ABIs listed in APP_ABI in a single
invocation if an explicit ABI was not given. Currently this will fall down over
that case. I've written most of the code to handle this, but I decided to
factor that for-each up several levels because it was going to make the device
tests rather messy and I haven't finished doing that yet.
TODO: Handle explicit test lists from command line.
The old test runner allowed specifying an exact list of tests to run with
--tests. That seems like a useful thing to keep around, but I haven't ported it
yet.
"""
from __future__ import print_function
import argparse
import contextlib
import distutils.spawn
import functools
import glob
import inspect
import multiprocessing
import os
import re
import shutil
import subprocess
import sys
import adb
import ndk
DEV_NULL = open(os.devnull, 'wb')
SUPPORTED_ABIS = (
'armeabi',
'armeabi-v7a',
'arm64-v8a',
'mips',
'mips64',
'x86',
'x86_64',
)
# TODO(danalbert): How much time do we actually save by not running these?
LONG_TESTS = (
'prebuild-stlport',
'test-stlport',
'test-gnustl-full',
'test-stlport_shared-exception',
'test-stlport_static-exception',
'test-gnustl_shared-exception-full',
'test-gnustl_static-exception-full',
'test-googletest-full',
'test-libc++-shared-full',
'test-libc++-static-full',
)
@contextlib.contextmanager
def cd(path):
curdir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(curdir)
def get_jobs_arg():
return '-j{}'.format(multiprocessing.cpu_count() * 2)
def color_string(string, color):
colors = {
'green': '\033[92m',
'red': '\033[91m',
'yellow': '\033[93m',
}
end_color = '\033[0m'
return colors[color] + string + end_color
class TestResult(object):
def __init__(self, test_name, passed):
self.passed = passed
self.test_name = test_name
def __repr__(self):
return self.to_string(colored=False)
def to_string(self, colored=False):
raise NotImplementedError
class Failure(TestResult):
def __init__(self, test_name, message):
super(Failure, self).__init__(test_name, passed=False)
self.message = message
def to_string(self, colored=False):
label = color_string('FAIL', 'red') if colored else 'FAIL'
return '{} {}: {}'.format(label, self.test_name, self.message)
class Success(TestResult):
def __init__(self, test_name):
super(Success, self).__init__(test_name, passed=True)
def to_string(self, colored=False):
label = color_string('PASS', 'green') if colored else 'PASS'
return '{} {}'.format(label, self.test_name)
class Skipped(TestResult):
def __init__(self, test_name, reason):
super(Skipped, self).__init__(test_name, passed=False)
self.reason = reason
def to_string(self, colored=False):
label = color_string('SKIP', 'yellow') if colored else 'SKIP'
return '{} {}: {}'.format(label, self.test_name, self.reason)
def test_is_disabled(test_dir, platform):
disable_file = os.path.join(test_dir, 'BROKEN_BUILD')
if os.path.isfile(disable_file):
if os.stat(disable_file).st_size == 0:
return True
# This might look like clang-3.6 and gcc-3.6 would overlap (not a
# problem today, but maybe when we hit clang-4.9), but clang is
# actually written as clang3.6 (with no hypen), so toolchain_version
# will end up being 'clang3.6'.
toolchain = ndk.get_build_var(test_dir, 'TARGET_TOOLCHAIN')
toolchain_version = toolchain.split('-')[-1]
with open(disable_file) as f:
contents = f.read()
broken_configs = re.split(r'\s+', contents)
if toolchain_version in broken_configs:
return True
if platform is not None and platform in broken_configs:
return True
return False
def run_is_disabled(test_case, test_dir):
"""Returns True if the test case is disabled.
There is no strict format for the BROKEN_RUN file; test cases are disabled
if their basename appears anywhere in the file.
"""
disable_file = os.path.join(test_dir, 'BROKEN_RUN')
if not os.path.exists(disable_file):
return False
return subprocess.call(['grep', '-qw', test_case, disable_file]) == 0
def should_skip_test(test_dir, abi, platform):
if test_is_disabled(test_dir, platform):
return 'disabled'
if abi is not None:
app_abi = ndk.get_build_var(test_dir, 'APP_ABI')
supported_abis = ndk.expand_app_abi(app_abi)
if abi not in supported_abis:
abi_string = ', '.join(supported_abis)
return 'incompatible ABI (requires {})'.format(abi_string)
return None
def run_awk_test_case(out_dir, test_name, script, test_case, golden_out_path):
out_path = os.path.join(out_dir, os.path.basename(golden_out_path))
with open(test_case, 'r') as test_in, open(out_path, 'w') as out_file:
print('awk -f {} < {} > {}'.format(script, test_case, out_path))
rc = subprocess.call(['awk', '-f', script], stdin=test_in,
stdout=out_file)
if rc != 0:
return Failure(test_name, 'awk failed')
rc = subprocess.call(['cmp', out_path, golden_out_path], stdout=DEV_NULL,
stderr=DEV_NULL)
if rc == 0:
return Success(test_name)
else:
p = subprocess.Popen(['diff', '-buN', out_path, golden_out_path],
stdout=subprocess.PIPE, stderr=DEV_NULL)
out, _ = p.communicate()
if p.returncode != 0:
raise RuntimeError('Could not generate diff')
message = 'output does not match expected:\n\n' + out
return Failure(test_name, message)
def run_awk_test(out_dir, test_dir):
test_name = '{}.awk'.format(os.path.basename(test_dir))
script = os.path.join(ndk.NDK_ROOT, 'build/awk', test_name)
if not os.path.isfile(script):
return [Failure(test_name, 'missing test script: {}'.format(script))]
results = []
for test_case in glob.glob(os.path.join(test_dir, '*.in')):
golden_path = re.sub(r'\.in$', '.out', test_case)
if not os.path.isfile(golden_path):
results.append(Failure(test_name,
'missing output: {}'.format(golden_path)))
results.append(run_awk_test_case(out_dir, test_name, script, test_case,
golden_path))
return results
def prep_build_dir(src_dir, out_dir):
if os.path.exists(out_dir):
shutil.rmtree(out_dir)
shutil.copytree(src_dir, out_dir)
def run_build_sh_test(test_name, build_dir, test_dir, build_flags, abi,
platform):
android_mk = os.path.join(test_dir, 'jni/Android.mk')
application_mk = os.path.join(test_dir, 'jni/Application.mk')
if os.path.isfile(android_mk) and os.path.isfile(application_mk):
result = subprocess.call(['grep', '-q', 'import-module', android_mk])
if result != 0:
reason = should_skip_test(test_dir, abi, platform)
if reason is not None:
return Skipped(test_name, reason)
prep_build_dir(test_dir, build_dir)
with cd(build_dir):
build_cmd = ['sh', 'build.sh', get_jobs_arg()] + build_flags
if subprocess.call(build_cmd) == 0:
return Success(test_name)
else:
return Failure(test_name, 'build failed')
def run_ndk_build_test(test_name, build_dir, test_dir, build_flags, abi,
platform):
reason = should_skip_test(test_dir, abi, platform)
if reason is not None:
return Skipped(test_name, reason)
prep_build_dir(test_dir, build_dir)
with cd(build_dir):
rc = ndk.build(build_flags + [get_jobs_arg()])
expect_failure = os.path.isfile(
os.path.join(test_dir, 'BUILD_SHOULD_FAIL'))
if rc == 0 and expect_failure:
return Failure(test_name, 'build should have failed')
elif rc != 0 and not expect_failure:
return Failure(test_name, 'build failed')
return Success(test_name)
def run_build_test(out_dir, test_dir, build_flags, abi, platform):
test_name = os.path.basename(test_dir)
print('Running build test: {}'.format(test_name))
build_dir = os.path.join(out_dir, test_name)
if os.path.isfile(os.path.join(test_dir, 'build.sh')):
return [run_build_sh_test(test_name, build_dir, test_dir, build_flags,
abi, platform)]
else:
return [run_ndk_build_test(test_name, build_dir, test_dir, build_flags,
abi, platform)]
def copy_test_to_device(build_dir, device_dir, abi):
abi_dir = os.path.join(build_dir, 'libs', abi)
if not os.path.isdir(abi_dir):
raise RuntimeError('No libraries for {}'.format(abi))
test_cases = []
for test_file in os.listdir(abi_dir):
if test_file in ('gdbserver', 'gdb.setup'):
continue
if not test_file.endswith('.so'):
test_cases.append(test_file)
# TODO(danalbert): Libs with the same name will clobber each other.
# This was the case with the old shell based script too. I'm trying not
# to change too much in the translation.
lib_path = os.path.join(abi_dir, test_file)
adb.push(lib_path, device_dir)
# TODO(danalbert): Sync data.
# The libc++ tests contain a DATA file that lists test names and their
# dependencies on file system data. These files need to be copied to
# the device.
if len(test_cases) == 0:
raise RuntimeError('Could not find any test executables.')
return test_cases
def run_device_test(out_dir, test_dir, build_flags, abi, platform):
test_name = os.path.basename(test_dir)
build_dir = os.path.join(out_dir, test_name)
build_result = run_ndk_build_test(test_name, build_dir, test_dir,
build_flags, abi, platform)
if not build_result.passed:
return [build_result]
device_dir = os.path.join('/data/local/tmp/ndk-tests', test_name)
result, out = adb.shell('mkdir -p {}'.format(device_dir))
if result != 0:
raise RuntimeError('mkdir failed:\n' + '\n'.join(out))
results = []
try:
test_cases = copy_test_to_device(build_dir, device_dir, abi)
for case in test_cases:
case_name = '.'.join([test_name, case])
if run_is_disabled(case, test_dir):
results.append(Skipped(case_name, 'run disabled'))
continue
cmd = 'cd {} && LD_LIBRARY_PATH={} ./{}'.format(
device_dir, device_dir, case)
result, out = adb.shell(cmd)
if result == 0:
results.append(Success(case_name))
else:
results.append(Failure(case_name, '\n'.join(out)))
return results
finally:
adb.shell('rm -rf {}'.format(device_dir))
def run_tests(out_dir, test_dir, test_func):
results = []
for dentry in os.listdir(test_dir):
path = os.path.join(test_dir, dentry)
if os.path.isdir(path):
try:
results.extend(test_func(out_dir, path))
except RuntimeError as ex:
results.append(Failure(os.path.basename(dentry), ex))
return results
def get_test_device():
if distutils.spawn.find_executable('adb') is None:
raise RuntimeError('Could not find adb.')
p = subprocess.Popen(['adb', 'devices'], stdout=subprocess.PIPE)
out, _ = p.communicate()
if p.returncode != 0:
raise RuntimeError('Failed to get list of devices from adb.')
# The first line of `adb devices` just says "List of attached devices", so
# skip that.
devices = []
for line in out.split('\n')[1:]:
if not line.strip():
continue
if 'offline' in line:
continue
serial, _ = re.split(r'\s+', line, maxsplit=1)
devices.append(serial)
if len(devices) == 0:
raise RuntimeError('No devices detected.')
device = os.getenv('ANDROID_SERIAL')
if device is None and len(devices) == 1:
device = devices[0]
if device is not None and device not in devices:
raise RuntimeError('Device {} is not available.'.format(device))
# TODO(danalbert): Handle running against multiple devices in one pass.
if len(devices) > 1 and device is None:
raise RuntimeError('Multiple devices detected and ANDROID_SERIAL not '
'set. Cannot continue.')
return device
def get_device_abis():
abis = [adb.get_prop('ro.product.cpu.abi')]
abi2 = adb.get_prop('ro.product.cpu.abi2')
if abi2 is not None:
abis.append(abi2)
return abis
def check_adb_works_or_die(abi):
# TODO(danalbert): Check that we can do anything with the device.
try:
device = get_test_device()
except RuntimeError as ex:
sys.exit('Error: {}'.format(ex))
if abi is not None and abi not in get_device_abis():
sys.exit('The test device ({}) does not support the requested ABI '
'({})'.format(device, abi))
def is_valid_platform_version(version_string):
match = re.match(r'^android-(\d+)$', version_string)
if not match:
return False
# We don't support anything before Gingerbread.
version = int(match.group(1))
return version >= 9
def android_platform_version(version_string):
if is_valid_platform_version(version_string):
return version_string
else:
raise argparse.ArgumentTypeError(
'Platform version must match the format "android-VERSION", where '
'VERSION >= 9.')
class ArgParser(argparse.ArgumentParser):
def __init__(self):
super(ArgParser, self).__init__(
description=inspect.getdoc(sys.modules[__name__]))
self.add_argument(
'--abi', default=None, choices=SUPPORTED_ABIS,
help=('Run tests against the specified ABI. Defaults to the '
'contents of APP_ABI in jni/Application.mk'))
self.add_argument(
'--platform', default=None, type=android_platform_version,
help=('Run tests against the specified platform version. Defaults '
'to the contents of APP_PLATFORM in jni/Application.mk'))
self.add_argument(
'--show-commands', action='store_true',
help='Show build commands for each test.')
self.add_argument(
'--suite', default=None,
choices=('awk', 'build', 'device', 'samples'),
help=('Run only the chosen test suite.'))
self.add_argument(
'--quick', action='store_true', help='Skip long running tests.')
self.add_argument(
'--show-all', action='store_true',
help='Show all test results, not just failures.')
def main():
os.chdir(os.path.dirname(os.path.realpath(__file__)))
args = ArgParser().parse_args()
ndk_build_flags = []
if args.abi is not None:
ndk_build_flags.append('APP_ABI={}'.format(args.abi))
if args.platform is not None:
ndk_build_flags.append('APP_PLATFORM={}'.format(args.platform))
if args.show_commands:
ndk_build_flags.append('V=1')
if not os.path.exists(os.path.join('../build/tools/prebuilt-common.sh')):
sys.exit('Error: Not run from a valid NDK.')
out_dir = 'out'
if os.path.exists(out_dir):
shutil.rmtree(out_dir)
os.makedirs(out_dir)
my_run_build_test = functools.partial(run_build_test,
build_flags=ndk_build_flags,
abi=args.abi,
platform=args.platform)
my_run_device_test = functools.partial(run_device_test,
build_flags=ndk_build_flags,
abi=args.abi,
platform=args.platform)
suites = ['awk', 'build', 'device', 'samples']
if args.suite:
suites = [args.suite]
# Do this early so we find any device issues now rather than after we've
# run all the build tests.
if 'device' in suites:
check_adb_works_or_die(args.abi)
os.environ['ANDROID_SERIAL'] = get_test_device()
results = {suite: [] for suite in suites}
if 'awk' in suites:
results['awk'] = run_tests(out_dir, 'awk', run_awk_test)
if 'build' in suites:
results['build'] = run_tests(out_dir, 'build', my_run_build_test)
if 'samples' in suites:
results['samples'] = run_tests(out_dir, '../samples',
my_run_build_test)
if 'device' in suites:
results['device'] = run_tests(out_dir, 'device', my_run_device_test)
num_tests = sum(len(s) for s in results.values())
zero_stats = {'pass': 0, 'skip': 0, 'fail': 0}
stats = {suite: dict(zero_stats) for suite in suites}
global_stats = dict(zero_stats)
for suite, test_results in results.items():
for result in test_results:
if isinstance(result, Failure):
stats[suite]['fail'] += 1
global_stats['fail'] += 1
if isinstance(result, Skipped):
stats[suite]['skip'] += 1
global_stats['skip'] += 1
if isinstance(result, Success):
stats[suite]['pass'] += 1
global_stats['pass'] += 1
def format_stats(num_tests, stats, use_color):
return '{pl} {p}/{t} {fl} {f}/{t} {sl} {s}/{t}'.format(
pl=color_string('PASS', 'green') if use_color else 'PASS',
fl=color_string('FAIL', 'red') if use_color else 'FAIL',
sl=color_string('SKIP', 'yellow') if use_color else 'SKIP',
p=stats['pass'], f=stats['fail'],
s=stats['skip'], t=num_tests)
use_color = sys.stdin.isatty()
print()
print(format_stats(num_tests, global_stats, use_color))
for suite, test_results in results.items():
stats_str = format_stats(len(test_results), stats[suite], use_color)
print()
print('{}: {}'.format(suite, stats_str))
for result in test_results:
if args.show_all or isinstance(result, Failure):
print(result.to_string(colored=use_color))
sys.exit(global_stats['fail'] == 0)
if __name__ == '__main__':
main()