| # Copyright 2016 Google Inc. |
| # |
| # 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. |
| |
| import collections |
| import contextlib |
| import copy |
| import functools |
| import inspect |
| import logging |
| import os |
| |
| from future.utils import raise_with_traceback |
| |
| from mobly import controller_manager |
| from mobly import expects |
| from mobly import records |
| from mobly import runtime_test_info |
| from mobly import signals |
| from mobly import utils |
| |
| # Macro strings for test result reporting. |
| TEST_CASE_TOKEN = '[Test]' |
| RESULT_LINE_TEMPLATE = TEST_CASE_TOKEN + ' %s %s' |
| |
| TEST_STAGE_BEGIN_LOG_TEMPLATE = '[{parent_token}]#{child_token} >>> BEGIN >>>' |
| TEST_STAGE_END_LOG_TEMPLATE = '[{parent_token}]#{child_token} <<< END <<<' |
| |
| # Names of execution stages, in the order they happen during test runs. |
| STAGE_NAME_SETUP_GENERATED_TESTS = 'setup_generated_tests' |
| STAGE_NAME_SETUP_CLASS = 'setup_class' |
| STAGE_NAME_SETUP_TEST = 'setup_test' |
| STAGE_NAME_TEARDOWN_TEST = 'teardown_test' |
| STAGE_NAME_TEARDOWN_CLASS = 'teardown_class' |
| STAGE_NAME_CLEAN_UP = 'clean_up' |
| |
| |
| class Error(Exception): |
| """Raised for exceptions that occurred in BaseTestClass.""" |
| |
| |
| class BaseTestClass(object): |
| """Base class for all test classes to inherit from. |
| |
| This class gets all the controller objects from test_runner and executes |
| the tests requested within itself. |
| |
| Most attributes of this class are set at runtime based on the configuration |
| provided. |
| |
| The default logger in logging module is set up for each test run. If you |
| want to log info to the test run output file, use `logging` directly, like |
| `logging.info`. |
| |
| Attributes: |
| tests: A list of strings, each representing a test method name. |
| TAG: A string used to refer to a test class. Default is the test class |
| name. |
| results: A records.TestResult object for aggregating test results from |
| the execution of tests. |
| controller_configs: dict, controller configs provided by the user via |
| test bed config. |
| current_test_info: RuntimeTestInfo, runtime information on the test |
| currently being executed. |
| root_output_path: string, storage path for output files associated with |
| the entire test run. A test run can have multiple test class |
| executions. This includes the test summary and Mobly log files. |
| log_path: string, storage path for files specific to a single test |
| class execution. |
| test_bed_name: [Deprecated, use 'testbed_name' instead] |
| string, the name of the test bed used by a test run. |
| testbed_name: string, the name of the test bed used by a test run. |
| user_params: dict, custom parameters from user, to be consumed by |
| the test logic. |
| """ |
| |
| TAG = None |
| |
| def __init__(self, configs): |
| """Constructor of BaseTestClass. |
| |
| The constructor takes a config_parser.TestRunConfig object and which has |
| all the information needed to execute this test class, like log_path |
| and controller configurations. For details, see the definition of class |
| config_parser.TestRunConfig. |
| |
| Args: |
| configs: A config_parser.TestRunConfig object. |
| """ |
| self.tests = [] |
| class_identifier = self.__class__.__name__ |
| if configs.test_class_name_suffix: |
| class_identifier = '%s_%s' % (class_identifier, |
| configs.test_class_name_suffix) |
| if self.TAG is None: |
| self.TAG = class_identifier |
| # Set params. |
| self.root_output_path = configs.log_path |
| self.log_path = os.path.join(self.root_output_path, class_identifier) |
| utils.create_dir(self.log_path) |
| # Deprecated, use 'testbed_name' |
| self.test_bed_name = configs.test_bed_name |
| self.testbed_name = configs.testbed_name |
| self.user_params = configs.user_params |
| self.results = records.TestResult() |
| self.summary_writer = configs.summary_writer |
| self._generated_test_table = collections.OrderedDict() |
| self._controller_manager = controller_manager.ControllerManager( |
| class_name=self.TAG, controller_configs=configs.controller_configs) |
| self.controller_configs = self._controller_manager.controller_configs |
| |
| def unpack_userparams(self, |
| req_param_names=None, |
| opt_param_names=None, |
| **kwargs): |
| """An optional function that unpacks user defined parameters into |
| individual variables. |
| |
| After unpacking, the params can be directly accessed with self.xxx. |
| |
| If a required param is not provided, an exception is raised. If an |
| optional param is not provided, a warning line will be logged. |
| |
| To provide a param, add it in the config file or pass it in as a kwarg. |
| If a param appears in both the config file and kwarg, the value in the |
| config file is used. |
| |
| User params from the config file can also be directly accessed in |
| self.user_params. |
| |
| Args: |
| req_param_names: A list of names of the required user params. |
| opt_param_names: A list of names of the optional user params. |
| **kwargs: Arguments that provide default values. |
| e.g. unpack_userparams(required_list, opt_list, arg_a='hello') |
| self.arg_a will be 'hello' unless it is specified again in |
| required_list or opt_list. |
| |
| Raises: |
| Error: A required user params is not provided. |
| """ |
| req_param_names = req_param_names or [] |
| opt_param_names = opt_param_names or [] |
| for k, v in kwargs.items(): |
| if k in self.user_params: |
| v = self.user_params[k] |
| setattr(self, k, v) |
| for name in req_param_names: |
| if hasattr(self, name): |
| continue |
| if name not in self.user_params: |
| raise Error('Missing required user param "%s" in test ' |
| 'configuration.' % name) |
| setattr(self, name, self.user_params[name]) |
| for name in opt_param_names: |
| if hasattr(self, name): |
| continue |
| if name in self.user_params: |
| setattr(self, name, self.user_params[name]) |
| else: |
| logging.warning( |
| 'Missing optional user param "%s" in ' |
| 'configuration, continue.', name) |
| |
| def register_controller(self, module, required=True, min_number=1): |
| """Loads a controller module and returns its loaded devices. |
| |
| A Mobly controller module is a Python lib that can be used to control |
| a device, service, or equipment. To be Mobly compatible, a controller |
| module needs to have the following members: |
| |
| .. code-block:: python |
| |
| def create(configs): |
| [Required] Creates controller objects from configurations. |
| |
| Args: |
| configs: A list of serialized data like string/dict. Each |
| element of the list is a configuration for a controller |
| object. |
| |
| Returns: |
| A list of objects. |
| |
| def destroy(objects): |
| [Required] Destroys controller objects created by the create |
| function. Each controller object shall be properly cleaned up |
| and all the resources held should be released, e.g. memory |
| allocation, sockets, file handlers etc. |
| |
| Args: |
| A list of controller objects created by the create function. |
| |
| def get_info(objects): |
| [Optional] Gets info from the controller objects used in a test |
| run. The info will be included in test_summary.yaml under |
| the key 'ControllerInfo'. Such information could include unique |
| ID, version, or anything that could be useful for describing the |
| test bed and debugging. |
| |
| Args: |
| objects: A list of controller objects created by the create |
| function. |
| |
| Returns: |
| A list of json serializable objects: each represents the |
| info of a controller object. The order of the info |
| object should follow that of the input objects. |
| |
| Registering a controller module declares a test class's dependency the |
| controller. If the module config exists and the module matches the |
| controller interface, controller objects will be instantiated with |
| corresponding configs. The module should be imported first. |
| |
| Args: |
| module: A module that follows the controller module interface. |
| required: A bool. If True, failing to register the specified |
| controller module raises exceptions. If False, the objects |
| failed to instantiate will be skipped. |
| min_number: An integer that is the minimum number of controller |
| objects to be created. Default is one, since you should not |
| register a controller module without expecting at least one |
| object. |
| |
| Returns: |
| A list of controller objects instantiated from controller_module, or |
| None if no config existed for this controller and it was not a |
| required controller. |
| |
| Raises: |
| ControllerError: |
| * The controller module has already been registered. |
| * The actual number of objects instantiated is less than the |
| * `min_number`. |
| * `required` is True and no corresponding config can be found. |
| * Any other error occurred in the registration process. |
| """ |
| return self._controller_manager.register_controller( |
| module, required, min_number) |
| |
| def _record_controller_info(self): |
| # Collect controller information and write to test result. |
| for record in self._controller_manager.get_controller_info_records(): |
| self.results.add_controller_info_record(record) |
| self.summary_writer.dump( |
| record.to_dict(), records.TestSummaryEntryType.CONTROLLER_INFO) |
| |
| def _setup_generated_tests(self): |
| """Proxy function to guarantee the base implementation of |
| setup_generated_tests is called. |
| |
| Returns: |
| True if setup is successful, False otherwise. |
| """ |
| stage_name = STAGE_NAME_SETUP_GENERATED_TESTS |
| record = records.TestResultRecord(stage_name, self.TAG) |
| record.test_begin() |
| self.current_test_info = runtime_test_info.RuntimeTestInfo( |
| stage_name, self.log_path, record) |
| try: |
| with self._log_test_stage(stage_name): |
| self.setup_generated_tests() |
| return True |
| except Exception as e: |
| logging.exception('%s failed for %s.', stage_name, self.TAG) |
| record.test_error(e) |
| self.results.add_class_error(record) |
| self.summary_writer.dump(record.to_dict(), |
| records.TestSummaryEntryType.RECORD) |
| return False |
| |
| def setup_generated_tests(self): |
| """Preprocesses that need to be done before setup_class. |
| |
| This phase is used to do pre-test processes like generating tests. |
| This is the only place `self.generate_tests` should be called. |
| |
| If this function throws an error, the test class will be marked failure |
| and the "Requested" field will be 0 because the number of tests |
| requested is unknown at this point. |
| """ |
| |
| def _setup_class(self): |
| """Proxy function to guarantee the base implementation of setup_class |
| is called. |
| |
| Returns: |
| If `self.results` is returned instead of None, this means something |
| has gone wrong, and the rest of the test class should not execute. |
| """ |
| # Setup for the class. |
| class_record = records.TestResultRecord(STAGE_NAME_SETUP_CLASS, |
| self.TAG) |
| class_record.test_begin() |
| self.current_test_info = runtime_test_info.RuntimeTestInfo( |
| STAGE_NAME_SETUP_CLASS, self.log_path, class_record) |
| expects.recorder.reset_internal_states(class_record) |
| try: |
| with self._log_test_stage(STAGE_NAME_SETUP_CLASS): |
| self.setup_class() |
| except signals.TestAbortSignal: |
| # Throw abort signals to outer try block for handling. |
| raise |
| except Exception as e: |
| # Setup class failed for unknown reasons. |
| # Fail the class and skip all tests. |
| logging.exception('Error in %s#setup_class.', self.TAG) |
| class_record.test_error(e) |
| self.results.add_class_error(class_record) |
| self._exec_procedure_func(self._on_fail, class_record) |
| class_record.update_record() |
| self.summary_writer.dump(class_record.to_dict(), |
| records.TestSummaryEntryType.RECORD) |
| self._skip_remaining_tests(e) |
| return self.results |
| if expects.recorder.has_error: |
| self._exec_procedure_func(self._on_fail, class_record) |
| class_record.test_error() |
| class_record.update_record() |
| self.summary_writer.dump(class_record.to_dict(), |
| records.TestSummaryEntryType.RECORD) |
| self.results.add_class_error(class_record) |
| self._skip_remaining_tests( |
| class_record.termination_signal.exception) |
| return self.results |
| |
| def setup_class(self): |
| """Setup function that will be called before executing any test in the |
| class. |
| |
| To signal setup failure, use asserts or raise your own exception. |
| |
| Errors raised from `setup_class` will trigger `on_fail`. |
| |
| Implementation is optional. |
| """ |
| |
| def _teardown_class(self): |
| """Proxy function to guarantee the base implementation of |
| teardown_class is called. |
| """ |
| stage_name = STAGE_NAME_TEARDOWN_CLASS |
| record = records.TestResultRecord(stage_name, self.TAG) |
| record.test_begin() |
| self.current_test_info = runtime_test_info.RuntimeTestInfo( |
| stage_name, self.log_path, record) |
| expects.recorder.reset_internal_states(record) |
| try: |
| with self._log_test_stage(stage_name): |
| self.teardown_class() |
| except signals.TestAbortAll as e: |
| setattr(e, 'results', self.results) |
| raise |
| except Exception as e: |
| logging.exception('Error encountered in %s.', stage_name) |
| record.test_error(e) |
| record.update_record() |
| self.results.add_class_error(record) |
| self.summary_writer.dump(record.to_dict(), |
| records.TestSummaryEntryType.RECORD) |
| else: |
| if expects.recorder.has_error: |
| record.update_record() |
| self.results.add_class_error(record) |
| self.summary_writer.dump(record.to_dict(), |
| records.TestSummaryEntryType.RECORD) |
| finally: |
| self._clean_up() |
| |
| def teardown_class(self): |
| """Teardown function that will be called after all the selected tests in |
| the test class have been executed. |
| |
| Errors raised from `teardown_class` do not trigger `on_fail`. |
| |
| Implementation is optional. |
| """ |
| |
| @contextlib.contextmanager |
| def _log_test_stage(self, stage_name): |
| """Logs the begin and end of a test stage. |
| |
| This context adds two log lines meant for clarifying the boundary of |
| each execution stage in Mobly log. |
| |
| Args: |
| stage_name: string, name of the stage to log. |
| """ |
| parent_token = self.current_test_info.name |
| # If the name of the stage is the same as the test name, in which case |
| # the stage is class-level instead of test-level, use the class's |
| # reference tag as the parent token instead. |
| if parent_token == stage_name: |
| parent_token = self.TAG |
| logging.debug( |
| TEST_STAGE_BEGIN_LOG_TEMPLATE.format(parent_token=parent_token, |
| child_token=stage_name)) |
| try: |
| yield |
| finally: |
| logging.debug( |
| TEST_STAGE_END_LOG_TEMPLATE.format(parent_token=parent_token, |
| child_token=stage_name)) |
| |
| def _setup_test(self, test_name): |
| """Proxy function to guarantee the base implementation of setup_test is |
| called. |
| """ |
| with self._log_test_stage(STAGE_NAME_SETUP_TEST): |
| self.setup_test() |
| |
| def setup_test(self): |
| """Setup function that will be called every time before executing each |
| test method in the test class. |
| |
| To signal setup failure, use asserts or raise your own exception. |
| |
| Implementation is optional. |
| """ |
| |
| def _teardown_test(self, test_name): |
| """Proxy function to guarantee the base implementation of teardown_test |
| is called. |
| """ |
| with self._log_test_stage(STAGE_NAME_TEARDOWN_TEST): |
| self.teardown_test() |
| |
| def teardown_test(self): |
| """Teardown function that will be called every time a test method has |
| been executed. |
| |
| Implementation is optional. |
| """ |
| |
| def _on_fail(self, record): |
| """Proxy function to guarantee the base implementation of on_fail is |
| called. |
| |
| Args: |
| record: records.TestResultRecord, a copy of the test record for |
| this test, containing all information of the test execution |
| including exception objects. |
| """ |
| self.on_fail(record) |
| |
| def on_fail(self, record): |
| """A function that is executed upon a test failure. |
| |
| User implementation is optional. |
| |
| Args: |
| record: records.TestResultRecord, a copy of the test record for |
| this test, containing all information of the test execution |
| including exception objects. |
| """ |
| |
| def _on_pass(self, record): |
| """Proxy function to guarantee the base implementation of on_pass is |
| called. |
| |
| Args: |
| record: records.TestResultRecord, a copy of the test record for |
| this test, containing all information of the test execution |
| including exception objects. |
| """ |
| msg = record.details |
| if msg: |
| logging.info(msg) |
| self.on_pass(record) |
| |
| def on_pass(self, record): |
| """A function that is executed upon a test passing. |
| |
| Implementation is optional. |
| |
| Args: |
| record: records.TestResultRecord, a copy of the test record for |
| this test, containing all information of the test execution |
| including exception objects. |
| """ |
| |
| def _on_skip(self, record): |
| """Proxy function to guarantee the base implementation of on_skip is |
| called. |
| |
| Args: |
| record: records.TestResultRecord, a copy of the test record for |
| this test, containing all information of the test execution |
| including exception objects. |
| """ |
| logging.info('Reason to skip: %s', record.details) |
| logging.info(RESULT_LINE_TEMPLATE, record.test_name, record.result) |
| self.on_skip(record) |
| |
| def on_skip(self, record): |
| """A function that is executed upon a test being skipped. |
| |
| Implementation is optional. |
| |
| Args: |
| record: records.TestResultRecord, a copy of the test record for |
| this test, containing all information of the test execution |
| including exception objects. |
| """ |
| |
| def _exec_procedure_func(self, func, tr_record): |
| """Executes a procedure function like on_pass, on_fail etc. |
| |
| This function will alter the 'Result' of the test's record if |
| exceptions happened when executing the procedure function, but |
| prevents procedure functions from altering test records themselves |
| by only passing in a copy. |
| |
| This will let signals.TestAbortAll through so abort_all works in all |
| procedure functions. |
| |
| Args: |
| func: The procedure function to be executed. |
| tr_record: The TestResultRecord object associated with the test |
| executed. |
| """ |
| func_name = func.__name__ |
| procedure_name = func_name[1:] if func_name[0] == '_' else func_name |
| with self._log_test_stage(procedure_name): |
| try: |
| # Pass a copy of the record instead of the actual object so that it |
| # will not be modified. |
| func(copy.deepcopy(tr_record)) |
| except signals.TestAbortSignal: |
| raise |
| except Exception as e: |
| logging.exception( |
| 'Exception happened when executing %s for %s.', |
| procedure_name, self.current_test_info.name) |
| tr_record.add_error(procedure_name, e) |
| |
| def record_data(self, content): |
| """Record an entry in test summary file. |
| |
| Sometimes additional data need to be recorded in summary file for |
| debugging or post-test analysis. |
| |
| Each call adds a new entry to the summary file, with no guarantee of |
| its position among the summary file entries. |
| |
| The content should be a dict. If absent, timestamp field is added for |
| ease of parsing later. |
| |
| Args: |
| content: dict, the data to add to summary file. |
| """ |
| if 'timestamp' not in content: |
| content['timestamp'] = utils.get_current_epoch_time() |
| self.summary_writer.dump(content, |
| records.TestSummaryEntryType.USER_DATA) |
| |
| def exec_one_test(self, test_name, test_method): |
| """Executes one test and update test results. |
| |
| Executes setup_test, the test method, and teardown_test; then creates a |
| records.TestResultRecord object with the execution information and adds |
| the record to the test class's test results. |
| |
| Args: |
| test_name: string, Name of the test. |
| test_method: function, The test method to execute. |
| """ |
| tr_record = records.TestResultRecord(test_name, self.TAG) |
| tr_record.uid = getattr(test_method, 'uid', None) |
| tr_record.test_begin() |
| self.current_test_info = runtime_test_info.RuntimeTestInfo( |
| test_name, self.log_path, tr_record) |
| expects.recorder.reset_internal_states(tr_record) |
| logging.info('%s %s', TEST_CASE_TOKEN, test_name) |
| # Did teardown_test throw an error. |
| teardown_test_failed = False |
| try: |
| try: |
| try: |
| self._setup_test(test_name) |
| except signals.TestFailure as e: |
| raise_with_traceback(signals.TestError( |
| e.details, e.extras)) |
| test_method() |
| except (signals.TestPass, signals.TestAbortSignal): |
| raise |
| except Exception: |
| logging.exception('Exception occurred in %s.', |
| self.current_test_info.name) |
| raise |
| finally: |
| before_count = expects.recorder.error_count |
| try: |
| self._teardown_test(test_name) |
| except signals.TestAbortSignal: |
| raise |
| except Exception as e: |
| logging.exception('Exception occurred in %s of %s.', |
| STAGE_NAME_TEARDOWN_TEST, |
| self.current_test_info.name) |
| tr_record.test_error() |
| tr_record.add_error(STAGE_NAME_TEARDOWN_TEST, e) |
| teardown_test_failed = True |
| else: |
| # Check if anything failed by `expects`. |
| if before_count < expects.recorder.error_count: |
| teardown_test_failed = True |
| except (signals.TestFailure, AssertionError) as e: |
| tr_record.test_fail(e) |
| except signals.TestSkip as e: |
| # Test skipped. |
| tr_record.test_skip(e) |
| except signals.TestAbortSignal as e: |
| # Abort signals, pass along. |
| tr_record.test_fail(e) |
| raise |
| except signals.TestPass as e: |
| # Explicit test pass. |
| tr_record.test_pass(e) |
| except Exception as e: |
| # Exception happened during test. |
| tr_record.test_error(e) |
| else: |
| # No exception is thrown from test and teardown, if `expects` has |
| # error, the test should fail with the first error in `expects`. |
| if expects.recorder.has_error and not teardown_test_failed: |
| tr_record.test_fail() |
| # Otherwise the test passed. |
| elif not teardown_test_failed: |
| tr_record.test_pass() |
| finally: |
| tr_record.update_record() |
| try: |
| if tr_record.result in ( |
| records.TestResultEnums.TEST_RESULT_ERROR, |
| records.TestResultEnums.TEST_RESULT_FAIL): |
| self._exec_procedure_func(self._on_fail, tr_record) |
| elif tr_record.result == records.TestResultEnums.TEST_RESULT_PASS: |
| self._exec_procedure_func(self._on_pass, tr_record) |
| elif tr_record.result == records.TestResultEnums.TEST_RESULT_SKIP: |
| self._exec_procedure_func(self._on_skip, tr_record) |
| finally: |
| logging.info(RESULT_LINE_TEMPLATE, tr_record.test_name, |
| tr_record.result) |
| self.results.add_record(tr_record) |
| self.summary_writer.dump(tr_record.to_dict(), |
| records.TestSummaryEntryType.RECORD) |
| self.current_test_info = None |
| |
| def _assert_function_name_in_stack(self, expected_func_name): |
| """Asserts that the current stack contains the given function name.""" |
| current_frame = inspect.currentframe() |
| caller_frames = inspect.getouterframes(current_frame, 2) |
| for caller_frame in caller_frames[2:]: |
| if caller_frame[3] == expected_func_name: |
| return |
| raise Error('"%s" cannot be called outside of %s' % |
| (caller_frames[1][3], expected_func_name)) |
| |
| def generate_tests(self, test_logic, name_func, arg_sets, uid_func=None): |
| """Generates tests in the test class. |
| |
| This function has to be called inside a test class's |
| `self.setup_generated_tests` function. |
| |
| Generated tests are not written down as methods, but as a list of |
| parameter sets. This way we reduce code repetition and improve test |
| scalability. |
| |
| Users can provide an optional function to specify the UID of each test. |
| Not all generated tests are required to have UID. |
| |
| Args: |
| test_logic: function, the common logic shared by all the generated |
| tests. |
| name_func: function, generate a test name according to a set of |
| test arguments. This function should take the same arguments as |
| the test logic function. |
| arg_sets: a list of tuples, each tuple is a set of arguments to be |
| passed to the test logic function and name function. |
| uid_func: function, an optional function that takes the same |
| arguments as the test logic function and returns a string that |
| is the corresponding UID. |
| """ |
| self._assert_function_name_in_stack(STAGE_NAME_SETUP_GENERATED_TESTS) |
| root_msg = 'During test generation of "%s":' % test_logic.__name__ |
| for args in arg_sets: |
| test_name = name_func(*args) |
| if test_name in self.get_existing_test_names(): |
| raise Error( |
| '%s Test name "%s" already exists, cannot be duplicated!' % |
| (root_msg, test_name)) |
| test_func = functools.partial(test_logic, *args) |
| if uid_func is not None: |
| uid = uid_func(*args) |
| if uid is None: |
| logging.warning('%s UID for arg set %s is None.', root_msg, |
| args) |
| else: |
| setattr(test_func, 'uid', uid) |
| self._generated_test_table[test_name] = test_func |
| |
| def _safe_exec_func(self, func, *args): |
| """Executes a function with exception safeguard. |
| |
| This will let signals.TestAbortAll through so abort_all works in all |
| procedure functions. |
| |
| Args: |
| func: Function to be executed. |
| args: Arguments to be passed to the function. |
| |
| Returns: |
| Whatever the function returns. |
| """ |
| try: |
| return func(*args) |
| except signals.TestAbortAll: |
| raise |
| except: |
| logging.exception('Exception happened when executing %s in %s.', |
| func.__name__, self.TAG) |
| |
| def get_existing_test_names(self): |
| """Gets the names of existing tests in the class. |
| |
| A method in the class is considered a test if its name starts with |
| 'test_*'. |
| |
| Note this only gets the names of tests that already exist. If |
| `setup_generated_test` has not happened when this was called, the |
| generated tests won't be listed. |
| |
| Returns: |
| A list of strings, each is a test method name. |
| """ |
| test_names = [] |
| for name, _ in inspect.getmembers(self, callable): |
| if name.startswith('test_'): |
| test_names.append(name) |
| return test_names + list(self._generated_test_table.keys()) |
| |
| def _get_test_methods(self, test_names): |
| """Resolves test method names to bound test methods. |
| |
| Args: |
| test_names: A list of strings, each string is a test method name. |
| |
| Returns: |
| A list of tuples of (string, function). String is the test method |
| name, function is the actual python method implementing its logic. |
| |
| Raises: |
| Error: The test name does not follow naming convention 'test_*'. |
| This can only be caused by user input. |
| """ |
| test_methods = [] |
| for test_name in test_names: |
| if not test_name.startswith('test_'): |
| raise Error('Test method name %s does not follow naming ' |
| 'convention test_*, abort.' % test_name) |
| if hasattr(self, test_name): |
| test_method = getattr(self, test_name) |
| elif test_name in self._generated_test_table: |
| test_method = self._generated_test_table[test_name] |
| else: |
| raise Error('%s does not have test method %s.' % |
| (self.TAG, test_name)) |
| test_methods.append((test_name, test_method)) |
| return test_methods |
| |
| def _skip_remaining_tests(self, exception): |
| """Marks any requested test that has not been executed in a class as |
| skipped. |
| |
| This is useful for handling abort class signal. |
| |
| Args: |
| exception: The exception object that was thrown to trigger the |
| skip. |
| """ |
| for test_name in self.results.requested: |
| if not self.results.is_test_executed(test_name): |
| test_record = records.TestResultRecord(test_name, self.TAG) |
| test_record.test_skip(exception) |
| self.results.add_record(test_record) |
| self.summary_writer.dump(test_record.to_dict(), |
| records.TestSummaryEntryType.RECORD) |
| |
| def run(self, test_names=None): |
| """Runs tests within a test class. |
| |
| One of these test method lists will be executed, shown here in priority |
| order: |
| |
| 1. The test_names list, which is passed from cmd line. Invalid names |
| are guarded by cmd line arg parsing. |
| 2. The self.tests list defined in test class. Invalid names are |
| ignored. |
| 3. All function that matches test method naming convention in the test |
| class. |
| |
| Args: |
| test_names: A list of string that are test method names requested in |
| cmd line. |
| |
| Returns: |
| The test results object of this class. |
| """ |
| logging.log_path = self.log_path |
| # Executes pre-setup procedures, like generating test methods. |
| if not self._setup_generated_tests(): |
| return self.results |
| logging.info('==========> %s <==========', self.TAG) |
| # Devise the actual test methods to run in the test class. |
| if not test_names: |
| if self.tests: |
| # Specified by run list in class. |
| test_names = list(self.tests) |
| else: |
| # No test method specified by user, execute all in test class. |
| test_names = self.get_existing_test_names() |
| self.results.requested = test_names |
| self.summary_writer.dump(self.results.requested_test_names_dict(), |
| records.TestSummaryEntryType.TEST_NAME_LIST) |
| tests = self._get_test_methods(test_names) |
| try: |
| setup_class_result = self._setup_class() |
| if setup_class_result: |
| return setup_class_result |
| # Run tests in order. |
| for test_name, test_method in tests: |
| self.exec_one_test(test_name, test_method) |
| return self.results |
| except signals.TestAbortClass as e: |
| e.details = 'Test class aborted due to: %s' % e.details |
| self._skip_remaining_tests(e) |
| return self.results |
| except signals.TestAbortAll as e: |
| e.details = 'All remaining tests aborted due to: %s' % e.details |
| self._skip_remaining_tests(e) |
| # Piggy-back test results on this exception object so we don't lose |
| # results from this test class. |
| setattr(e, 'results', self.results) |
| raise e |
| finally: |
| self._teardown_class() |
| logging.info('Summary for test class %s: %s', self.TAG, |
| self.results.summary_str()) |
| |
| def _clean_up(self): |
| """The final stage of a test class execution.""" |
| stage_name = STAGE_NAME_CLEAN_UP |
| record = records.TestResultRecord(stage_name, self.TAG) |
| record.test_begin() |
| self.current_test_info = runtime_test_info.RuntimeTestInfo( |
| stage_name, self.log_path, record) |
| expects.recorder.reset_internal_states(record) |
| with self._log_test_stage(stage_name): |
| # Write controller info and summary to summary file. |
| self._record_controller_info() |
| self._controller_manager.unregister_controllers() |
| if expects.recorder.has_error: |
| record.test_error() |
| record.update_record() |
| self.results.add_class_error(record) |
| self.summary_writer.dump(record.to_dict(), |
| records.TestSummaryEntryType.RECORD) |