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 re 23import sys 24 25from mobly import controller_manager 26from mobly import expects 27from mobly import records 28from mobly import runtime_test_info 29from mobly import signals 30from mobly import utils 31 32# Macro strings for test result reporting. 33TEST_CASE_TOKEN = '[Test]' 34RESULT_LINE_TEMPLATE = TEST_CASE_TOKEN + ' %s %s' 35TEST_SELECTOR_REGEX_PREFIX = 're:' 36 37TEST_STAGE_BEGIN_LOG_TEMPLATE = '[{parent_token}]#{child_token} >>> BEGIN >>>' 38TEST_STAGE_END_LOG_TEMPLATE = '[{parent_token}]#{child_token} <<< END <<<' 39 40# Names of execution stages, in the order they happen during test runs. 41STAGE_NAME_PRE_RUN = 'pre_run' 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 return True 374 except Exception as e: 375 logging.exception('%s failed for %s.', stage_name, self.TAG) 376 record.test_error(e) 377 self.results.add_class_error(record) 378 self.summary_writer.dump( 379 record.to_dict(), records.TestSummaryEntryType.RECORD 380 ) 381 return False 382 383 def pre_run(self): 384 """Preprocesses that need to be done before setup_class. 385 386 This phase is used to do pre-test processes like generating tests. 387 This is the only place `self.generate_tests` should be called. 388 389 If this function throws an error, the test class will be marked failure 390 and the "Requested" field will be 0 because the number of tests 391 requested is unknown at this point. 392 """ 393 394 def _setup_class(self): 395 """Proxy function to guarantee the base implementation of setup_class 396 is called. 397 398 Returns: 399 If `self.results` is returned instead of None, this means something 400 has gone wrong, and the rest of the test class should not execute. 401 """ 402 # Setup for the class. 403 class_record = records.TestResultRecord(STAGE_NAME_SETUP_CLASS, self.TAG) 404 class_record.test_begin() 405 self.current_test_info = runtime_test_info.RuntimeTestInfo( 406 STAGE_NAME_SETUP_CLASS, self.log_path, class_record 407 ) 408 expects.recorder.reset_internal_states(class_record) 409 try: 410 with self._log_test_stage(STAGE_NAME_SETUP_CLASS): 411 self.setup_class() 412 except signals.TestAbortSignal: 413 # Throw abort signals to outer try block for handling. 414 raise 415 except Exception as e: 416 # Setup class failed for unknown reasons. 417 # Fail the class and skip all tests. 418 logging.exception('Error in %s#setup_class.', self.TAG) 419 class_record.test_error(e) 420 self.results.add_class_error(class_record) 421 self._exec_procedure_func(self._on_fail, class_record) 422 class_record.update_record() 423 self.summary_writer.dump( 424 class_record.to_dict(), records.TestSummaryEntryType.RECORD 425 ) 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( 433 class_record.to_dict(), records.TestSummaryEntryType.RECORD 434 ) 435 self.results.add_class_error(class_record) 436 self._skip_remaining_tests(class_record.termination_signal.exception) 437 return self.results 438 439 def setup_class(self): 440 """Setup function that will be called before executing any test in the 441 class. 442 443 To signal setup failure, use asserts or raise your own exception. 444 445 Errors raised from `setup_class` will trigger `on_fail`. 446 447 Implementation is optional. 448 """ 449 450 def _teardown_class(self): 451 """Proxy function to guarantee the base implementation of 452 teardown_class is called. 453 """ 454 stage_name = STAGE_NAME_TEARDOWN_CLASS 455 record = records.TestResultRecord(stage_name, self.TAG) 456 record.test_begin() 457 self.current_test_info = runtime_test_info.RuntimeTestInfo( 458 stage_name, self.log_path, record 459 ) 460 expects.recorder.reset_internal_states(record) 461 try: 462 with self._log_test_stage(stage_name): 463 self.teardown_class() 464 except signals.TestAbortAll as e: 465 setattr(e, 'results', self.results) 466 raise 467 except Exception as e: 468 logging.exception('Error encountered in %s.', stage_name) 469 record.test_error(e) 470 record.update_record() 471 self.results.add_class_error(record) 472 self.summary_writer.dump( 473 record.to_dict(), records.TestSummaryEntryType.RECORD 474 ) 475 else: 476 if expects.recorder.has_error: 477 record.test_error() 478 record.update_record() 479 self.results.add_class_error(record) 480 self.summary_writer.dump( 481 record.to_dict(), records.TestSummaryEntryType.RECORD 482 ) 483 finally: 484 self._clean_up() 485 486 def teardown_class(self): 487 """Teardown function that will be called after all the selected tests in 488 the test class have been executed. 489 490 Errors raised from `teardown_class` do not trigger `on_fail`. 491 492 Implementation is optional. 493 """ 494 495 @contextlib.contextmanager 496 def _log_test_stage(self, stage_name): 497 """Logs the begin and end of a test stage. 498 499 This context adds two log lines meant for clarifying the boundary of 500 each execution stage in Mobly log. 501 502 Args: 503 stage_name: string, name of the stage to log. 504 """ 505 parent_token = self.current_test_info.name 506 # If the name of the stage is the same as the test name, in which case 507 # the stage is class-level instead of test-level, use the class's 508 # reference tag as the parent token instead. 509 if parent_token == stage_name: 510 parent_token = self.TAG 511 logging.debug( 512 TEST_STAGE_BEGIN_LOG_TEMPLATE.format( 513 parent_token=parent_token, child_token=stage_name 514 ) 515 ) 516 try: 517 yield 518 finally: 519 logging.debug( 520 TEST_STAGE_END_LOG_TEMPLATE.format( 521 parent_token=parent_token, child_token=stage_name 522 ) 523 ) 524 525 def _setup_test(self, test_name): 526 """Proxy function to guarantee the base implementation of setup_test is 527 called. 528 """ 529 with self._log_test_stage(STAGE_NAME_SETUP_TEST): 530 self.setup_test() 531 532 def setup_test(self): 533 """Setup function that will be called every time before executing each 534 test method in the test class. 535 536 To signal setup failure, use asserts or raise your own exception. 537 538 Implementation is optional. 539 """ 540 541 def _teardown_test(self, test_name): 542 """Proxy function to guarantee the base implementation of teardown_test 543 is called. 544 """ 545 with self._log_test_stage(STAGE_NAME_TEARDOWN_TEST): 546 self.teardown_test() 547 548 def teardown_test(self): 549 """Teardown function that will be called every time a test method has 550 been executed. 551 552 Implementation is optional. 553 """ 554 555 def _on_fail(self, record): 556 """Proxy function to guarantee the base implementation of on_fail is 557 called. 558 559 Args: 560 record: records.TestResultRecord, a copy of the test record for 561 this test, containing all information of the test execution 562 including exception objects. 563 """ 564 self.on_fail(record) 565 566 def on_fail(self, record): 567 """A function that is executed upon a test failure. 568 569 User implementation is optional. 570 571 Args: 572 record: records.TestResultRecord, a copy of the test record for 573 this test, containing all information of the test execution 574 including exception objects. 575 """ 576 577 def _on_pass(self, record): 578 """Proxy function to guarantee the base implementation of on_pass is 579 called. 580 581 Args: 582 record: records.TestResultRecord, a copy of the test record for 583 this test, containing all information of the test execution 584 including exception objects. 585 """ 586 msg = record.details 587 if msg: 588 logging.info(msg) 589 self.on_pass(record) 590 591 def on_pass(self, record): 592 """A function that is executed upon a test passing. 593 594 Implementation is optional. 595 596 Args: 597 record: records.TestResultRecord, a copy of the test record for 598 this test, containing all information of the test execution 599 including exception objects. 600 """ 601 602 def _on_skip(self, record): 603 """Proxy function to guarantee the base implementation of on_skip is 604 called. 605 606 Args: 607 record: records.TestResultRecord, a copy of the test record for 608 this test, containing all information of the test execution 609 including exception objects. 610 """ 611 logging.info('Reason to skip: %s', record.details) 612 logging.info(RESULT_LINE_TEMPLATE, record.test_name, record.result) 613 self.on_skip(record) 614 615 def on_skip(self, record): 616 """A function that is executed upon a test being skipped. 617 618 Implementation is optional. 619 620 Args: 621 record: records.TestResultRecord, a copy of the test record for 622 this test, containing all information of the test execution 623 including exception objects. 624 """ 625 626 def _exec_procedure_func(self, func, tr_record): 627 """Executes a procedure function like on_pass, on_fail etc. 628 629 This function will alter the 'Result' of the test's record if 630 exceptions happened when executing the procedure function, but 631 prevents procedure functions from altering test records themselves 632 by only passing in a copy. 633 634 This will let signals.TestAbortAll through so abort_all works in all 635 procedure functions. 636 637 Args: 638 func: The procedure function to be executed. 639 tr_record: The TestResultRecord object associated with the test 640 executed. 641 """ 642 func_name = func.__name__ 643 procedure_name = func_name[1:] if func_name[0] == '_' else func_name 644 with self._log_test_stage(procedure_name): 645 try: 646 # Pass a copy of the record instead of the actual object so that it 647 # will not be modified. 648 func(copy.deepcopy(tr_record)) 649 except signals.TestAbortSignal: 650 raise 651 except Exception as e: 652 logging.exception( 653 'Exception happened when executing %s for %s.', 654 procedure_name, 655 self.current_test_info.name, 656 ) 657 tr_record.add_error(procedure_name, e) 658 659 def record_data(self, content): 660 """Record an entry in test summary file. 661 662 Sometimes additional data need to be recorded in summary file for 663 debugging or post-test analysis. 664 665 Each call adds a new entry to the summary file, with no guarantee of 666 its position among the summary file entries. 667 668 The content should be a dict. If absent, timestamp field is added for 669 ease of parsing later. 670 671 Args: 672 content: dict, the data to add to summary file. 673 """ 674 if 'timestamp' not in content: 675 content = content.copy() 676 content['timestamp'] = utils.get_current_epoch_time() 677 self.summary_writer.dump(content, records.TestSummaryEntryType.USER_DATA) 678 679 def _exec_one_test_with_retry(self, test_name, test_method, max_count): 680 """Executes one test and retry the test if needed. 681 682 Repeatedly execute a test case until it passes or the maximum count of 683 iteration has been reached. 684 685 Args: 686 test_name: string, Name of the test. 687 test_method: function, The test method to execute. 688 max_count: int, the maximum number of iterations to execute the test for. 689 """ 690 691 def should_retry(record): 692 return record.result in [ 693 records.TestResultEnums.TEST_RESULT_FAIL, 694 records.TestResultEnums.TEST_RESULT_ERROR, 695 ] 696 697 previous_record = self.exec_one_test(test_name, test_method) 698 699 if not should_retry(previous_record): 700 return 701 702 for i in range(max_count - 1): 703 retry_name = f'{test_name}_retry_{i+1}' 704 new_record = records.TestResultRecord(retry_name, self.TAG) 705 new_record.retry_parent = previous_record 706 new_record.parent = (previous_record, records.TestParentType.RETRY) 707 previous_record = self.exec_one_test(retry_name, test_method, new_record) 708 if not should_retry(previous_record): 709 break 710 711 def _exec_one_test_with_repeat( 712 self, test_name, test_method, repeat_count, max_consecutive_error 713 ): 714 """Repeatedly execute a test case. 715 716 This method performs the action defined by the `repeat` decorator. 717 718 If the number of consecutive failures reach the threshold set by 719 `max_consecutive_error`, the remaining iterations will be abandoned. 720 721 Args: 722 test_name: string, Name of the test. 723 test_method: function, The test method to execute. 724 repeat_count: int, the number of times to repeat the test case. 725 max_consecutive_error: int, the maximum number of consecutive iterations 726 allowed to fail before abandoning the remaining iterations. 727 """ 728 729 consecutive_error_count = 0 730 731 # If max_consecutive_error is not set by user, it is considered the same as 732 # the repeat_count. 733 if max_consecutive_error == 0: 734 max_consecutive_error = repeat_count 735 736 previous_record = None 737 for i in range(repeat_count): 738 new_test_name = f'{test_name}_{i}' 739 new_record = records.TestResultRecord(new_test_name, self.TAG) 740 if i > 0: 741 new_record.parent = (previous_record, records.TestParentType.REPEAT) 742 previous_record = self.exec_one_test( 743 new_test_name, test_method, new_record 744 ) 745 if previous_record.result in [ 746 records.TestResultEnums.TEST_RESULT_FAIL, 747 records.TestResultEnums.TEST_RESULT_ERROR, 748 ]: 749 consecutive_error_count += 1 750 else: 751 consecutive_error_count = 0 752 753 if consecutive_error_count == max_consecutive_error: 754 logging.error( 755 'Repeated test case "%s" has consecutively failed %d iterations, ' 756 'aborting the remaining %d iterations.', 757 test_name, 758 consecutive_error_count, 759 repeat_count - 1 - i, 760 ) 761 return 762 763 def exec_one_test(self, test_name, test_method, record=None): 764 """Executes one test and update test results. 765 766 Executes setup_test, the test method, and teardown_test; then creates a 767 records.TestResultRecord object with the execution information and adds 768 the record to the test class's test results. 769 770 Args: 771 test_name: string, Name of the test. 772 test_method: function, The test method to execute. 773 record: records.TestResultRecord, optional arg for injecting a record 774 object to use for this test execution. If not set, a new one is created 775 created. This is meant for passing information between consecutive test 776 case execution for retry purposes. Do NOT abuse this for "magical" 777 features. 778 779 Returns: 780 TestResultRecord, the test result record object of the test execution. 781 This object is strictly for read-only purposes. Modifying this record 782 will not change what is reported in the test run's summary yaml file. 783 """ 784 tr_record = record or records.TestResultRecord(test_name, self.TAG) 785 tr_record.uid = getattr(test_method, 'uid', None) 786 tr_record.test_begin() 787 self.current_test_info = runtime_test_info.RuntimeTestInfo( 788 test_name, self.log_path, tr_record 789 ) 790 expects.recorder.reset_internal_states(tr_record) 791 logging.info('%s %s', TEST_CASE_TOKEN, test_name) 792 # Did teardown_test throw an error. 793 teardown_test_failed = False 794 try: 795 try: 796 try: 797 self._setup_test(test_name) 798 except signals.TestFailure as e: 799 _, _, traceback = sys.exc_info() 800 raise signals.TestError(e.details, e.extras).with_traceback(traceback) 801 test_method() 802 except (signals.TestPass, signals.TestAbortSignal, signals.TestSkip): 803 raise 804 except Exception: 805 logging.exception( 806 'Exception occurred in %s.', self.current_test_info.name 807 ) 808 raise 809 finally: 810 before_count = expects.recorder.error_count 811 try: 812 self._teardown_test(test_name) 813 except signals.TestAbortSignal: 814 raise 815 except Exception as e: 816 logging.exception( 817 'Exception occurred in %s of %s.', 818 STAGE_NAME_TEARDOWN_TEST, 819 self.current_test_info.name, 820 ) 821 tr_record.test_error() 822 tr_record.add_error(STAGE_NAME_TEARDOWN_TEST, e) 823 teardown_test_failed = True 824 else: 825 # Check if anything failed by `expects`. 826 if before_count < expects.recorder.error_count: 827 tr_record.test_error() 828 teardown_test_failed = True 829 except (signals.TestFailure, AssertionError) as e: 830 tr_record.test_fail(e) 831 except signals.TestSkip as e: 832 # Test skipped. 833 tr_record.test_skip(e) 834 except signals.TestAbortSignal as e: 835 # Abort signals, pass along. 836 tr_record.test_fail(e) 837 raise 838 except signals.TestPass as e: 839 # Explicit test pass. 840 tr_record.test_pass(e) 841 except Exception as e: 842 # Exception happened during test. 843 tr_record.test_error(e) 844 else: 845 # No exception is thrown from test and teardown, if `expects` has 846 # error, the test should fail with the first error in `expects`. 847 if expects.recorder.has_error and not teardown_test_failed: 848 tr_record.test_fail() 849 # Otherwise the test passed. 850 elif not teardown_test_failed: 851 tr_record.test_pass() 852 finally: 853 tr_record.update_record() 854 try: 855 if tr_record.result in ( 856 records.TestResultEnums.TEST_RESULT_ERROR, 857 records.TestResultEnums.TEST_RESULT_FAIL, 858 ): 859 self._exec_procedure_func(self._on_fail, tr_record) 860 elif tr_record.result == records.TestResultEnums.TEST_RESULT_PASS: 861 self._exec_procedure_func(self._on_pass, tr_record) 862 elif tr_record.result == records.TestResultEnums.TEST_RESULT_SKIP: 863 self._exec_procedure_func(self._on_skip, tr_record) 864 finally: 865 logging.info( 866 RESULT_LINE_TEMPLATE, tr_record.test_name, tr_record.result 867 ) 868 self.results.add_record(tr_record) 869 self.summary_writer.dump( 870 tr_record.to_dict(), records.TestSummaryEntryType.RECORD 871 ) 872 self.current_test_info = None 873 return tr_record 874 875 def _assert_function_names_in_stack(self, expected_func_names): 876 """Asserts that the current stack contains any of the given function names.""" 877 current_frame = inspect.currentframe() 878 caller_frames = inspect.getouterframes(current_frame, 2) 879 for caller_frame in caller_frames[2:]: 880 if caller_frame[3] in expected_func_names: 881 return 882 raise Error( 883 f"'{caller_frames[1][3]}' cannot be called outside of the " 884 f'following functions: {expected_func_names}.' 885 ) 886 887 def generate_tests(self, test_logic, name_func, arg_sets, uid_func=None): 888 """Generates tests in the test class. 889 890 This function has to be called inside a test class's `self.pre_run`. 891 892 Generated tests are not written down as methods, but as a list of 893 parameter sets. This way we reduce code repetition and improve test 894 scalability. 895 896 Users can provide an optional function to specify the UID of each test. 897 Not all generated tests are required to have UID. 898 899 Args: 900 test_logic: function, the common logic shared by all the generated 901 tests. 902 name_func: function, generate a test name according to a set of 903 test arguments. This function should take the same arguments as 904 the test logic function. 905 arg_sets: a list of tuples, each tuple is a set of arguments to be 906 passed to the test logic function and name function. 907 uid_func: function, an optional function that takes the same 908 arguments as the test logic function and returns a string that 909 is the corresponding UID. 910 """ 911 self._assert_function_names_in_stack([STAGE_NAME_PRE_RUN]) 912 root_msg = 'During test generation of "%s":' % test_logic.__name__ 913 for args in arg_sets: 914 test_name = name_func(*args) 915 if test_name in self.get_existing_test_names(): 916 raise Error( 917 '%s Test name "%s" already exists, cannot be duplicated!' 918 % (root_msg, test_name) 919 ) 920 test_func = functools.partial(test_logic, *args) 921 # If the `test_logic` method is decorated by `retry` or `repeat` 922 # decorators, copy the attributes added by the decorators to the 923 # generated test methods as well, so the generated test methods 924 # also have the retry/repeat behavior. 925 for attr_name in ( 926 ATTR_MAX_RETRY_CNT, 927 ATTR_MAX_CONSEC_ERROR, 928 ATTR_REPEAT_CNT, 929 ): 930 attr = getattr(test_logic, attr_name, None) 931 if attr is not None: 932 setattr(test_func, attr_name, attr) 933 if uid_func is not None: 934 uid = uid_func(*args) 935 if uid is None: 936 logging.warning('%s UID for arg set %s is None.', root_msg, args) 937 else: 938 setattr(test_func, 'uid', uid) 939 self._generated_test_table[test_name] = test_func 940 941 def _safe_exec_func(self, func, *args): 942 """Executes a function with exception safeguard. 943 944 This will let signals.TestAbortAll through so abort_all works in all 945 procedure functions. 946 947 Args: 948 func: Function to be executed. 949 args: Arguments to be passed to the function. 950 951 Returns: 952 Whatever the function returns. 953 """ 954 try: 955 return func(*args) 956 except signals.TestAbortAll: 957 raise 958 except Exception: 959 logging.exception( 960 'Exception happened when executing %s in %s.', func.__name__, self.TAG 961 ) 962 963 def get_existing_test_names(self): 964 """Gets the names of existing tests in the class. 965 966 A method in the class is considered a test if its name starts with 967 'test_*'. 968 969 Note this only gets the names of tests that already exist. If 970 `generate_tests` has not happened when this was called, the 971 generated tests won't be listed. 972 973 Returns: 974 A list of strings, each is a test method name. 975 """ 976 test_names = [] 977 for name, _ in inspect.getmembers(type(self), callable): 978 if name.startswith('test_'): 979 test_names.append(name) 980 return test_names + list(self._generated_test_table.keys()) 981 982 def _get_test_methods(self, test_names): 983 """Resolves test method names to bound test methods. 984 985 Args: 986 test_names: A list of strings, each string is a test method name or a 987 regex for matching test names. 988 989 Returns: 990 A list of tuples of (string, function). String is the test method 991 name, function is the actual python method implementing its logic. 992 993 Raises: 994 Error: The test name does not follow naming convention 'test_*'. 995 This can only be caused by user input. 996 """ 997 test_methods = [] 998 # Process the test name selector one by one. 999 for test_name in test_names: 1000 if test_name.startswith(TEST_SELECTOR_REGEX_PREFIX): 1001 # process the selector as a regex. 1002 regex_matching_methods = self._get_regex_matching_test_methods( 1003 test_name.removeprefix(TEST_SELECTOR_REGEX_PREFIX) 1004 ) 1005 test_methods += regex_matching_methods 1006 continue 1007 # process the selector as a regular test name string. 1008 self._assert_valid_test_name(test_name) 1009 if test_name not in self.get_existing_test_names(): 1010 raise Error(f'{self.TAG} does not have test method {test_name}.') 1011 if hasattr(self, test_name): 1012 test_method = getattr(self, test_name) 1013 elif test_name in self._generated_test_table: 1014 test_method = self._generated_test_table[test_name] 1015 test_methods.append((test_name, test_method)) 1016 return test_methods 1017 1018 def _get_regex_matching_test_methods(self, test_name_regex): 1019 matching_name_tuples = [] 1020 for name, method in inspect.getmembers(self, callable): 1021 if ( 1022 name.startswith('test_') 1023 and re.fullmatch(test_name_regex, name) is not None 1024 ): 1025 matching_name_tuples.append((name, method)) 1026 for name, method in self._generated_test_table.items(): 1027 if re.fullmatch(test_name_regex, name) is not None: 1028 self._assert_valid_test_name(name) 1029 matching_name_tuples.append((name, method)) 1030 if not matching_name_tuples: 1031 raise Error( 1032 f'{test_name_regex} does not match with any valid test case ' 1033 f'in {self.TAG}, abort!' 1034 ) 1035 return matching_name_tuples 1036 1037 def _assert_valid_test_name(self, test_name): 1038 if not test_name.startswith('test_'): 1039 raise Error( 1040 'Test method name %s does not follow naming ' 1041 'convention test_*, abort.' % test_name 1042 ) 1043 1044 def _skip_remaining_tests(self, exception): 1045 """Marks any requested test that has not been executed in a class as 1046 skipped. 1047 1048 This is useful for handling abort class signal. 1049 1050 Args: 1051 exception: The exception object that was thrown to trigger the 1052 skip. 1053 """ 1054 for test_name in self.results.requested: 1055 if not self.results.is_test_executed(test_name): 1056 test_record = records.TestResultRecord(test_name, self.TAG) 1057 test_record.test_skip(exception) 1058 self.results.add_record(test_record) 1059 self.summary_writer.dump( 1060 test_record.to_dict(), records.TestSummaryEntryType.RECORD 1061 ) 1062 1063 def run(self, test_names=None): 1064 """Runs tests within a test class. 1065 1066 One of these test method lists will be executed, shown here in priority 1067 order: 1068 1069 1. The test_names list, which is passed from cmd line. Invalid names 1070 are guarded by cmd line arg parsing. 1071 2. The self.tests list defined in test class. Invalid names are 1072 ignored. 1073 3. All function that matches test method naming convention in the test 1074 class. 1075 1076 Args: 1077 test_names: A list of string that are test method names requested in 1078 cmd line. 1079 1080 Returns: 1081 The test results object of this class. 1082 """ 1083 logging.log_path = self.log_path 1084 # Executes pre-setup procedures, like generating test methods. 1085 if not self._pre_run(): 1086 return self.results 1087 logging.info('==========> %s <==========', self.TAG) 1088 # Devise the actual test methods to run in the test class. 1089 if not test_names: 1090 if self.tests: 1091 # Specified by run list in class. 1092 test_names = list(self.tests) 1093 else: 1094 # No test method specified by user, execute all in test class. 1095 test_names = self.get_existing_test_names() 1096 self.results.requested = test_names 1097 self.summary_writer.dump( 1098 self.results.requested_test_names_dict(), 1099 records.TestSummaryEntryType.TEST_NAME_LIST, 1100 ) 1101 tests = self._get_test_methods(test_names) 1102 try: 1103 setup_class_result = self._setup_class() 1104 if setup_class_result: 1105 return setup_class_result 1106 # Run tests in order. 1107 for test_name, test_method in tests: 1108 max_consecutive_error = getattr(test_method, ATTR_MAX_CONSEC_ERROR, 0) 1109 repeat_count = getattr(test_method, ATTR_REPEAT_CNT, 0) 1110 max_retry_count = getattr(test_method, ATTR_MAX_RETRY_CNT, 0) 1111 if max_retry_count: 1112 self._exec_one_test_with_retry( 1113 test_name, test_method, max_retry_count 1114 ) 1115 elif repeat_count: 1116 self._exec_one_test_with_repeat( 1117 test_name, test_method, repeat_count, max_consecutive_error 1118 ) 1119 else: 1120 self.exec_one_test(test_name, test_method) 1121 return self.results 1122 except signals.TestAbortClass as e: 1123 e.details = 'Test class aborted due to: %s' % e.details 1124 self._skip_remaining_tests(e) 1125 return self.results 1126 except signals.TestAbortAll as e: 1127 e.details = 'All remaining tests aborted due to: %s' % e.details 1128 self._skip_remaining_tests(e) 1129 # Piggy-back test results on this exception object so we don't lose 1130 # results from this test class. 1131 setattr(e, 'results', self.results) 1132 raise e 1133 finally: 1134 self._teardown_class() 1135 logging.info( 1136 'Summary for test class %s: %s', self.TAG, self.results.summary_str() 1137 ) 1138 1139 def _clean_up(self): 1140 """The final stage of a test class execution.""" 1141 stage_name = STAGE_NAME_CLEAN_UP 1142 record = records.TestResultRecord(stage_name, self.TAG) 1143 record.test_begin() 1144 self.current_test_info = runtime_test_info.RuntimeTestInfo( 1145 stage_name, self.log_path, record 1146 ) 1147 expects.recorder.reset_internal_states(record) 1148 with self._log_test_stage(stage_name): 1149 # Write controller info and summary to summary file. 1150 self._record_controller_info() 1151 self._controller_manager.unregister_controllers() 1152 if expects.recorder.has_error: 1153 record.test_error() 1154 record.update_record() 1155 self.results.add_class_error(record) 1156 self.summary_writer.dump( 1157 record.to_dict(), records.TestSummaryEntryType.RECORD 1158 ) 1159