blob: ea928b4093eaa90a09cdc95962d9249d53b5844e [file] [log] [blame]
#!/usr/bin/env python
#
# 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.
#
"""Runs the test suite over a set of devices."""
from __future__ import print_function
import argparse
import distutils.spawn
import os
import re
import shutil
import signal
import site
import subprocess
import sys
import tempfile
import ndk.debug
THIS_DIR = os.path.realpath(os.path.dirname(__file__))
class Device(object):
def __init__(self, serial, name, version, build_id, abis, is_emulator):
self.serial = serial
self.name = name
self.version = version
self.build_id = build_id
self.abis = abis
self.is_emulator = is_emulator
def __str__(self):
return 'android-{} {} {} {}'.format(
self.version, self.name, self.serial, self.build_id)
class DeviceFleet(object):
def __init__(self):
self.devices = {
10: {
'armeabi': None,
'armeabi-v7a': None,
},
16: {
'armeabi': None,
'armeabi-v7a': None,
'x86': None,
},
23: {
'armeabi': None,
'armeabi-v7a': None,
'arm64-v8a': None,
'x86': None,
'x86_64': None,
},
}
def add_device(self, device):
if device.version not in self.devices:
print('Ignoring device for unwanted API level: {}'.format(device))
return
same_version = self.devices[device.version]
for abi, current_device in same_version.iteritems():
# This device can't fulfill this ABI.
if abi not in device.abis:
continue
# Anything is better than nothing.
if current_device is None:
self.devices[device.version][abi] = device
continue
# The emulator images have actually been changed over time, so the
# devices are more trustworthy.
if current_device.is_emulator and not device.is_emulator:
self.devices[device.version][abi] = device
def get_device(self, version, abi):
return self.devices[version][abi]
def get_missing(self):
missing = []
for version, abis in self.devices.iteritems():
for abi, device in abis.iteritems():
if device is None:
missing.append('android-{} {}'.format(version, abi))
return missing
def get_versions(self):
return self.devices.keys()
def get_abis(self, version):
return self.devices[version].keys()
def get_device_abis(properties):
# 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()
for abi_prop in abi_properties:
if abi_prop in properties:
abis.update(properties[abi_prop].split(','))
return sorted(list(abis))
def get_device_details(serial):
import adb # pylint: disable=import-error
props = adb.get_device(serial).get_props()
name = props['ro.product.name']
version = int(props['ro.build.version.sdk'])
supported_abis = get_device_abis(props)
build_id = props['ro.build.id']
is_emulator = props.get('ro.build.characteristics') == 'emulator'
return Device(serial, name, version, build_id, supported_abis, is_emulator)
def find_devices():
"""Detects connected devices and returns a set for testing.
We get a list of devices by scanning the output of `adb devices`. We want
to run the tests for the cross product of the following configurations:
ABIs: {armeabi, armeabi-v7a, arm64-v8a, mips, mips64, x86, x86_64}
Platform versions: {android-10, android-16, android-21}
Toolchains: {clang, gcc}
Note that not all ABIs are available for every platform version. There are
no 64-bit ABIs before android-21, and there were no MIPS ABIs for
android-10.
"""
if distutils.spawn.find_executable('adb') is None:
raise RuntimeError('Could not find adb.')
# We could get the device name from `adb devices -l`, but we need to
# getprop to find other details anyway, and older devices don't report
# their names properly (nakasi on android-16, for example).
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.
fleet = DeviceFleet()
for line in out.split('\n')[1:]:
if not line.strip():
continue
serial, _ = re.split(r'\s+', line, maxsplit=1)
if 'offline' in line:
print('Ignoring offline device: ' + serial)
continue
if 'unauthorized' in line:
print('Ignoring unauthorized device: ' + serial)
continue
device = get_device_details(serial)
print('Found device {}'.format(device))
fleet.add_device(device)
return fleet
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
'ndk', metavar='NDK', type=os.path.realpath, help='NDK to validate.')
parser.add_argument(
'--filter', help='Only run tests that match the given pattern.')
parser.add_argument(
'--log-dir', type=os.path.realpath, default='test-logs',
help='Directory to store test logs.')
return parser.parse_args()
def get_aggregate_results(details):
tests = {}
for version, abis in details.iteritems():
for abi, toolchains in abis.iteritems():
for toolchain, results in toolchains.iteritems():
for suite, test_results in results.iteritems():
for test in test_results:
if test.failed():
name = '.'.join([suite, test.test_name])
if name not in tests:
tests[name] = []
tests[name].append((version, abi, toolchain, test))
return tests
def print_aggregate_details(details, use_color):
tests = get_aggregate_results(details)
for test_name, configs in tests.iteritems():
# We might be printing a lot of crap here, so let's be obvious about
# where each test starts.
print('BEGIN TEST RESULT: ' + test_name)
print('=' * 80)
for version, abi, toolchain, result in configs:
print('FAILED {} {} {}:'.format(toolchain, abi, version))
print(result.to_string(colored=use_color))
def main():
if sys.platform != 'win32':
ndk.debug.register_debug_handler(signal.SIGUSR1)
ndk.debug.register_trace_handler(signal.SIGUSR2)
args = parse_args()
os.chdir(THIS_DIR)
# We need to do this here rather than at the top because we load the module
# from a path that is given on the command line. We load it from the NDK
# given on the command line so this script can be run even without a full
# platform checkout.
site.addsitedir(os.path.join(args.ndk, 'python-packages'))
ndk_build_path = os.path.join(args.ndk, 'ndk-build')
if os.name == 'nt':
ndk_build_path += '.cmd'
if not os.path.exists(ndk_build_path):
sys.exit(ndk_build_path + ' does not exist.')
fleet = find_devices()
print('Test configuration:')
for version in sorted(fleet.get_versions()):
print('\tandroid-{}:'.format(version))
for abi in sorted(fleet.get_abis(version)):
print('\t\t{}: {}'.format(abi, fleet.get_device(version, abi)))
missing_configs = fleet.get_missing()
if len(missing_configs):
print('Missing configurations: {}'.format(', '.join(missing_configs)))
if os.path.exists(args.log_dir):
shutil.rmtree(args.log_dir)
os.makedirs(args.log_dir)
use_color = sys.stdin.isatty() and os.name != 'nt'
out_dir = tempfile.mkdtemp()
try:
import tests.runners
good, details = tests.runners.run_for_fleet(
args.ndk, fleet, out_dir, args.log_dir, args.filter, use_color)
finally:
shutil.rmtree(out_dir)
print_aggregate_details(details, use_color)
sys.exit(not good)
if __name__ == '__main__':
main()