blob: 8e9690eb2d5ff88fdd28d2b5ab84e4c3c9a8930a [file] [log] [blame]
#
# Copyright (C) 2016 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.
#
from __future__ import absolute_import
from __future__ import print_function
import logging
import os
import subprocess
import sys
import adb # pylint: disable=import-error
import build.lib.build_support
import tests.filters
import tests.printers
import tests.ndk
import tests.testlib
import tests.util
def logger():
return logging.getLogger(__name__)
def get_device_abis(device):
# 64-bit devices list their ABIs differently than 32-bit devices. Check all
# the possible places for stashing ABI info and merge them.
abi_properties = [
'ro.product.cpu.abi',
'ro.product.cpu.abi2',
'ro.product.cpu.abilist',
]
abis = set()
properties = device.get_props()
for abi_prop in abi_properties:
if abi_prop in properties:
abis.update(properties[abi_prop].split(','))
return sorted(list(abis))
def check_adb_works_or_die(device, abi):
supported_abis = get_device_abis(device)
if abi is not None and abi not in supported_abis:
msg = ('The test device ({}) does not support the requested ABI '
'({}).\nSupported ABIs: {}'.format(device.serial, abi,
', '.join(supported_abis)))
sys.exit(msg)
def can_use_asan(device, abi, api, toolchain):
# ASAN is currently only supported for 32-bit ARM and x86...
if not abi.startswith('armeabi') and not abi == 'x86':
logger().info('Cannot use ASAN: unsupported ABI (%s)', abi)
return False
# From non-Windows (asan_device_setup is a shell script)...
if os.name == 'nt':
logger().info('Cannot use ASAN: Windows is not supported')
return False
# On KitKat and newer...
if api < 19:
logger().info('Cannot use ASAN: device is too old '
'(is android-%s, minimum android-19)', api)
return False
# When using clang...
if toolchain != 'clang':
logger().info('Cannot use ASAN: GCC is not supprted')
return False
# On rooted devices.
if int(device.get_prop('ro.debuggable')) == 0:
logger().info('Cannot use ASAN: device must be rooted')
return False
return True
def asan_device_setup():
path = os.path.join(
os.environ['NDK'], 'toolchains', 'llvm', 'prebuilt',
tests.ndk.get_host_tag(), 'bin', 'asan_device_setup')
try:
# Don't want to use check_call because we want to keep this quiet
# unless there's a problem.
subprocess.check_output([path], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as ex:
print(ex.output)
raise ex
class ResultStats(object):
def __init__(self, suites, results):
self.num_tests = sum(len(s) for s in results.values())
zero_stats = {'pass': 0, 'skip': 0, 'fail': 0}
self.global_stats = dict(zero_stats)
self.suite_stats = {suite: dict(zero_stats) for suite in suites}
self._analyze_results(results)
def _analyze_results(self, results):
for suite, test_results in results.items():
for result in test_results:
if result.failed():
self.suite_stats[suite]['fail'] += 1
self.global_stats['fail'] += 1
elif result.passed():
self.suite_stats[suite]['pass'] += 1
self.global_stats['pass'] += 1
else:
self.suite_stats[suite]['skip'] += 1
self.global_stats['skip'] += 1
def run_single_configuration(ndk_path, out_dir, printer, abi, toolchain,
build_api_level=None, verbose_build=False,
suites=None, test_filter=None,
device_serial=None, skip_run=False):
"""Runs all the tests for the given configuration.
Sets up the necessary build flags and environment, checks that the device
under test is in working order and performs device setup (if running device
tests), and finally runs the tests for the selected suites.
Args:
ndk_path: Absolute path the the NDK being tested.
out_dir: Directory to use when building tests.
printer: Instance of printers.Printer that will be used to print test
results.
abi: ABI to test against.
toolchain: Toolchain to build with.
build_api_level: API level to build against. If None, will default to
the value set in the test's Application.mk, or ndk-build's default.
verbose_build: Show verbose output from ndk-build and cmake.
suites: Set of strings denoting which test suites to run. Possible
values are 'awk', 'build', and 'device'. If None, will run all
suites.
test_filter: Filter string for selecting a subset of tests.
device_serial: Serial number of the device to use for device tests. If
none, will try to find a device from ANDROID_SERIAL or a unique
attached device.
skip_run: Skip running the tests; just build. Useful for post-build
steps if CI doesn't have the device available.
Returns:
Tuple of (result, details).
result is True if all tests completed successfully, False if there were
failures.
details is the dict returned by tests.TestRunner: {
"suite_name": [
tests.TestResult,
...
],
...
}
"""
if suites is None:
suites = ('awk', 'build', 'device')
os.chdir(os.path.dirname(os.path.realpath(__file__)))
os.environ['NDK'] = ndk_path
ndk_build_flags = []
cmake_flags = []
if verbose_build:
# Don't decrease our log level.
root_logger = logging.getLogger()
if root_logger.getEffectiveLevel() != logging.DEBUG:
root_logger.setLevel(logging.INFO)
ndk_build_flags.append('V=1')
cmake_flags.append('-DCMAKE_VERBOSE_MAKEFILE=ON')
force_pie = False
# Do this early so we find any device issues now rather than after we've
# run all the build tests.
if 'device' in suites and not skip_run:
device = adb.get_device(device_serial)
check_adb_works_or_die(device, abi)
device_api_level = int(device.get_prop('ro.build.version.sdk'))
# PIE is required in L. All of the device tests are written toward the
# ndk-build defaults, so we need to inform the build that we need PIE
# if we're running on a newer device.
if device_api_level >= 21:
force_pie = True
ndk_build_flags.append('APP_PIE=true')
cmake_flags.append('-DANDROID_PIE=TRUE')
os.environ['ANDROID_SERIAL'] = device.serial
if can_use_asan(device, abi, device_api_level, toolchain):
asan_device_setup()
# Do this as part of initialization rather than with a `mkdir -p` later
# because Gingerbread didn't actually support -p :(
device.shell_nocheck(['rm -r /data/local/tmp/ndk-tests 2>&1'])
device.shell(['mkdir /data/local/tmp/ndk-tests 2>&1'])
device.shell_nocheck(['rm -r /data/local/tmp/cmake-tests 2>&1'])
device.shell(['mkdir /data/local/tmp/cmake-tests 2>&1'])
elif skip_run:
# We need to fake these if we're skipping running the tests. Set device
# to None so any attempt to interact with it will raise an error, and
# set the API level to the minimum supported by the ABI so
# test_config.py checks still behave as expected.
device = None
device_api_level = build.lib.build_support.minimum_platform_level(abi)
runner = tests.testlib.TestRunner(printer)
if 'awk' in suites:
awk_scanner = tests.testlib.AwkTestScanner()
runner.add_suite('awk', 'awk', awk_scanner)
if 'build' in suites:
build_scanner = tests.testlib.BuildTestScanner()
build_scanner.add_build_configuration(
abi, build_api_level, toolchain, force_pie, verbose_build)
runner.add_suite('build', 'build', build_scanner)
if 'device' in suites:
device_scanner = tests.testlib.DeviceTestScanner()
device_scanner.add_device_configuration(
abi, build_api_level, toolchain, force_pie, verbose_build, device,
device_api_level, skip_run)
runner.add_suite('device', 'device', device_scanner)
test_filters = tests.filters.TestFilter.from_string(test_filter)
results = runner.run(out_dir, test_filters)
stats = ResultStats(suites, results)
printer.print_summary(results, stats)
return stats.global_stats['fail'] == 0, results
def run_tests(ndk_path, device, abi, toolchain, out_dir, log_dir, test_filter):
print('Running {} {} tests for {}... '.format(toolchain, abi, device),
end='')
sys.stdout.flush()
toolchain_name = 'gcc' if toolchain == '4.9' else toolchain
log_file_name = '{}-{}-{}.log'.format(toolchain_name, abi, device.version)
with open(os.path.join(log_dir, log_file_name), 'w') as log_file:
printer = tests.printers.FilePrinter(log_file)
good, details = run_single_configuration(
ndk_path, out_dir, printer, abi, toolchain,
device_serial=device.serial, test_filter=test_filter)
print('PASS' if good else 'FAIL')
return good, details
def run_for_fleet(ndk_path, fleet, out_dir, log_dir, test_filter,
use_color=False):
# Note that we are duplicating some testing here.
#
# * The awk tests only need to be run once because they do not vary by
# configuration.
# * The build tests only vary per-device by the PIE configuration, so we
# only need to run them twice per ABI/toolchain.
# * The build tests are already run as a part of the build process.
results = []
details = {}
good = True
for version in fleet.get_versions():
details[version] = {}
for abi in fleet.get_abis(version):
details[version][abi] = {}
device = fleet.get_device(version, abi)
for toolchain in ('clang', '4.9'):
if device is None:
results.append('android-{} {} {}: {}'.format(
version, abi, toolchain, 'SKIP'))
continue
details[version][abi][toolchain] = None
result, run_details = run_tests(
ndk_path, device, abi, toolchain, out_dir, log_dir,
test_filter)
pass_label = tests.util.maybe_color('PASS', 'green', use_color)
fail_label = tests.util.maybe_color('FAIL', 'red', use_color)
results.append('android-{} {} {}: {}'.format(
version, abi, toolchain,
pass_label if result else fail_label))
details[version][abi][toolchain] = run_details
if not result:
good = False
print('\n'.join(results))
return good, details