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