Home | History | Annotate | Download | only in tests
      1 #!/usr/bin/env python
      2 
      3 # Copyright (C) 2016 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 
     17 '''This script will run one specific test.'''
     18 from __future__ import print_function, absolute_import
     19 
     20 import os
     21 import sys
     22 import atexit
     23 import inspect
     24 import logging
     25 import argparse
     26 import warnings
     27 
     28 import harness
     29 from harness import util_constants
     30 from harness import util_log
     31 from harness import util_warnings
     32 from harness.util_functions import load_py_module
     33 from harness.util_lldb import UtilLLDB
     34 from harness.exception import DisconnectedException
     35 from harness.exception import TestSuiteException, TestIgnoredException
     36 from harness.util_timer import Timer
     37 
     38 
     39 class TestState(object):
     40     '''Simple mutable mapping (like namedtuple)'''
     41     def __init__(self, **kwargs):
     42         for key, val in kwargs.items():
     43             setattr(self, key, val)
     44 
     45 
     46 def _test_pre_run(state):
     47     '''This function is called before a test is executed (setup).
     48 
     49     Args:
     50         state: Test suite state collection, instance of TestState.
     51 
     52     Returns:
     53         True if the pre_run step completed without error. Currently the pre-run
     54         will launch the target test binary on the device and attach an
     55         lldb-server to it in platform mode.
     56 
     57     Raises:
     58         AssertionError: If an assertion fails.
     59         TestSuiteException: Previous processes of this apk required for this
     60                             test could not be killed.
     61     '''
     62     assert state.test
     63     assert state.bundle
     64 
     65     log = util_log.get_logger()
     66     log.info('running: {0}'.format(state.name))
     67 
     68     # Remove any cached NDK scripts between tests
     69     state.bundle.delete_ndk_cache()
     70 
     71     # query our test case for the remote target app it needs
     72     # First try the legacy behaviour
     73     try:
     74         target_name = state.test.get_bundle_target()
     75         warnings.warn("get_bundle_target() is deprecated and will be removed soon"
     76                       " - use the `bundle_target` dictionary attribute instead")
     77     except AttributeError:
     78         try:
     79             target_name = state.test.bundle_target[state.bundle_type]
     80         except KeyError:
     81             raise TestIgnoredException()
     82 
     83     if target_name is None:
     84         # test case doesn't require a remote process to debug
     85         return True
     86     else:
     87         # find the pid of our remote test process
     88         state.pid = state.bundle.launch(target_name)
     89         if not state.pid:
     90             log.error('unable to get pid of target')
     91             return False
     92         state.android.kill_servers()
     93         # spawn lldb platform on the target device
     94         state.android.launch_lldb_platform(state.device_port)
     95         return True
     96 
     97 
     98 def _test_post_run(state):
     99     '''This function is called after a test is executed (cleanup).
    100 
    101     Args:
    102         state: Test suite state collection, instance of TestState.
    103 
    104     Raises:
    105         AssertionError: If an assertion fails.
    106     '''
    107     assert state.test
    108     assert state.bundle
    109 
    110     try:
    111         target_name = state.test.get_bundle_target()
    112         warnings.warn("get_bundle_target() is deprecated and will be removed soon"
    113                       " - use the `bundle_target` dictionary attribute instead")
    114     except AttributeError:
    115         try:
    116             target_name = state.test.bundle_target[state.bundle_type]
    117         except KeyError:
    118             raise TestIgnoredException()
    119 
    120 
    121     if target_name:
    122         if state.bundle.is_apk(target_name):
    123             state.android.stop_app(state.bundle.get_package(target_name))
    124         else:
    125             state.android.kill_process(target_name)
    126 
    127 
    128 def _test_run(state):
    129     '''Execute a single test suite.
    130 
    131     Args:
    132         state: test suite state collection, instance of TestState.
    133 
    134     Returns:
    135         True: if the test case ran successfully and passed.
    136         False: if the test case failed or suffered an error.
    137 
    138     Raises:
    139         AssertionError: If an assertion fails.
    140     '''
    141     assert state.lldb
    142     assert state.lldb_module
    143     assert state.test
    144 
    145     test_failures = state.test.run(state.lldb, state.pid, state.lldb_module)
    146 
    147     if test_failures:
    148         log = util_log.get_logger()
    149         for test, err in test_failures:
    150             log.error('test %s:%s failed: %r' % (state.name, test, err))
    151 
    152         return False
    153 
    154     return True
    155 
    156 
    157 def _initialise_timer(android, interval):
    158     '''Start a 'timeout' timer, to catch stalled execution.
    159 
    160     This function will start a timer that will act as a timeout killing this
    161     test session if a test becomes un-responsive.
    162 
    163     Args:
    164         android: current instance of harness.UtilAndroid
    165         interval: the interval for the timeout, in seconds
    166 
    167     Returns:
    168         The instance of the Timer class that was created.
    169     '''
    170 
    171     def on_timeout():
    172         '''This is a callback function that will fire if a test takes longer
    173         then a threshold time to complete.'''
    174         # Clean up the android properties
    175         android.reset_all_props()
    176         # pylint: disable=protected-access
    177         sys.stdout.flush()
    178         # hard exit to force kill all threads that may block our exit
    179         os._exit(util_constants.RC_TEST_TIMEOUT)
    180 
    181     timer = Timer(interval, on_timeout)
    182     timer.start()
    183     atexit.register(Timer.stop, timer)
    184     return timer
    185 
    186 
    187 def _quit_test(num, timer):
    188     '''This function will exit making sure the timeout thread is killed.
    189 
    190     Args:
    191         num: An integer specifying the exit status, 0 meaning "successful
    192              termination".
    193         timer: The current Timer instance.
    194     '''
    195     if timer:
    196         timer.stop()
    197     sys.stdout.flush()
    198     sys.exit(num)
    199 
    200 
    201 def _execute_test(state):
    202     '''Execute a test suite.
    203 
    204     Args:
    205         state: The current TestState object.
    206     '''
    207     log = util_log.get_logger()
    208 
    209     state.test.setup(state.android)
    210     try:
    211         if not _test_pre_run(state):
    212             raise TestSuiteException('test_pre_run() failed')
    213         if not _test_run(state):
    214             raise TestSuiteException('test_run() failed')
    215         _test_post_run(state)
    216         log.info('Test passed')
    217 
    218     finally:
    219         state.test.post_run()
    220         state.test.teardown(state.android)
    221 
    222 
    223 def _get_test_case_class(module):
    224     '''Inspect a test case module and return the test case class.
    225 
    226     Args:
    227         module: A loaded test case module.
    228     '''
    229     # We consider only subclasses of TestCase that have `test_` methods`
    230     log = util_log.get_logger()
    231     log.debug("loading test suites from %r", module)
    232     for name, klass in inspect.getmembers(module, inspect.isclass):
    233         for attr in dir(klass):
    234             if attr.startswith('test_'):
    235                 log.info("Found test class %r", name)
    236                 return klass
    237         else:
    238             log.debug("class %r has no test_ methods", name)
    239     return None
    240 
    241 
    242 def get_test_dir(test_name):
    243     ''' Get the directory that contains a test with a given name.
    244 
    245     Returns:
    246         A string that is the directory containing the test.
    247 
    248     Raises:
    249         TestSuiteException: If a test with this name does not exist.
    250     '''
    251     tests_dir = os.path.dirname(os.path.realpath(__file__))
    252     for sub_dir in os.listdir(tests_dir):
    253         current_test_dir = os.path.join(tests_dir, sub_dir)
    254         if (os.path.isdir(current_test_dir) and
    255             test_name in os.listdir(current_test_dir)):
    256             return current_test_dir
    257 
    258     raise TestSuiteException(
    259         'unable to find test: {0}'.format(test_name))
    260 
    261 
    262 def main():
    263     '''Test runner entry point.'''
    264 
    265     # re-open stdout with no buffering
    266     sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
    267 
    268     android = None
    269     timer = None
    270     log = None
    271 
    272     # parse the command line (positional arguments only)
    273     truthy = lambda x: x.lower() in ('true', '1')
    274     parser = argparse.ArgumentParser("Run a single RenderScript TestSuite against lldb")
    275     for name, formatter in (
    276        ('test_name', str),
    277        ('log_file_path', str),
    278        ('adb_path', str),
    279        ('lldb_server_path_device', str),
    280        ('aosp_product_path', str),
    281        ('device_port', int),
    282        ('device', str),
    283        ('print_to_stdout', truthy),
    284        ('verbose', truthy),
    285        ('wimpy', truthy),
    286        ('timeout', int),
    287        ('bundle_type', str),
    288     ):
    289         parser.add_argument(name, type=formatter)
    290 
    291     args = parser.parse_args()
    292 
    293     try:
    294         # create utility classes
    295         harness.util_log.initialise(
    296             '%s(%s)' % (args.test_name, args.bundle_type),
    297             print_to_stdout=args.print_to_stdout,
    298             level=logging.INFO if not args.verbose else logging.DEBUG,
    299             file_path=args.log_file_path,
    300             file_mode='a'
    301         )
    302         log = util_log.get_logger()
    303         log.debug('Logger initialised')
    304 
    305         android = harness.UtilAndroid(args.adb_path,
    306                                       args.lldb_server_path_device,
    307                                       args.device)
    308 
    309         # start the timeout counter
    310         timer = _initialise_timer(android, args.timeout)
    311 
    312         # startup lldb and register teardown handler
    313         atexit.register(UtilLLDB.stop)
    314         UtilLLDB.start()
    315 
    316         current_test_dir = get_test_dir(args.test_name)
    317 
    318         # load a test case module
    319         test_module = load_py_module(os.path.join(current_test_dir,
    320                                                   args.test_name))
    321 
    322 
    323         # inspect the test module and locate our test case class
    324         test_class = _get_test_case_class(test_module)
    325 
    326         # if our test inherits from TestBaseRemote, check we have a valid device
    327         if (hasattr(test_module, "TestBaseRemote") and
    328             issubclass(test_class, test_module.TestBaseRemote)):
    329             android.validate_device()
    330 
    331         # create an instance of our test case
    332         test_inst = test_class(
    333             args.device_port,
    334             args.device,
    335             timer,
    336             args.bundle_type,
    337             wimpy=args.wimpy
    338         )
    339 
    340         # instantiate a test target bundle
    341         bundle = harness.UtilBundle(android, args.aosp_product_path)
    342 
    343         # execute the test case
    344         try:
    345             for _ in range(2):
    346                 try:
    347                     # create an lldb instance
    348                     lldb = UtilLLDB.create_debugger()
    349 
    350                     # create state object to encapsulate instances
    351 
    352                     state = TestState(
    353                          android=android,
    354                          bundle=bundle,
    355                          lldb=lldb,
    356                          lldb_module=UtilLLDB.get_module(),
    357                          test=test_inst,
    358                          pid=None,
    359                          name=args.test_name,
    360                          device_port=args.device_port,
    361                          bundle_type=args.bundle_type
    362                     )
    363 
    364                     util_warnings.redirect_warnings()
    365 
    366                     _execute_test(state)
    367 
    368                     # tear down the lldb instance
    369                     UtilLLDB.destroy_debugger(lldb)
    370                     break
    371                 except DisconnectedException as error:
    372                     log.warning(error)
    373                     log.warning('Trying again.')
    374             else:
    375                 log.fatal('Not trying again, maximum retries exceeded.')
    376                 raise TestSuiteException('Lost connection to lldb-server')
    377 
    378         finally:
    379             util_warnings.restore_warnings()
    380 
    381         _quit_test(util_constants.RC_TEST_OK, timer)
    382 
    383     except AssertionError:
    384         if log:
    385             log.critical('Internal test suite error', exc_info=1)
    386         print('Internal test suite error', file=sys.stderr)
    387         _quit_test(util_constants.RC_TEST_FATAL, timer)
    388 
    389     except TestIgnoredException:
    390         if log:
    391             log.warn("test ignored")
    392         _quit_test(util_constants.RC_TEST_IGNORED, timer)
    393 
    394     except TestSuiteException as error:
    395         if log:
    396             log.exception(str(error))
    397         else:
    398             print(error, file=sys.stderr)
    399         _quit_test(util_constants.RC_TEST_FAIL, timer)
    400 
    401     # use a global exception handler to be sure that we will
    402     # exit safely and correctly
    403     except Exception:
    404         if log:
    405             log.exception('INTERNAL ERROR')
    406         else:
    407             import traceback
    408             print('Exception {0}'.format(traceback.format_exc()),
    409                   file=sys.stderr)
    410         _quit_test(util_constants.RC_TEST_FATAL, timer)
    411 
    412     finally:
    413         if android:
    414             android.reset_all_props()
    415         if timer:
    416             timer.stop()
    417 
    418 
    419 # execution trampoline
    420 if __name__ == '__main__':
    421     print(' '.join(sys.argv))
    422     main()
    423