1# Copyright 2016 Google Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import collections 16import contextlib 17import copy 18import functools 19import inspect 20import logging 21import os 22import sys 23 24from mobly import controller_manager 25from mobly import expects 26from mobly import records 27from mobly import runtime_test_info 28from mobly import signals 29from mobly import utils 30 31# Macro strings for test result reporting. 32TEST_CASE_TOKEN = '[Test]' 33RESULT_LINE_TEMPLATE = TEST_CASE_TOKEN + ' %s %s' 34 35TEST_STAGE_BEGIN_LOG_TEMPLATE = '[{parent_token}]#{child_token} >>> BEGIN >>>' 36TEST_STAGE_END_LOG_TEMPLATE = '[{parent_token}]#{child_token} <<< END <<<' 37 38# Names of execution stages, in the order they happen during test runs. 39STAGE_NAME_PRE_RUN = 'pre_run' 40# Deprecated, use `STAGE_NAME_PRE_RUN` instead. 41STAGE_NAME_SETUP_GENERATED_TESTS = 'setup_generated_tests' 42STAGE_NAME_SETUP_CLASS = 'setup_class' 43STAGE_NAME_SETUP_TEST = 'setup_test' 44STAGE_NAME_TEARDOWN_TEST = 'teardown_test' 45STAGE_NAME_TEARDOWN_CLASS = 'teardown_class' 46STAGE_NAME_CLEAN_UP = 'clean_up' 47 48# Attribute names 49ATTR_REPEAT_CNT = '_repeat_count' 50ATTR_MAX_RETRY_CNT = '_max_retry_count' 51ATTR_MAX_CONSEC_ERROR = '_max_consecutive_error' 52 53 54class Error(Exception): 55 """Raised for exceptions that occurred in BaseTestClass.""" 56 57 58def repeat(count, max_consecutive_error=None): 59 """Decorator for repeating a test case multiple times. 60 61 The BaseTestClass will execute the test cases annotated with this decorator 62 the specified number of time. 63 64 This decorator only stores the information needed for the repeat. It does not 65 execute the repeat. 66 67 Args: 68 count: int, the total number of times to execute the decorated test case. 69 max_consecutive_error: int, the maximum number of consecutively failed 70 iterations allowed. If reached, the remaining iterations is abandoned. 71 By default this is not enabled. 72 73 Returns: 74 The wrapped test function. 75 76 Raises: 77 ValueError, if the user input is invalid. 78 """ 79 if count <= 1: 80 raise ValueError( 81 f'The `count` for `repeat` must be larger than 1, got "{count}".') 82 83 if max_consecutive_error is not None and max_consecutive_error > count: 84 raise ValueError( 85 f'The `max_consecutive_error` ({max_consecutive_error}) for `repeat` ' 86 f'must be smaller than `count` ({count}).') 87 88 def _outer_decorator(func): 89 setattr(func, ATTR_REPEAT_CNT, count) 90 setattr(func, ATTR_MAX_CONSEC_ERROR, max_consecutive_error) 91 92 @functools.wraps(func) 93 def _wrapper(*args): 94 func(*args) 95 96 return _wrapper 97 98 return _outer_decorator 99 100 101def retry(max_count): 102 """Decorator for retrying a test case until it passes. 103 104 The BaseTestClass will keep executing the test cases annotated with this 105 decorator until the test passes, or the maxinum number of iterations have 106 been met. 107 108 This decorator only stores the information needed for the retry. It does not 109 execute the retry. 110 111 Args: 112 max_count: int, the maximum number of times to execute the decorated test 113 case. 114 115 Returns: 116 The wrapped test function. 117 118 Raises: 119 ValueError, if the user input is invalid. 120 """ 121 if max_count <= 1: 122 raise ValueError( 123 f'The `max_count` for `retry` must be larger than 1, got "{max_count}".' 124 ) 125 126 def _outer_decorator(func): 127 setattr(func, ATTR_MAX_RETRY_CNT, max_count) 128 129 @functools.wraps(func) 130 def _wrapper(*args): 131 func(*args) 132 133 return _wrapper 134 135 return _outer_decorator 136 137 138class BaseTestClass: 139 """Base class for all test classes to inherit from. 140 141 This class gets all the controller objects from test_runner and executes 142 the tests requested within itself. 143 144 Most attributes of this class are set at runtime based on the configuration 145 provided. 146 147 The default logger in logging module is set up for each test run. If you 148 want to log info to the test run output file, use `logging` directly, like 149 `logging.info`. 150 151 Attributes: 152 tests: A list of strings, each representing a test method name. 153 TAG: A string used to refer to a test class. Default is the test class 154 name. 155 results: A records.TestResult object for aggregating test results from 156 the execution of tests. 157 controller_configs: dict, controller configs provided by the user via 158 test bed config. 159 current_test_info: RuntimeTestInfo, runtime information on the test 160 currently being executed. 161 root_output_path: string, storage path for output files associated with 162 the entire test run. A test run can have multiple test class 163 executions. This includes the test summary and Mobly log files. 164 log_path: string, storage path for files specific to a single test 165 class execution. 166 test_bed_name: [Deprecated, use 'testbed_name' instead] 167 string, the name of the test bed used by a test run. 168 testbed_name: string, the name of the test bed used by a test run. 169 user_params: dict, custom parameters from user, to be consumed by 170 the test logic. 171 """ 172 173 TAG = None 174 175 def __init__(self, configs): 176 """Constructor of BaseTestClass. 177 178 The constructor takes a config_parser.TestRunConfig object and which has 179 all the information needed to execute this test class, like log_path 180 and controller configurations. For details, see the definition of class 181 config_parser.TestRunConfig. 182 183 Args: 184 configs: A config_parser.TestRunConfig object. 185 """ 186 self.tests = [] 187 class_identifier = self.__class__.__name__ 188 if configs.test_class_name_suffix: 189 class_identifier = '%s_%s' % (class_identifier, 190 configs.test_class_name_suffix) 191 if self.TAG is None: 192 self.TAG = class_identifier 193 # Set params. 194 self.root_output_path = configs.log_path 195 self.log_path = os.path.join(self.root_output_path, class_identifier) 196 utils.create_dir(self.log_path) 197 # Deprecated, use 'testbed_name' 198 self.test_bed_name = configs.test_bed_name 199 self.testbed_name = configs.testbed_name 200 self.user_params = configs.user_params 201 self.results = records.TestResult() 202 self.summary_writer = configs.summary_writer 203 self._generated_test_table = collections.OrderedDict() 204 self._controller_manager = controller_manager.ControllerManager( 205 class_name=self.TAG, controller_configs=configs.controller_configs) 206 self.controller_configs = self._controller_manager.controller_configs 207 208 def unpack_userparams(self, 209 req_param_names=None, 210 opt_param_names=None, 211 **kwargs): 212 """An optional function that unpacks user defined parameters into 213 individual variables. 214 215 After unpacking, the params can be directly accessed with self.xxx. 216 217 If a required param is not provided, an exception is raised. If an 218 optional param is not provided, a warning line will be logged. 219 220 To provide a param, add it in the config file or pass it in as a kwarg. 221 If a param appears in both the config file and kwarg, the value in the 222 config file is used. 223 224 User params from the config file can also be directly accessed in 225 self.user_params. 226 227 Args: 228 req_param_names: A list of names of the required user params. 229 opt_param_names: A list of names of the optional user params. 230 **kwargs: Arguments that provide default values. 231 e.g. unpack_userparams(required_list, opt_list, arg_a='hello') 232 self.arg_a will be 'hello' unless it is specified again in 233 required_list or opt_list. 234 235 Raises: 236 Error: A required user params is not provided. 237 """ 238 req_param_names = req_param_names or [] 239 opt_param_names = opt_param_names or [] 240 for k, v in kwargs.items(): 241 if k in self.user_params: 242 v = self.user_params[k] 243 setattr(self, k, v) 244 for name in req_param_names: 245 if hasattr(self, name): 246 continue 247 if name not in self.user_params: 248 raise Error('Missing required user param "%s" in test ' 249 'configuration.' % name) 250 setattr(self, name, self.user_params[name]) 251 for name in opt_param_names: 252 if hasattr(self, name): 253 continue 254 if name in self.user_params: 255 setattr(self, name, self.user_params[name]) 256 else: 257 logging.warning( 258 'Missing optional user param "%s" in ' 259 'configuration, continue.', name) 260 261 def register_controller(self, module, required=True, min_number=1): 262 """Loads a controller module and returns its loaded devices. 263 264 A Mobly controller module is a Python lib that can be used to control 265 a device, service, or equipment. To be Mobly compatible, a controller 266 module needs to have the following members: 267 268 .. code-block:: python 269 270 def create(configs): 271 [Required] Creates controller objects from configurations. 272 273 Args: 274 configs: A list of serialized data like string/dict. Each 275 element of the list is a configuration for a controller 276 object. 277 278 Returns: 279 A list of objects. 280 281 def destroy(objects): 282 [Required] Destroys controller objects created by the create 283 function. Each controller object shall be properly cleaned up 284 and all the resources held should be released, e.g. memory 285 allocation, sockets, file handlers etc. 286 287 Args: 288 A list of controller objects created by the create function. 289 290 def get_info(objects): 291 [Optional] Gets info from the controller objects used in a test 292 run. The info will be included in test_summary.yaml under 293 the key 'ControllerInfo'. Such information could include unique 294 ID, version, or anything that could be useful for describing the 295 test bed and debugging. 296 297 Args: 298 objects: A list of controller objects created by the create 299 function. 300 301 Returns: 302 A list of json serializable objects: each represents the 303 info of a controller object. The order of the info 304 object should follow that of the input objects. 305 306 Registering a controller module declares a test class's dependency the 307 controller. If the module config exists and the module matches the 308 controller interface, controller objects will be instantiated with 309 corresponding configs. The module should be imported first. 310 311 Args: 312 module: A module that follows the controller module interface. 313 required: A bool. If True, failing to register the specified 314 controller module raises exceptions. If False, the objects 315 failed to instantiate will be skipped. 316 min_number: An integer that is the minimum number of controller 317 objects to be created. Default is one, since you should not 318 register a controller module without expecting at least one 319 object. 320 321 Returns: 322 A list of controller objects instantiated from controller_module, or 323 None if no config existed for this controller and it was not a 324 required controller. 325 326 Raises: 327 ControllerError: 328 * The controller module has already been registered. 329 * The actual number of objects instantiated is less than the 330 * `min_number`. 331 * `required` is True and no corresponding config can be found. 332 * Any other error occurred in the registration process. 333 """ 334 return self._controller_manager.register_controller(module, required, 335 min_number) 336 337 def _record_controller_info(self): 338 # Collect controller information and write to test result. 339 for record in self._controller_manager.get_controller_info_records(): 340 self.results.add_controller_info_record(record) 341 self.summary_writer.dump(record.to_dict(), 342 records.TestSummaryEntryType.CONTROLLER_INFO) 343 344 def _pre_run(self): 345 """Proxy function to guarantee the base implementation of `pre_run` is 346 called. 347 348 Returns: 349 True if setup is successful, False otherwise. 350 """ 351 stage_name = STAGE_NAME_PRE_RUN 352 record = records.TestResultRecord(stage_name, self.TAG) 353 record.test_begin() 354 self.current_test_info = runtime_test_info.RuntimeTestInfo( 355 stage_name, self.log_path, record) 356 try: 357 with self._log_test_stage(stage_name): 358 self.pre_run() 359 # TODO(angli): Remove this context block after the full deprecation of 360 # `setup_generated_tests`. 361 with self._log_test_stage(stage_name): 362 self.setup_generated_tests() 363 return True 364 except Exception as e: 365 logging.exception('%s failed for %s.', stage_name, self.TAG) 366 record.test_error(e) 367 self.results.add_class_error(record) 368 self.summary_writer.dump(record.to_dict(), 369 records.TestSummaryEntryType.RECORD) 370 return False 371 372 def pre_run(self): 373 """Preprocesses that need to be done before setup_class. 374 375 This phase is used to do pre-test processes like generating tests. 376 This is the only place `self.generate_tests` should be called. 377 378 If this function throws an error, the test class will be marked failure 379 and the "Requested" field will be 0 because the number of tests 380 requested is unknown at this point. 381 """ 382 383 def setup_generated_tests(self): 384 """[DEPRECATED] Use `pre_run` instead. 385 386 Preprocesses that need to be done before setup_class. 387 388 This phase is used to do pre-test processes like generating tests. 389 This is the only place `self.generate_tests` should be called. 390 391 If this function throws an error, the test class will be marked failure 392 and the "Requested" field will be 0 because the number of tests 393 requested is unknown at this point. 394 """ 395 396 def _setup_class(self): 397 """Proxy function to guarantee the base implementation of setup_class 398 is called. 399 400 Returns: 401 If `self.results` is returned instead of None, this means something 402 has gone wrong, and the rest of the test class should not execute. 403 """ 404 # Setup for the class. 405 class_record = records.TestResultRecord(STAGE_NAME_SETUP_CLASS, self.TAG) 406 class_record.test_begin() 407 self.current_test_info = runtime_test_info.RuntimeTestInfo( 408 STAGE_NAME_SETUP_CLASS, self.log_path, class_record) 409 expects.recorder.reset_internal_states(class_record) 410 try: 411 with self._log_test_stage(STAGE_NAME_SETUP_CLASS): 412 self.setup_class() 413 except signals.TestAbortSignal: 414 # Throw abort signals to outer try block for handling. 415 raise 416 except Exception as e: 417 # Setup class failed for unknown reasons. 418 # Fail the class and skip all tests. 419 logging.exception('Error in %s#setup_class.', self.TAG) 420 class_record.test_error(e) 421 self.results.add_class_error(class_record) 422 self._exec_procedure_func(self._on_fail, class_record) 423 class_record.update_record() 424 self.summary_writer.dump(class_record.to_dict(), 425 records.TestSummaryEntryType.RECORD) 426 self._skip_remaining_tests(e) 427 return self.results 428 if expects.recorder.has_error: 429 self._exec_procedure_func(self._on_fail, class_record) 430 class_record.test_error() 431 class_record.update_record() 432 self.summary_writer.dump(class_record.to_dict(), 433 records.TestSummaryEntryType.RECORD) 434 self.results.add_class_error(class_record) 435 self._skip_remaining_tests(class_record.termination_signal.exception) 436 return self.results 437 438 def setup_class(self): 439 """Setup function that will be called before executing any test in the 440 class. 441 442 To signal setup failure, use asserts or raise your own exception. 443 444 Errors raised from `setup_class` will trigger `on_fail`. 445 446 Implementation is optional. 447 """ 448 449 def _teardown_class(self): 450 """Proxy function to guarantee the base implementation of 451 teardown_class is called. 452 """ 453 stage_name = STAGE_NAME_TEARDOWN_CLASS 454 record = records.TestResultRecord(stage_name, self.TAG) 455 record.test_begin() 456 self.current_test_info = runtime_test_info.RuntimeTestInfo( 457 stage_name, self.log_path, record) 458 expects.recorder.reset_internal_states(record) 459 try: 460 with self._log_test_stage(stage_name): 461 self.teardown_class() 462 except signals.TestAbortAll as e: 463 setattr(e, 'results', self.results) 464 raise 465 except Exception as e: 466 logging.exception('Error encountered in %s.', stage_name) 467 record.test_error(e) 468 record.update_record() 469 self.results.add_class_error(record) 470 self.summary_writer.dump(record.to_dict(), 471 records.TestSummaryEntryType.RECORD) 472 else: 473 if expects.recorder.has_error: 474 record.test_error() 475 record.update_record() 476 self.results.add_class_error(record) 477 self.summary_writer.dump(record.to_dict(), 478 records.TestSummaryEntryType.RECORD) 479 finally: 480 self._clean_up() 481 482 def teardown_class(self): 483 """Teardown function that will be called after all the selected tests in 484 the test class have been executed. 485 486 Errors raised from `teardown_class` do not trigger `on_fail`. 487 488 Implementation is optional. 489 """ 490 491 @contextlib.contextmanager 492 def _log_test_stage(self, stage_name): 493 """Logs the begin and end of a test stage. 494 495 This context adds two log lines meant for clarifying the boundary of 496 each execution stage in Mobly log. 497 498 Args: 499 stage_name: string, name of the stage to log. 500 """ 501 parent_token = self.current_test_info.name 502 # If the name of the stage is the same as the test name, in which case 503 # the stage is class-level instead of test-level, use the class's 504 # reference tag as the parent token instead. 505 if parent_token == stage_name: 506 parent_token = self.TAG 507 logging.debug( 508 TEST_STAGE_BEGIN_LOG_TEMPLATE.format(parent_token=parent_token, 509 child_token=stage_name)) 510 try: 511 yield 512 finally: 513 logging.debug( 514 TEST_STAGE_END_LOG_TEMPLATE.format(parent_token=parent_token, 515 child_token=stage_name)) 516 517 def _setup_test(self, test_name): 518 """Proxy function to guarantee the base implementation of setup_test is 519 called. 520 """ 521 with self._log_test_stage(STAGE_NAME_SETUP_TEST): 522 self.setup_test() 523 524 def setup_test(self): 525 """Setup function that will be called every time before executing each 526 test method in the test class. 527 528 To signal setup failure, use asserts or raise your own exception. 529 530 Implementation is optional. 531 """ 532 533 def _teardown_test(self, test_name): 534 """Proxy function to guarantee the base implementation of teardown_test 535 is called. 536 """ 537 with self._log_test_stage(STAGE_NAME_TEARDOWN_TEST): 538 self.teardown_test() 539 540 def teardown_test(self): 541 """Teardown function that will be called every time a test method has 542 been executed. 543 544 Implementation is optional. 545 """ 546 547 def _on_fail(self, record): 548 """Proxy function to guarantee the base implementation of on_fail is 549 called. 550 551 Args: 552 record: records.TestResultRecord, a copy of the test record for 553 this test, containing all information of the test execution 554 including exception objects. 555 """ 556 self.on_fail(record) 557 558 def on_fail(self, record): 559 """A function that is executed upon a test failure. 560 561 User implementation is optional. 562 563 Args: 564 record: records.TestResultRecord, a copy of the test record for 565 this test, containing all information of the test execution 566 including exception objects. 567 """ 568 569 def _on_pass(self, record): 570 """Proxy function to guarantee the base implementation of on_pass is 571 called. 572 573 Args: 574 record: records.TestResultRecord, a copy of the test record for 575 this test, containing all information of the test execution 576 including exception objects. 577 """ 578 msg = record.details 579 if msg: 580 logging.info(msg) 581 self.on_pass(record) 582 583 def on_pass(self, record): 584 """A function that is executed upon a test passing. 585 586 Implementation is optional. 587 588 Args: 589 record: records.TestResultRecord, a copy of the test record for 590 this test, containing all information of the test execution 591 including exception objects. 592 """ 593 594 def _on_skip(self, record): 595 """Proxy function to guarantee the base implementation of on_skip is 596 called. 597 598 Args: 599 record: records.TestResultRecord, a copy of the test record for 600 this test, containing all information of the test execution 601 including exception objects. 602 """ 603 logging.info('Reason to skip: %s', record.details) 604 logging.info(RESULT_LINE_TEMPLATE, record.test_name, record.result) 605 self.on_skip(record) 606 607 def on_skip(self, record): 608 """A function that is executed upon a test being skipped. 609 610 Implementation is optional. 611 612 Args: 613 record: records.TestResultRecord, a copy of the test record for 614 this test, containing all information of the test execution 615 including exception objects. 616 """ 617 618 def _exec_procedure_func(self, func, tr_record): 619 """Executes a procedure function like on_pass, on_fail etc. 620 621 This function will alter the 'Result' of the test's record if 622 exceptions happened when executing the procedure function, but 623 prevents procedure functions from altering test records themselves 624 by only passing in a copy. 625 626 This will let signals.TestAbortAll through so abort_all works in all 627 procedure functions. 628 629 Args: 630 func: The procedure function to be executed. 631 tr_record: The TestResultRecord object associated with the test 632 executed. 633 """ 634 func_name = func.__name__ 635 procedure_name = func_name[1:] if func_name[0] == '_' else func_name 636 with self._log_test_stage(procedure_name): 637 try: 638 # Pass a copy of the record instead of the actual object so that it 639 # will not be modified. 640 func(copy.deepcopy(tr_record)) 641 except signals.TestAbortSignal: 642 raise 643 except Exception as e: 644 logging.exception('Exception happened when executing %s for %s.', 645 procedure_name, self.current_test_info.name) 646 tr_record.add_error(procedure_name, e) 647 648 def record_data(self, content): 649 """Record an entry in test summary file. 650 651 Sometimes additional data need to be recorded in summary file for 652 debugging or post-test analysis. 653 654 Each call adds a new entry to the summary file, with no guarantee of 655 its position among the summary file entries. 656 657 The content should be a dict. If absent, timestamp field is added for 658 ease of parsing later. 659 660 Args: 661 content: dict, the data to add to summary file. 662 """ 663 if 'timestamp' not in content: 664 content = content.copy() 665 content['timestamp'] = utils.get_current_epoch_time() 666 self.summary_writer.dump(content, records.TestSummaryEntryType.USER_DATA) 667 668 def _exec_one_test_with_retry(self, test_name, test_method, max_count): 669 """Executes one test and retry the test if needed. 670 671 Repeatedly execute a test case until it passes or the maximum count of 672 iteration has been reached. 673 674 Args: 675 test_name: string, Name of the test. 676 test_method: function, The test method to execute. 677 max_count: int, the maximum number of iterations to execute the test for. 678 """ 679 680 def should_retry(record): 681 return record.result in [ 682 records.TestResultEnums.TEST_RESULT_FAIL, 683 records.TestResultEnums.TEST_RESULT_ERROR, 684 ] 685 686 previous_record = self.exec_one_test(test_name, test_method) 687 688 if not should_retry(previous_record): 689 return 690 691 for i in range(max_count - 1): 692 retry_name = f'{test_name}_retry_{i+1}' 693 new_record = records.TestResultRecord(retry_name, self.TAG) 694 new_record.retry_parent = previous_record 695 previous_record = self.exec_one_test(retry_name, test_method, new_record) 696 if not should_retry(previous_record): 697 break 698 699 def _exec_one_test_with_repeat(self, test_name, test_method, repeat_count, 700 max_consecutive_error): 701 """Repeatedly execute a test case. 702 703 This method performs the action defined by the `repeat` decorator. 704 705 If the number of consecutive failures reach the threshold set by 706 `max_consecutive_error`, the remaining iterations will be abandoned. 707 708 Args: 709 test_name: string, Name of the test. 710 test_method: function, The test method to execute. 711 repeat_count: int, the number of times to repeat the test case. 712 max_consecutive_error: int, the maximum number of consecutive iterations 713 allowed to fail before abandoning the remaining iterations. 714 """ 715 716 consecutive_error_count = 0 717 718 # If max_consecutive_error is not set by user, it is considered the same as 719 # the repeat_count. 720 if max_consecutive_error == 0: 721 max_consecutive_error = repeat_count 722 723 for i in range(repeat_count): 724 new_test_name = f'{test_name}_{i}' 725 record = self.exec_one_test(new_test_name, test_method) 726 if record.result in [ 727 records.TestResultEnums.TEST_RESULT_FAIL, 728 records.TestResultEnums.TEST_RESULT_ERROR, 729 ]: 730 consecutive_error_count += 1 731 else: 732 consecutive_error_count = 0 733 if consecutive_error_count == max_consecutive_error: 734 logging.error( 735 'Repeated test case "%s" has consecutively failed %d iterations, ' 736 'aborting the remaining %d iterations.', test_name, 737 consecutive_error_count, repeat_count - 1 - i) 738 return 739 740 def exec_one_test(self, test_name, test_method, record=None): 741 """Executes one test and update test results. 742 743 Executes setup_test, the test method, and teardown_test; then creates a 744 records.TestResultRecord object with the execution information and adds 745 the record to the test class's test results. 746 747 Args: 748 test_name: string, Name of the test. 749 test_method: function, The test method to execute. 750 record: records.TestResultRecord, optional arg for injecting a record 751 object to use for this test execution. If not set, a new one is created 752 created. This is meant for passing information between consecutive test 753 case execution for retry purposes. Do NOT abuse this for "magical" 754 features. 755 756 Returns: 757 TestResultRecord, the test result record object of the test execution. 758 This object is strictly for read-only purposes. Modifying this record 759 will not change what is reported in the test run's summary yaml file. 760 """ 761 tr_record = record or records.TestResultRecord(test_name, self.TAG) 762 tr_record.uid = getattr(test_method, 'uid', None) 763 tr_record.test_begin() 764 self.current_test_info = runtime_test_info.RuntimeTestInfo( 765 test_name, self.log_path, tr_record) 766 expects.recorder.reset_internal_states(tr_record) 767 logging.info('%s %s', TEST_CASE_TOKEN, test_name) 768 # Did teardown_test throw an error. 769 teardown_test_failed = False 770 try: 771 try: 772 try: 773 self._setup_test(test_name) 774 except signals.TestFailure as e: 775 _, _, traceback = sys.exc_info() 776 raise signals.TestError(e.details, e.extras).with_traceback(traceback) 777 test_method() 778 except (signals.TestPass, signals.TestAbortSignal, signals.TestSkip): 779 raise 780 except Exception: 781 logging.exception('Exception occurred in %s.', 782 self.current_test_info.name) 783 raise 784 finally: 785 before_count = expects.recorder.error_count 786 try: 787 self._teardown_test(test_name) 788 except signals.TestAbortSignal: 789 raise 790 except Exception as e: 791 logging.exception('Exception occurred in %s of %s.', 792 STAGE_NAME_TEARDOWN_TEST, 793 self.current_test_info.name) 794 tr_record.test_error() 795 tr_record.add_error(STAGE_NAME_TEARDOWN_TEST, e) 796 teardown_test_failed = True 797 else: 798 # Check if anything failed by `expects`. 799 if before_count < expects.recorder.error_count: 800 teardown_test_failed = True 801 except (signals.TestFailure, AssertionError) as e: 802 tr_record.test_fail(e) 803 except signals.TestSkip as e: 804 # Test skipped. 805 tr_record.test_skip(e) 806 except signals.TestAbortSignal as e: 807 # Abort signals, pass along. 808 tr_record.test_fail(e) 809 raise 810 except signals.TestPass as e: 811 # Explicit test pass. 812 tr_record.test_pass(e) 813 except Exception as e: 814 # Exception happened during test. 815 tr_record.test_error(e) 816 else: 817 # No exception is thrown from test and teardown, if `expects` has 818 # error, the test should fail with the first error in `expects`. 819 if expects.recorder.has_error and not teardown_test_failed: 820 tr_record.test_fail() 821 # Otherwise the test passed. 822 elif not teardown_test_failed: 823 tr_record.test_pass() 824 finally: 825 tr_record.update_record() 826 try: 827 if tr_record.result in (records.TestResultEnums.TEST_RESULT_ERROR, 828 records.TestResultEnums.TEST_RESULT_FAIL): 829 self._exec_procedure_func(self._on_fail, tr_record) 830 elif tr_record.result == records.TestResultEnums.TEST_RESULT_PASS: 831 self._exec_procedure_func(self._on_pass, tr_record) 832 elif tr_record.result == records.TestResultEnums.TEST_RESULT_SKIP: 833 self._exec_procedure_func(self._on_skip, tr_record) 834 finally: 835 logging.info(RESULT_LINE_TEMPLATE, tr_record.test_name, 836 tr_record.result) 837 self.results.add_record(tr_record) 838 self.summary_writer.dump(tr_record.to_dict(), 839 records.TestSummaryEntryType.RECORD) 840 self.current_test_info = None 841 return tr_record 842 843 def _assert_function_names_in_stack(self, expected_func_names): 844 """Asserts that the current stack contains any of the given function names. 845 """ 846 current_frame = inspect.currentframe() 847 caller_frames = inspect.getouterframes(current_frame, 2) 848 for caller_frame in caller_frames[2:]: 849 if caller_frame[3] in expected_func_names: 850 return 851 raise Error(f"'{caller_frames[1][3]}' cannot be called outside of the " 852 f"following functions: {expected_func_names}.") 853 854 def generate_tests(self, test_logic, name_func, arg_sets, uid_func=None): 855 """Generates tests in the test class. 856 857 This function has to be called inside a test class's `self.pre_run` or 858 `self.setup_generated_tests`. 859 860 Generated tests are not written down as methods, but as a list of 861 parameter sets. This way we reduce code repetition and improve test 862 scalability. 863 864 Users can provide an optional function to specify the UID of each test. 865 Not all generated tests are required to have UID. 866 867 Args: 868 test_logic: function, the common logic shared by all the generated 869 tests. 870 name_func: function, generate a test name according to a set of 871 test arguments. This function should take the same arguments as 872 the test logic function. 873 arg_sets: a list of tuples, each tuple is a set of arguments to be 874 passed to the test logic function and name function. 875 uid_func: function, an optional function that takes the same 876 arguments as the test logic function and returns a string that 877 is the corresponding UID. 878 """ 879 self._assert_function_names_in_stack( 880 [STAGE_NAME_PRE_RUN, STAGE_NAME_SETUP_GENERATED_TESTS]) 881 root_msg = 'During test generation of "%s":' % test_logic.__name__ 882 for args in arg_sets: 883 test_name = name_func(*args) 884 if test_name in self.get_existing_test_names(): 885 raise Error('%s Test name "%s" already exists, cannot be duplicated!' % 886 (root_msg, test_name)) 887 test_func = functools.partial(test_logic, *args) 888 # If the `test_logic` method is decorated by `retry` or `repeat` 889 # decorators, copy the attributes added by the decorators to the 890 # generated test methods as well, so the generated test methods 891 # also have the retry/repeat behavior. 892 for attr_name in (ATTR_MAX_RETRY_CNT, ATTR_MAX_CONSEC_ERROR, 893 ATTR_REPEAT_CNT): 894 attr = getattr(test_logic, attr_name, None) 895 if attr is not None: 896 setattr(test_func, attr_name, attr) 897 if uid_func is not None: 898 uid = uid_func(*args) 899 if uid is None: 900 logging.warning('%s UID for arg set %s is None.', root_msg, args) 901 else: 902 setattr(test_func, 'uid', uid) 903 self._generated_test_table[test_name] = test_func 904 905 def _safe_exec_func(self, func, *args): 906 """Executes a function with exception safeguard. 907 908 This will let signals.TestAbortAll through so abort_all works in all 909 procedure functions. 910 911 Args: 912 func: Function to be executed. 913 args: Arguments to be passed to the function. 914 915 Returns: 916 Whatever the function returns. 917 """ 918 try: 919 return func(*args) 920 except signals.TestAbortAll: 921 raise 922 except Exception: 923 logging.exception('Exception happened when executing %s in %s.', 924 func.__name__, self.TAG) 925 926 def get_existing_test_names(self): 927 """Gets the names of existing tests in the class. 928 929 A method in the class is considered a test if its name starts with 930 'test_*'. 931 932 Note this only gets the names of tests that already exist. If 933 `generate_tests` has not happened when this was called, the 934 generated tests won't be listed. 935 936 Returns: 937 A list of strings, each is a test method name. 938 """ 939 test_names = [] 940 for name, _ in inspect.getmembers(type(self), callable): 941 if name.startswith('test_'): 942 test_names.append(name) 943 return test_names + list(self._generated_test_table.keys()) 944 945 def _get_test_methods(self, test_names): 946 """Resolves test method names to bound test methods. 947 948 Args: 949 test_names: A list of strings, each string is a test method name. 950 951 Returns: 952 A list of tuples of (string, function). String is the test method 953 name, function is the actual python method implementing its logic. 954 955 Raises: 956 Error: The test name does not follow naming convention 'test_*'. 957 This can only be caused by user input. 958 """ 959 test_methods = [] 960 for test_name in test_names: 961 if not test_name.startswith('test_'): 962 raise Error('Test method name %s does not follow naming ' 963 'convention test_*, abort.' % test_name) 964 if hasattr(self, test_name): 965 test_method = getattr(self, test_name) 966 elif test_name in self._generated_test_table: 967 test_method = self._generated_test_table[test_name] 968 else: 969 raise Error('%s does not have test method %s.' % (self.TAG, test_name)) 970 test_methods.append((test_name, test_method)) 971 return test_methods 972 973 def _skip_remaining_tests(self, exception): 974 """Marks any requested test that has not been executed in a class as 975 skipped. 976 977 This is useful for handling abort class signal. 978 979 Args: 980 exception: The exception object that was thrown to trigger the 981 skip. 982 """ 983 for test_name in self.results.requested: 984 if not self.results.is_test_executed(test_name): 985 test_record = records.TestResultRecord(test_name, self.TAG) 986 test_record.test_skip(exception) 987 self.results.add_record(test_record) 988 self.summary_writer.dump(test_record.to_dict(), 989 records.TestSummaryEntryType.RECORD) 990 991 def run(self, test_names=None): 992 """Runs tests within a test class. 993 994 One of these test method lists will be executed, shown here in priority 995 order: 996 997 1. The test_names list, which is passed from cmd line. Invalid names 998 are guarded by cmd line arg parsing. 999 2. The self.tests list defined in test class. Invalid names are 1000 ignored. 1001 3. All function that matches test method naming convention in the test 1002 class. 1003 1004 Args: 1005 test_names: A list of string that are test method names requested in 1006 cmd line. 1007 1008 Returns: 1009 The test results object of this class. 1010 """ 1011 logging.log_path = self.log_path 1012 # Executes pre-setup procedures, like generating test methods. 1013 if not self._pre_run(): 1014 return self.results 1015 logging.info('==========> %s <==========', self.TAG) 1016 # Devise the actual test methods to run in the test class. 1017 if not test_names: 1018 if self.tests: 1019 # Specified by run list in class. 1020 test_names = list(self.tests) 1021 else: 1022 # No test method specified by user, execute all in test class. 1023 test_names = self.get_existing_test_names() 1024 self.results.requested = test_names 1025 self.summary_writer.dump(self.results.requested_test_names_dict(), 1026 records.TestSummaryEntryType.TEST_NAME_LIST) 1027 tests = self._get_test_methods(test_names) 1028 try: 1029 setup_class_result = self._setup_class() 1030 if setup_class_result: 1031 return setup_class_result 1032 # Run tests in order. 1033 for test_name, test_method in tests: 1034 max_consecutive_error = getattr(test_method, ATTR_MAX_CONSEC_ERROR, 0) 1035 repeat_count = getattr(test_method, ATTR_REPEAT_CNT, 0) 1036 max_retry_count = getattr(test_method, ATTR_MAX_RETRY_CNT, 0) 1037 if max_retry_count: 1038 self._exec_one_test_with_retry(test_name, test_method, 1039 max_retry_count) 1040 elif repeat_count: 1041 self._exec_one_test_with_repeat(test_name, test_method, repeat_count, 1042 max_consecutive_error) 1043 else: 1044 self.exec_one_test(test_name, test_method) 1045 return self.results 1046 except signals.TestAbortClass as e: 1047 e.details = 'Test class aborted due to: %s' % e.details 1048 self._skip_remaining_tests(e) 1049 return self.results 1050 except signals.TestAbortAll as e: 1051 e.details = 'All remaining tests aborted due to: %s' % e.details 1052 self._skip_remaining_tests(e) 1053 # Piggy-back test results on this exception object so we don't lose 1054 # results from this test class. 1055 setattr(e, 'results', self.results) 1056 raise e 1057 finally: 1058 self._teardown_class() 1059 logging.info('Summary for test class %s: %s', self.TAG, 1060 self.results.summary_str()) 1061 1062 def _clean_up(self): 1063 """The final stage of a test class execution.""" 1064 stage_name = STAGE_NAME_CLEAN_UP 1065 record = records.TestResultRecord(stage_name, self.TAG) 1066 record.test_begin() 1067 self.current_test_info = runtime_test_info.RuntimeTestInfo( 1068 stage_name, self.log_path, record) 1069 expects.recorder.reset_internal_states(record) 1070 with self._log_test_stage(stage_name): 1071 # Write controller info and summary to summary file. 1072 self._record_controller_info() 1073 self._controller_manager.unregister_controllers() 1074 if expects.recorder.has_error: 1075 record.test_error() 1076 record.update_record() 1077 self.results.add_class_error(record) 1078 self.summary_writer.dump(record.to_dict(), 1079 records.TestSummaryEntryType.RECORD) 1080