blob: 50a0530bb97d0c7bcc79174d1699e1081c7b7cd4 [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.
'''This script will run one specific test.'''
from __future__ import print_function, absolute_import
import os
import sys
import atexit
import inspect
import logging
import argparse
import warnings
import harness
from harness import util_constants
from harness import util_log
from harness import util_warnings
from harness.util_functions import load_py_module
from harness.util_lldb import UtilLLDB
from harness.exception import DisconnectedException
from harness.exception import TestSuiteException, TestIgnoredException
from harness.util_timer import Timer
class TestState(object):
'''Simple mutable mapping (like namedtuple)'''
def __init__(self, **kwargs):
for key, val in kwargs.items():
setattr(self, key, val)
def _test_pre_run(state):
'''This function is called before a test is executed (setup).
Args:
state: Test suite state collection, instance of TestState.
Returns:
True if the pre_run step completed without error. Currently the pre-run
will launch the target test binary on the device and attach an
lldb-server to it in platform mode.
Raises:
AssertionError: If an assertion fails.
TestSuiteException: Previous processes of this apk required for this
test could not be killed.
'''
assert state.test
assert state.bundle
log = util_log.get_logger()
log.info('running: {0}'.format(state.name))
# Remove any cached NDK scripts between tests
state.bundle.delete_ndk_cache()
# query our test case for the remote target app it needs
# First try the legacy behaviour
try:
target_name = state.test.get_bundle_target()
warnings.warn("get_bundle_target() is deprecated and will be removed soon"
" - use the `bundle_target` dictionary attribute instead")
except AttributeError:
try:
target_name = state.test.bundle_target[state.bundle_type]
except KeyError:
raise TestIgnoredException()
if target_name is None:
# test case doesn't require a remote process to debug
return True
else:
# find the pid of our remote test process
state.pid = state.bundle.launch(target_name)
if not state.pid:
log.error('unable to get pid of target')
return False
state.android.kill_servers()
# spawn lldb platform on the target device
state.android.launch_lldb_platform(state.device_port)
return True
def _test_post_run(state):
'''This function is called after a test is executed (cleanup).
Args:
state: Test suite state collection, instance of TestState.
Raises:
AssertionError: If an assertion fails.
'''
assert state.test
assert state.bundle
try:
target_name = state.test.get_bundle_target()
warnings.warn("get_bundle_target() is deprecated and will be removed soon"
" - use the `bundle_target` dictionary attribute instead")
except AttributeError:
try:
target_name = state.test.bundle_target[state.bundle_type]
except KeyError:
raise TestIgnoredException()
if target_name:
if state.bundle.is_apk(target_name):
state.android.stop_app(state.bundle.get_package(target_name))
else:
state.android.kill_process(target_name)
def _test_run(state):
'''Execute a single test suite.
Args:
state: test suite state collection, instance of TestState.
Returns:
True: if the test case ran successfully and passed.
False: if the test case failed or suffered an error.
Raises:
AssertionError: If an assertion fails.
'''
assert state.lldb
assert state.lldb_module
assert state.test
test_failures = state.test.run(state.lldb, state.pid, state.lldb_module)
if test_failures:
log = util_log.get_logger()
for test, err in test_failures:
log.error('test %s:%s failed: %r' % (state.name, test, err))
return False
return True
def _initialise_timer(android, interval):
'''Start a 'timeout' timer, to catch stalled execution.
This function will start a timer that will act as a timeout killing this
test session if a test becomes un-responsive.
Args:
android: current instance of harness.UtilAndroid
interval: the interval for the timeout, in seconds
Returns:
The instance of the Timer class that was created.
'''
def on_timeout():
'''This is a callback function that will fire if a test takes longer
then a threshold time to complete.'''
# Clean up the android properties
android.reset_all_props()
# pylint: disable=protected-access
sys.stdout.flush()
# hard exit to force kill all threads that may block our exit
os._exit(util_constants.RC_TEST_TIMEOUT)
timer = Timer(interval, on_timeout)
timer.start()
atexit.register(Timer.stop, timer)
return timer
def _quit_test(num, timer):
'''This function will exit making sure the timeout thread is killed.
Args:
num: An integer specifying the exit status, 0 meaning "successful
termination".
timer: The current Timer instance.
'''
if timer:
timer.stop()
sys.stdout.flush()
sys.exit(num)
def _execute_test(state):
'''Execute a test suite.
Args:
state: The current TestState object.
'''
log = util_log.get_logger()
state.test.setup(state.android)
try:
if not _test_pre_run(state):
raise TestSuiteException('test_pre_run() failed')
if not _test_run(state):
raise TestSuiteException('test_run() failed')
_test_post_run(state)
log.info('Test passed')
finally:
state.test.post_run()
state.test.teardown(state.android)
def _get_test_case_class(module):
'''Inspect a test case module and return the test case class.
Args:
module: A loaded test case module.
'''
# We consider only subclasses of TestCase that have `test_` methods`
log = util_log.get_logger()
log.debug("loading test suites from %r", module)
for name, klass in inspect.getmembers(module, inspect.isclass):
for attr in dir(klass):
if attr.startswith('test_'):
log.info("Found test class %r", name)
return klass
else:
log.debug("class %r has no test_ methods", name)
return None
def get_test_dir(test_name):
''' Get the directory that contains a test with a given name.
Returns:
A string that is the directory containing the test.
Raises:
TestSuiteException: If a test with this name does not exist.
'''
tests_dir = os.path.dirname(os.path.realpath(__file__))
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) and
test_name in os.listdir(current_test_dir)):
return current_test_dir
raise TestSuiteException(
'unable to find test: {0}'.format(test_name))
def main():
'''Test runner entry point.'''
# re-open stdout with no buffering
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
android = None
timer = None
log = None
# parse the command line (positional arguments only)
truthy = lambda x: x.lower() in ('true', '1')
parser = argparse.ArgumentParser("Run a single RenderScript TestSuite against lldb")
for name, formatter in (
('test_name', str),
('log_file_path', str),
('adb_path', str),
('lldb_server_path_device', str),
('aosp_product_path', str),
('device_port', int),
('device', str),
('print_to_stdout', truthy),
('verbose', truthy),
('wimpy', truthy),
('timeout', int),
('bundle_type', str),
):
parser.add_argument(name, type=formatter)
args = parser.parse_args()
try:
# create utility classes
harness.util_log.initialise(
'%s(%s)' % (args.test_name, args.bundle_type),
print_to_stdout=args.print_to_stdout,
level=logging.INFO if not args.verbose else logging.DEBUG,
file_path=args.log_file_path,
file_mode='a'
)
log = util_log.get_logger()
log.debug('Logger initialised')
android = harness.UtilAndroid(args.adb_path,
args.lldb_server_path_device,
args.device)
# start the timeout counter
timer = _initialise_timer(android, args.timeout)
# startup lldb and register teardown handler
atexit.register(UtilLLDB.stop)
UtilLLDB.start()
current_test_dir = get_test_dir(args.test_name)
# load a test case module
test_module = load_py_module(os.path.join(current_test_dir,
args.test_name))
# inspect the test module and locate our test case class
test_class = _get_test_case_class(test_module)
# if our test inherits from TestBaseRemote, check we have a valid device
if (hasattr(test_module, "TestBaseRemote") and
issubclass(test_class, test_module.TestBaseRemote)):
android.validate_device()
# create an instance of our test case
test_inst = test_class(
args.device_port,
args.device,
timer,
args.bundle_type,
wimpy=args.wimpy
)
# instantiate a test target bundle
bundle = harness.UtilBundle(android, args.aosp_product_path)
# execute the test case
try:
for _ in range(2):
try:
# create an lldb instance
lldb = UtilLLDB.create_debugger()
# create state object to encapsulate instances
state = TestState(
android=android,
bundle=bundle,
lldb=lldb,
lldb_module=UtilLLDB.get_module(),
test=test_inst,
pid=None,
name=args.test_name,
device_port=args.device_port,
bundle_type=args.bundle_type
)
util_warnings.redirect_warnings()
_execute_test(state)
# tear down the lldb instance
UtilLLDB.destroy_debugger(lldb)
break
except DisconnectedException as error:
log.warning(error)
log.warning('Trying again.')
else:
log.fatal('Not trying again, maximum retries exceeded.')
raise TestSuiteException('Lost connection to lldb-server')
finally:
util_warnings.restore_warnings()
_quit_test(util_constants.RC_TEST_OK, timer)
except AssertionError:
if log:
log.critical('Internal test suite error', exc_info=1)
print('Internal test suite error', file=sys.stderr)
_quit_test(util_constants.RC_TEST_FATAL, timer)
except TestIgnoredException:
if log:
log.warn("test ignored")
_quit_test(util_constants.RC_TEST_IGNORED, timer)
except TestSuiteException as error:
if log:
log.exception(str(error))
else:
print(error, file=sys.stderr)
_quit_test(util_constants.RC_TEST_FAIL, timer)
# use a global exception handler to be sure that we will
# exit safely and correctly
except Exception:
if log:
log.exception('INTERNAL ERROR')
else:
import traceback
print('Exception {0}'.format(traceback.format_exc()),
file=sys.stderr)
_quit_test(util_constants.RC_TEST_FATAL, timer)
finally:
if android:
android.reset_all_props()
if timer:
timer.stop()
# execution trampoline
if __name__ == '__main__':
print(' '.join(sys.argv))
main()