blob: d1700bbd5d89c880de69cd9f531e5d15296a41ad [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.
'''Main test suite execution script.'''
import argparse
import inspect
import logging
import os
import signal
import subprocess
import sys
import time
import collections
import xml.etree.ElementTree as ET
from config import Config
from tests.harness import util_constants
from tests.harness.exception import TestSuiteException, FailFastException
from tests.harness import UtilAndroid
from tests.harness import UtilBundle
from tests.harness import util_log
from tests.harness.util_functions import load_py_module
from tests.harness.decorators import deprecated
# For some reason pylint is not able to understand the class returned by
# from util_log.get_logger() and generates a lot of false warnings
#pylint: disable=maybe-no-member
EMU_PROC = None
def _parse_args():
'''Parse the command line arguments.
Returns:
A namespace object that contains the options specified to run_tests on
the command line.
'''
parser = argparse.ArgumentParser(description='Run the test suite.')
parser.add_argument('--config', '-c',
metavar='path',
help='Path to a custom config file.')
parser.add_argument('--device', '-d',
help='Specify the device id of the device to test on.')
parser.add_argument('--test', '-t',
metavar='path',
help='Specify a specific test to run.')
group = parser.add_mutually_exclusive_group()
group.add_argument('--wimpy', '-w',
action='store_true',
default=None,
help='Test only a core subset of features.')
group.add_argument('--app-types',
default=['java', 'cpp', 'jni'],
nargs='*',
help='Specify a list of Android app types against which'
' to run the tests',
dest='bundle_types')
parser.add_argument('--install-only',
action='store_true',
default=False,
help='It only runs the pre-run stage of the test suite.'
' It installs the required APKs but does not '
'execute the tests.',
dest='install_only')
parser.add_argument('--no-install', '-n',
action='store_true',
default=False,
help='Stop the test suite installing apks to device.',
dest='noinstall')
parser.add_argument('--no-uninstall',
action='store_true',
default=False,
help='Stop the test suite uninstalling apks after '
'completion.',
dest='nouninstall')
parser.add_argument('--print-to-stdout',
action='store_true',
default=False,
help='Print all logging information to standard out.',
dest='print_to_stdout')
parser.add_argument('--verbose', '-v',
action='store_true',
default=None,
help='Store extra info in the log.')
parser.add_argument('--fail-fast',
action='store_true',
default=False,
help='Exit the test suite immediately on the first failure.')
parser.add_argument('--run-emu',
action='store_true',
default=None,
help='Spawn an emulator and run the test suite on that.'
' Specify the emulator command line in the config'
' file or with -emu-cmd.',
dest='run_emu')
# Get the properties of the Config class and add a command line argument
# for each.
this_module = sys.modules[__name__]
for member_name, member_obj in inspect.getmembers(Config):
if (inspect.isdatadescriptor(member_obj) and
member_name not in ['__weakref__', 'device', 'verbose']):
# List type properties can take one or more arguments
num_args = None
if (isinstance(member_obj, property)
and isinstance(member_obj.fget(Config), list)):
num_args = '+'
opt_name = member_name.replace('_', '-')
setattr(this_module, opt_name, '')
parser.add_argument('--' + opt_name,
nargs=num_args,
help=member_obj.__doc__,
dest=member_name)
return parser.parse_args()
def _choice(first_choice, second_choice):
'''Return first_choice if it is not None otherwise return second_choice.
Args:
first_choice: The first choice value.
second_choice: The alternative value.
Returns:
The first argument if it is not None, and the second otherwise.
'''
return first_choice if first_choice else second_choice
class State(object):
'''This class manages all objects required by the test suite.'''
# pylint: disable=too-many-instance-attributes
# Since this is a state class many attributes are expected.
def __init__(self):
'''State constructor.
Raises:
TestSuiteException: When unable to load config file.
AssertionError: When assertions fail.
'''
# Parse the command line options
args = _parse_args()
# create a config instance
if args.config:
# use the user supplied
config = State.load_user_configuration(args.config)
else:
# use the default configuration
config = Config()
# save the test denylist
self.blocklist = _choice(args.blocklist, config.blocklist)
# Allow any of the command line arguments to override the
# values in the config file.
self.adb_path = _choice(args.adb_path, config.adb_path)
self.host_port = int(_choice(args.host_port, config.host_port))
self.device = _choice(args.device, config.device)
self.user_specified_device = self.device
self.device_port = int(_choice(args.device_port, config.device_port))
self.lldb_server_path_device = _choice(args.lldb_server_path_device,
config.lldb_server_path_device)
self.lldb_server_path_host = _choice(args.lldb_server_path_host,
config.lldb_server_path_host)
self.aosp_product_path = _choice(args.aosp_product_path,
config.aosp_product_path)
self.log_file_path = _choice(args.log_file_path, config.log_file_path)
self.results_file_path = _choice(args.results_file_path,
config.results_file_path)
self.lldb_path = _choice(args.lldb_path, config.lldb_path)
self.print_to_stdout = args.print_to_stdout
self.verbose = _choice(args.verbose, config.verbose)
self.timeout = int(_choice(args.timeout, config.timeout))
self.emu_cmd = _choice(args.emu_cmd, config.emu_cmd)
self.run_emu = args.run_emu
self.wimpy = args.wimpy
self.bundle_types = args.bundle_types if not self.wimpy else ['java']
self.fail_fast = args.fail_fast
# validate the param "verbose"
if not isinstance(self.verbose, bool):
raise TestSuiteException('The parameter "verbose" should be a '
'boolean: {0}'.format(self.verbose))
# create result array
self.results = dict()
self.single_test = args.test
# initialise the logging facility
log_level = logging.INFO if not self.verbose else logging.DEBUG
util_log.initialise("driver",
print_to_stdout=self.print_to_stdout,
level=log_level,
file_mode='w', # open for write
file_path=self.log_file_path
)
log = util_log.get_logger()
if self.run_emu and not self.emu_cmd:
log.TestSuiteException('Need to specify --emu-cmd (or specify a'
' value in the config file) if using --run-emu.')
# create a results file
self.results_file = open(self.results_file_path, 'w')
# create an android helper object
self.android = UtilAndroid(self.adb_path,
self.lldb_server_path_device,
self.device)
assert self.android
# create a test bundle
self.bundle = UtilBundle(self.android,
self.aosp_product_path)
assert self.bundle
# save the no pushing option
assert isinstance(args.noinstall, bool)
self.noinstall = args.noinstall
assert isinstance(args.nouninstall, bool)
self.nouninstall = args.nouninstall
# install only option
assert type(args.install_only) is bool
self.install_only = args.install_only
if self.install_only:
log.log_and_print('Option --install-only set. The test APKs will '
'be installed on the device but the tests will '
'not be executed.')
if self.noinstall:
raise TestSuiteException('Conflicting options given: '
'--install-only and --no-install')
# TCP port modifier which is used to increment the port number used for
# each test case to avoid collisions.
self.port_mod = 0
# total number of test files that have been executed
self.test_count = 0
def get_android(self):
'''Return the android ADB helper instance.
Returns:
The android ADB helper, instance of UtilAndroid.
'''
assert self.android
return self.android
def get_bundle(self):
'''Return the test executable bundle.
Returns:
The test exectable collection, instance of UtilBundle.
'''
return self.bundle
def add_result(self, name, app_type, result):
'''Add a test result to the collection.
Args:
name: String name of the test that has executed.
app_type: type of app i.e. java, jni, or cpp
result: String result of the test, "pass", "fail", "error".
'''
key = (name, app_type)
assert key not in self.results
self.results[key] = result
def get_single_test(self):
'''Get the name of the single test to run.
Returns:
A string that is the name of the python file containing the test to
be run. If all tests are to be run this returns None.
'''
return self.single_test
@staticmethod
def load_user_configuration(path):
'''Load the test suite config from the give path.
Instantiate the Config class found in the module at the given path.
If no suitable class is available, it raises a TestSuiteException.
Args:
path: String location of the module.
Returns:
an instance of the Config class, defined in the module.
Raises:
TestSuiteException: when unable to import the module or when a
subclass of Config is not found inside it.
'''
# load the module
config_module = load_py_module(path)
if not config_module:
raise TestSuiteException('Unable to import the module from "%s"'
% (path))
# look for a subclass of Config
for name, value in inspect.getmembers(config_module):
if (inspect.isclass(value)
and name != 'Config'
and issubclass(value, Config)):
# that's our candidate
return value()
# otherwise there are no valid candidates
raise TestSuiteException('The provided user configuration is not '
'valid. The module must define a subclass '
'of Config')
def _kill_emulator():
''' Kill the emulator process. '''
global EMU_PROC
if EMU_PROC:
try:
EMU_PROC.terminate()
except OSError:
# can't kill a dead proc
log = util_log.get_logger()
log.debug('Trying to kill an emulator but it is already dead.')
def _check_emulator_terminated():
''' Throw an exception if the emulator process has ended.
Raises:
TestSuiteException: If the emulator process has ended.
'''
global EMU_PROC
assert EMU_PROC
if EMU_PROC.poll():
stdout, stderr = EMU_PROC.communicate()
raise TestSuiteException('The emulator terminated with output:'
'\nstderr: {0}\nstdout: {1}.'.format(stderr, stdout))
@deprecated()
def _launch_emulator(state):
'''Launch the emulator and wait for it to boot.
Args:
emu_cmd: The command line to run the emulator.
Raises:
TestSuiteException: If an emulator already exists or the emulator
process terminated before we could connect to it, or
we failed to copy lldb-server to the emulator.
'''
global EMU_PROC
android = state.android
if state.user_specified_device:
if android.device_with_substring_exists(state.user_specified_device):
raise TestSuiteException(
'A device with name {0} already exists.',
state.user_specified_device)
else:
if android.device_with_substring_exists('emulator'):
raise TestSuiteException('An emulator already exists.')
assert state.emu_cmd
EMU_PROC = subprocess.Popen(state.emu_cmd.split(),
stdout=None,
stderr=subprocess.STDOUT)
log = util_log.get_logger()
log.info('Launching emulator with command line {0}'.format(state.emu_cmd))
tries_number = 180
tries = tries_number
found_device = False
while not found_device:
try:
android.validate_device(False, 'emulator')
found_device = True
except TestSuiteException as ex:
tries -= 1
if tries == 0:
# Avoid infinitely looping if the emulator won't boot
log.warning(
'Giving up trying to validate device after {0} tries.'
.format(tries_number))
raise ex
_check_emulator_terminated()
# wait a bit and try again, maybe it has now booted
time.sleep(10)
tries = 500
while not android.is_booted():
tries -= 1
if tries == 0:
# Avoid infinitely looping if the emulator won't boot
raise TestSuiteException('The emulator has failed to boot.')
_check_emulator_terminated()
time.sleep(5)
# Need to be root before we can push lldb-server
android.adb_root()
android.wait_for_device()
# Push the lldb-server executable to the device.
output = android.adb('push {0} {1}'.format(state.lldb_server_path_host,
state.lldb_server_path_device))
if 'failed to copy' in output or 'No such file or directory' in output:
raise TestSuiteException(
'unable to push lldb-server to the emulator: {0}.'
.format(output))
output = android.shell('chmod a+x {0}'
.format(state.lldb_server_path_device))
if 'No such file or directory' in output:
raise TestSuiteException('Failed to copy lldb-server to the emulator.')
def _restart_emulator(state):
'''Kill the emulator and start a new instance.
Args:
state: Test suite state collection, instance of State.
'''
_kill_emulator()
_launch_emulator(state)
def _run_test(state, name, bundle_type):
'''Execute a single test case.
Args:
state: Test suite state collection, instance of State.
name: String file name of the test to execute.
bundle_type: string for the installed app type (cpp|jni|java)
Raises:
AssertionError: When assertion fails.
'''
assert isinstance(name, str)
try:
state.android.check_adb_alive()
except TestSuiteException as expt:
global EMU_PROC
if EMU_PROC:
_restart_emulator(state)
else:
raise expt
log = util_log.get_logger()
sys.stdout.write('Running {0}\r'.format(name))
sys.stdout.flush()
log.info('Running {0}'.format(name))
run_tests_dir = os.path.dirname(os.path.realpath(__file__))
run_test_path = os.path.join(run_tests_dir, 'tests', 'run_test.py')
# Forward port for lldb-server on the device to our host
hport = int(state.host_port) + state.port_mod
dport = int(state.device_port) + state.port_mod
state.android.forward_port(hport, dport)
state.port_mod += 1
log.debug('Giving up control to {0}...'.format(name))
params = map(str, [
sys.executable,
run_test_path,
name,
state.log_file_path,
state.adb_path,
state.lldb_server_path_device,
state.aosp_product_path,
dport,
state.android.get_device_id(),
state.print_to_stdout,
state.verbose,
state.wimpy,
state.timeout,
bundle_type
])
return_code = subprocess.call(params)
state.test_count += 1
state.android.remove_port_forwarding()
log.seek_to_end()
# report in sys.stdout the result
success = return_code == util_constants.RC_TEST_OK
status_handlers = collections.defaultdict(lambda: ('error', log.error), (
(util_constants.RC_TEST_OK, ('pass', log.info)),
(util_constants.RC_TEST_TIMEOUT, ('timeout', log.error)),
(util_constants.RC_TEST_IGNORED, ('ignored', log.info)),
(util_constants.RC_TEST_FAIL, ('fail', log.critical))
)
)
status_name, status_logger = status_handlers[return_code]
log.info('Running %s: %s', name, status_name.upper())
status_logger("Test %r: %s", name, status_name)
# Special case for ignored tests - just return now
if return_code == util_constants.RC_TEST_IGNORED:
return
state.add_result(name, bundle_type, status_name)
if state.fail_fast and not success:
raise FailFastException(name)
# print a running total pass rate
passes = sum(1 for key, value in state.results.items() if value == 'pass')
log.info('Current pass rate: %s of %s executed.', passes, len(state.results))
def _check_lldbserver_exists(state):
'''Check lldb-server exists on the target device and it is executable.
Raises:
TestSuiteError: If lldb-server does not exist on the target.
'''
assert state
message = 'Unable to verify valid lldb-server on target'
android = state.get_android()
assert android
cmd = state.lldb_server_path_device
out = android.shell(cmd, False)
if not isinstance(out, str):
raise TestSuiteException(message)
if out.find('Usage:') < 0:
raise TestSuiteException(message)
def _suite_pre_run(state):
'''This function is executed before the test cases are run (setup).
Args:
state: Test suite state collection, instance of State.
Return:
True if the pre_run step completes without error.
Checks made:
- Validating that adb exists and runs.
- Validating that a device is attached.
- We have root access to the device.
- All test binaries were pushed to the device.
- The port for lldb-server was forwarded correctly.
Raises:
AssertionError: When assertions fail.
'''
assert state
log = util_log.get_logger()
try:
android = state.get_android()
bundle = state.get_bundle()
assert android
assert bundle
# validate ADB helper class
android.validate_adb()
log.log_and_print('Located ADB')
if state.run_emu:
log.log_and_print('Launching emulator...')
_launch_emulator(state)
log.log_and_print('Started emulator ' + android.device)
else:
android.validate_device()
log.log_and_print('Located device ' + android.device)
if state.noinstall and not state.single_test:
bundle.check_apps_installed(state.wimpy)
# elevate to root user
android.adb_root()
android.wait_for_device()
# check that lldb-server exists on device
android.kill_servers()
_check_lldbserver_exists(state)
if not state.noinstall:
# push all tests to the device
log.log_and_print('Pushing all tests...')
bundle.push_all()
log.log_and_print('Pushed all tests')
log.log_and_print('Pre run complete')
except TestSuiteException as expt:
log.exception('Test suite pre run failure')
# Even if we are logging the error, it may be helpful and more
# immediate to find out the error into the terminal
log.log_and_print('ERROR: Unable to set up the test suite: %s\n'
% expt.message, logging.ERROR)
return False
return True
def _suite_post_run(state):
'''This function is executed after the test cases have run (teardown).
Args:
state: Test suite state collection, instance of State.
Returns:
Number of failures
'''
log = util_log.get_logger()
if not state.noinstall and not state.nouninstall:
if state.wimpy:
state.bundle.uninstall_all_apk()
else:
state.bundle.uninstall_all()
log.log_and_print('Uninstalled/Deleted all tests')
total = 0
passes = 0
failures = 0
results = ET.Element('testsuite')
results.attrib['name'] = 'LLDB RS Test Suite'
for key, value in state.results.items():
total += 1
if value == 'pass':
passes += 1
else:
failures += 1
# test case name, followed by pass, failure or error elements
testcase = ET.Element('testcase')
testcase.attrib['name'] = "%s:%s" % key
result_element = ET.Element(value)
result_element.text = "%s:%s" % key
testcase.append(result_element)
results.append(testcase)
assert passes + failures == total, 'Invalid test results status'
if failures:
log.log_and_print(
'The following failures occurred:\n%s\n' %
'\n'.join('failed: %s:%s' % test_spec
for test_spec, result in state.results.items() if result != 'pass'
))
log.log_and_print('{0} of {1} passed'.format(passes, total))
if total:
log.log_and_print('{0}% rate'.format((passes*100)/total))
results.attrib['tests'] = str(total)
state.results_file.write(ET.tostring(results, encoding='iso-8859-1'))
return failures
def _discover_tests(state):
'''Discover all tests in the tests directory.
Returns:
List of strings, test file names from the 'tests' directory.
'''
tests = []
single_test = state.get_single_test()
if single_test is None:
file_dir = os.path.dirname(os.path.realpath(__file__))
tests_dir = os.path.join(file_dir, 'tests')
for sub_dir in os.listdir(tests_dir):
current_test_dir = os.path.join(tests_dir, sub_dir)
if os.path.isdir(current_test_dir):
dir_name = os.path.basename(current_test_dir)
if dir_name == 'harness':
continue
for item in os.listdir(current_test_dir):
if (item.startswith('test')
and item.endswith('.py')
and not item in state.blocklist):
tests.append(item)
else:
if single_test.endswith('.py'):
tests.append(single_test)
else:
tests.append(single_test + '.py')
return tests
def _deduce_python_path(state):
'''Try to deduce the PYTHONPATH environment variable via the LLDB binary.
Args:
state: Test suite state collection, instance of State.
Returns:
True if PYTHONPATH has been updated, False otherwise.
Raises:
TestSuiteException: If lldb path provided in the config or command line
is incorrect.
AssertionError: If an assertion fails.
'''
lldb_path = state.lldb_path
if not lldb_path:
# lldb may not be provided in preference of a manual $PYTHONPATH
return False
params = [lldb_path, '-P']
try:
proc = subprocess.Popen(params, stdout=subprocess.PIPE)
except OSError as err:
error_string = 'Could not run lldb at %s: %s' % (lldb_path, str(err))
raise TestSuiteException(error_string)
stdout = proc.communicate()[0]
if stdout:
os.environ['PYTHONPATH'] = stdout.strip()
return True
return False
def main():
'''The lldb-renderscript test suite entry point.'''
log = None
try:
# parse the command line
state = State()
assert state
# logging is initialised in State()
log = util_log.get_logger()
# if we can, set PYTHONPATH for lldb bindings
if not _deduce_python_path(state):
log.log_and_print('Unable to deduce PYTHONPATH', logging.WARN)
# pre run step
if not _suite_pre_run(state):
raise TestSuiteException('Test suite pre-run step failed')
# discover all tests and execute them
tests = _discover_tests(state)
log.log_and_print('Found {0} tests'.format(len(tests)))
if state.install_only:
log.log_and_print('Test applications installed. Terminating due to '
'--install-only option')
else:
# run the tests
for bundle_type in state.bundle_types:
log.info("Running bundle type '%s'", bundle_type)
for item in tests:
_run_test(state, item, bundle_type)
# post run step
quit(0 if _suite_post_run(state) == 0 else 1)
except AssertionError:
if log:
log.exception('Internal test suite error')
print('Internal test suite error')
quit(1)
except FailFastException:
log.exception('Early exit after first test failure')
quit(1)
except TestSuiteException as error:
if log:
log.exception('Test suite exception')
print('{0}'.format(str(error)))
quit(2)
finally:
_kill_emulator()
logging.shutdown()
def signal_handler(_, _unused):
'''Signal handler for SIGINT, caused by the user typing Ctrl-C.'''
# pylint: disable=unused-argument
# pylint: disable=protected-access
print('Ctrl+C!')
os._exit(1)
# execution trampoline
if __name__ == '__main__':
signal.signal(signal.SIGINT, signal_handler)
main()