1 #!/usr/bin/env python3.4 2 # 3 # Copyright 2016 - The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 import os 18 19 from acts import asserts 20 from acts import keys 21 from acts import logger 22 from acts import records 23 from acts import signals 24 from acts import test_runner 25 from acts import utils 26 27 # Macro strings for test result reporting 28 TEST_CASE_TOKEN = "[Test Case]" 29 RESULT_LINE_TEMPLATE = TEST_CASE_TOKEN + " %s %s" 30 31 class BaseTestError(Exception): 32 """Raised for exceptions that occured in BaseTestClass.""" 33 34 class BaseTestClass(object): 35 """Base class for all test classes to inherit from. 36 37 This class gets all the controller objects from test_runner and executes 38 the test cases requested within itself. 39 40 Most attributes of this class are set at runtime based on the configuration 41 provided. 42 43 Attributes: 44 tests: A list of strings, each representing a test case name. 45 TAG: A string used to refer to a test class. Default is the test class 46 name. 47 log: A logger object used for logging. 48 results: A records.TestResult object for aggregating test results from 49 the execution of test cases. 50 current_test_name: A string that's the name of the test case currently 51 being executed. If no test is executing, this should 52 be None. 53 """ 54 55 TAG = None 56 57 def __init__(self, configs): 58 self.tests = [] 59 if not self.TAG: 60 self.TAG = self.__class__.__name__ 61 # Set all the controller objects and params. 62 for name, value in configs.items(): 63 setattr(self, name, value) 64 self.results = records.TestResult() 65 self.current_test_name = None 66 67 def __enter__(self): 68 return self 69 70 def __exit__(self, *args): 71 self._exec_func(self.clean_up) 72 73 def unpack_userparams(self, req_param_names=[], opt_param_names=[], 74 **kwargs): 75 """Unpacks user defined parameters in test config into individual 76 variables. 77 78 Instead of accessing the user param with self.user_params["xxx"], the 79 variable can be directly accessed with self.xxx. 80 81 A missing required param will raise an exception. If an optional param 82 is missing, an INFO line will be logged. 83 84 Args: 85 req_param_names: A list of names of the required user params. 86 opt_param_names: A list of names of the optional user params. 87 **kwargs: Arguments that provide default values. 88 e.g. unpack_userparams(required_list, opt_list, arg_a="hello") 89 self.arg_a will be "hello" unless it is specified again in 90 required_list or opt_list. 91 92 Raises: 93 BaseTestError is raised if a required user params is missing from 94 test config. 95 """ 96 for k, v in kwargs.items(): 97 setattr(self, k, v) 98 for name in req_param_names: 99 if name not in self.user_params: 100 raise BaseTestError(("Missing required user param '%s' in test" 101 " configuration.") % name) 102 setattr(self, name, self.user_params[name]) 103 for name in opt_param_names: 104 if name not in self.user_params: 105 self.log.info(("Missing optional user param '%s' in " 106 "configuration, continue."), name) 107 else: 108 setattr(self, name, self.user_params[name]) 109 110 def _setup_class(self): 111 """Proxy function to guarantee the base implementation of setup_class 112 is called. 113 """ 114 return self.setup_class() 115 116 def setup_class(self): 117 """Setup function that will be called before executing any test case in 118 the test class. 119 120 To signal setup failure, return False or raise an exception. If 121 exceptions were raised, the stack trace would appear in log, but the 122 exceptions would not propagate to upper levels. 123 124 Implementation is optional. 125 """ 126 127 def teardown_class(self): 128 """Teardown function that will be called after all the selected test 129 cases in the test class have been executed. 130 131 Implementation is optional. 132 """ 133 134 def _setup_test(self, test_name): 135 """Proxy function to guarantee the base implementation of setup_test is 136 called. 137 """ 138 self.current_test_name = test_name 139 try: 140 # Write test start token to adb log if android device is attached. 141 for ad in self.android_devices: 142 ad.droid.logV("%s BEGIN %s" % (TEST_CASE_TOKEN, test_name)) 143 except: 144 pass 145 return self.setup_test() 146 147 def setup_test(self): 148 """Setup function that will be called every time before executing each 149 test case in the test class. 150 151 To signal setup failure, return False or raise an exception. If 152 exceptions were raised, the stack trace would appear in log, but the 153 exceptions would not propagate to upper levels. 154 155 Implementation is optional. 156 """ 157 158 def _teardown_test(self, test_name): 159 """Proxy function to guarantee the base implementation of teardown_test 160 is called. 161 """ 162 try: 163 # Write test end token to adb log if android device is attached. 164 for ad in self.android_devices: 165 ad.droid.logV("%s END %s" % (TEST_CASE_TOKEN, test_name)) 166 except: 167 pass 168 try: 169 self.teardown_test() 170 finally: 171 self.current_test_name = None 172 173 def teardown_test(self): 174 """Teardown function that will be called every time a test case has 175 been executed. 176 177 Implementation is optional. 178 """ 179 180 def _on_fail(self, record): 181 """Proxy function to guarantee the base implementation of on_fail is 182 called. 183 184 Args: 185 record: The records.TestResultRecord object for the failed test 186 case. 187 """ 188 test_name = record.test_name 189 self.log.error(record.details) 190 begin_time = logger.epoch_to_log_line_timestamp(record.begin_time) 191 self.log.info(RESULT_LINE_TEMPLATE, test_name, record.result) 192 self.on_fail(test_name, begin_time) 193 194 def on_fail(self, test_name, begin_time): 195 """A function that is executed upon a test case failure. 196 197 User implementation is optional. 198 199 Args: 200 test_name: Name of the test that triggered this function. 201 begin_time: Logline format timestamp taken when the test started. 202 """ 203 204 def _on_pass(self, record): 205 """Proxy function to guarantee the base implementation of on_pass is 206 called. 207 208 Args: 209 record: The records.TestResultRecord object for the passed test 210 case. 211 """ 212 test_name = record.test_name 213 begin_time = logger.epoch_to_log_line_timestamp(record.begin_time) 214 msg = record.details 215 if msg: 216 self.log.info(msg) 217 self.log.info(RESULT_LINE_TEMPLATE, test_name, record.result) 218 self.on_pass(test_name, begin_time) 219 220 def on_pass(self, test_name, begin_time): 221 """A function that is executed upon a test case passing. 222 223 Implementation is optional. 224 225 Args: 226 test_name: Name of the test that triggered this function. 227 begin_time: Logline format timestamp taken when the test started. 228 """ 229 230 def _on_skip(self, record): 231 """Proxy function to guarantee the base implementation of on_skip is 232 called. 233 234 Args: 235 record: The records.TestResultRecord object for the skipped test 236 case. 237 """ 238 test_name = record.test_name 239 begin_time = logger.epoch_to_log_line_timestamp(record.begin_time) 240 self.log.info(RESULT_LINE_TEMPLATE, test_name, record.result) 241 self.log.info("Reason to skip: %s", record.details) 242 self.on_skip(test_name, begin_time) 243 244 def on_skip(self, test_name, begin_time): 245 """A function that is executed upon a test case being skipped. 246 247 Implementation is optional. 248 249 Args: 250 test_name: Name of the test that triggered this function. 251 begin_time: Logline format timestamp taken when the test started. 252 """ 253 254 def _on_exception(self, record): 255 """Proxy function to guarantee the base implementation of on_exception 256 is called. 257 258 Args: 259 record: The records.TestResultRecord object for the failed test 260 case. 261 """ 262 test_name = record.test_name 263 self.log.exception(record.details) 264 begin_time = logger.epoch_to_log_line_timestamp(record.begin_time) 265 self.log.info(RESULT_LINE_TEMPLATE, test_name, record.result) 266 self.on_exception(test_name, begin_time) 267 268 def on_exception(self, test_name, begin_time): 269 """A function that is executed upon an unhandled exception from a test 270 case. 271 272 Implementation is optional. 273 274 Args: 275 test_name: Name of the test that triggered this function. 276 begin_time: Logline format timestamp taken when the test started. 277 """ 278 279 def _exec_procedure_func(self, func, tr_record): 280 """Executes a procedure function like on_pass, on_fail etc. 281 282 This function will alternate the 'Result' of the test's record if 283 exceptions happened when executing the procedure function. 284 285 This will let signals.TestAbortAll through so abort_all works in all 286 procedure functions. 287 288 Args: 289 func: The procedure function to be executed. 290 tr_record: The TestResultRecord object associated with the test 291 case executed. 292 """ 293 try: 294 func(tr_record) 295 except signals.TestAbortAll: 296 raise 297 except Exception as e: 298 self.log.exception("Exception happened when executing %s for %s.", 299 func.__name__, self.current_test_name) 300 tr_record.add_error(func.__name__, e) 301 302 def exec_one_testcase(self, test_name, test_func, args, **kwargs): 303 """Executes one test case and update test results. 304 305 Executes one test case, create a records.TestResultRecord object with 306 the execution information, and add the record to the test class's test 307 results. 308 309 Args: 310 test_name: Name of the test. 311 test_func: The test function. 312 args: A tuple of params. 313 kwargs: Extra kwargs. 314 """ 315 is_generate_trigger = False 316 tr_record = records.TestResultRecord(test_name, self.TAG) 317 tr_record.test_begin() 318 self.log.info("%s %s", TEST_CASE_TOKEN, test_name) 319 verdict = None 320 try: 321 ret = self._setup_test(test_name) 322 asserts.assert_true(ret is not False, 323 "Setup for %s failed." % test_name) 324 try: 325 if args or kwargs: 326 verdict = test_func(*args, **kwargs) 327 else: 328 verdict = test_func() 329 finally: 330 self._teardown_test(test_name) 331 except (signals.TestFailure, AssertionError) as e: 332 tr_record.test_fail(e) 333 self._exec_procedure_func(self._on_fail, tr_record) 334 except signals.TestSkip as e: 335 # Test skipped. 336 tr_record.test_skip(e) 337 self._exec_procedure_func(self._on_skip, tr_record) 338 except (signals.TestAbortClass, signals.TestAbortAll) as e: 339 # Abort signals, pass along. 340 tr_record.test_fail(e) 341 raise e 342 except signals.TestPass as e: 343 # Explicit test pass. 344 tr_record.test_pass(e) 345 self._exec_procedure_func(self._on_pass, tr_record) 346 except signals.TestSilent as e: 347 # This is a trigger test for generated tests, suppress reporting. 348 is_generate_trigger = True 349 self.results.requested.remove(test_name) 350 except Exception as e: 351 # Exception happened during test. 352 tr_record.test_unknown(e) 353 self._exec_procedure_func(self._on_exception, tr_record) 354 self._exec_procedure_func(self._on_fail, tr_record) 355 else: 356 # Keep supporting return False for now. 357 # TODO(angli): Deprecate return False support. 358 if verdict or (verdict is None): 359 # Test passed. 360 tr_record.test_pass() 361 self._exec_procedure_func(self._on_pass, tr_record) 362 return 363 # Test failed because it didn't return True. 364 # This should be removed eventually. 365 tr_record.test_fail() 366 self._exec_procedure_func(self._on_fail, tr_record) 367 finally: 368 if not is_generate_trigger: 369 self.results.add_record(tr_record) 370 371 def run_generated_testcases(self, test_func, settings, 372 args=None, kwargs=None, 373 tag="", name_func=None): 374 """Runs generated test cases. 375 376 Generated test cases are not written down as functions, but as a list 377 of parameter sets. This way we reduce code repetition and improve 378 test case scalability. 379 380 Args: 381 test_func: The common logic shared by all these generated test 382 cases. This function should take at least one argument, 383 which is a parameter set. 384 settings: A list of strings representing parameter sets. These are 385 usually json strings that get loaded in the test_func. 386 args: Iterable of additional position args to be passed to 387 test_func. 388 kwargs: Dict of additional keyword args to be passed to test_func 389 tag: Name of this group of generated test cases. Ignored if 390 name_func is provided and operates properly. 391 name_func: A function that takes a test setting and generates a 392 proper test name. The test name should be shorter than 393 utils.MAX_FILENAME_LEN. Names over the limit will be 394 truncated. 395 396 Returns: 397 A list of settings that did not pass. 398 """ 399 args = args or () 400 kwargs = kwargs or {} 401 failed_settings = [] 402 for s in settings: 403 test_name = "{} {}".format(tag, s) 404 if name_func: 405 try: 406 test_name = name_func(s, *args, **kwargs) 407 except: 408 self.log.exception(("Failed to get test name from " 409 "test_func. Fall back to default %s"), 410 test_name) 411 self.results.requested.append(test_name) 412 if len(test_name) > utils.MAX_FILENAME_LEN: 413 test_name = test_name[:utils.MAX_FILENAME_LEN] 414 previous_success_cnt = len(self.results.passed) 415 self.exec_one_testcase(test_name, test_func, (s,) + args, **kwargs) 416 if len(self.results.passed) - previous_success_cnt != 1: 417 failed_settings.append(s) 418 return failed_settings 419 420 def _exec_func(self, func, *args): 421 """Executes a function with exception safeguard. 422 423 This will let signals.TestAbortAll through so abort_all works in all 424 procedure functions. 425 426 Args: 427 func: Function to be executed. 428 args: Arguments to be passed to the function. 429 430 Returns: 431 Whatever the function returns, or False if unhandled exception 432 occured. 433 """ 434 try: 435 return func(*args) 436 except signals.TestAbortAll: 437 raise 438 except: 439 self.log.exception("Exception happened when executing %s in %s.", 440 func.__name__, self.TAG) 441 return False 442 443 def _get_all_test_names(self): 444 """Finds all the function names that match the test case naming 445 convention in this class. 446 447 Returns: 448 A list of strings, each is a test case name. 449 """ 450 test_names = [] 451 for name in dir(self): 452 if name.startswith("test_"): 453 test_names.append(name) 454 return test_names 455 456 def _get_test_funcs(self, test_names): 457 """Obtain the actual functions of test cases based on test names. 458 459 Args: 460 test_names: A list of strings, each string is a test case name. 461 462 Returns: 463 A list of tuples of (string, function). String is the test case 464 name, function is the actual test case function. 465 466 Raises: 467 test_runner.USERError is raised if the test name does not follow 468 naming convention "test_*". This can only be caused by user input 469 here. 470 """ 471 test_funcs = [] 472 for test_name in test_names: 473 if not test_name.startswith("test_"): 474 msg = ("Test case name %s does not follow naming convention " 475 "test_*, abort.") % test_name 476 raise test_runner.USERError(msg) 477 try: 478 test_funcs.append((test_name, getattr(self, test_name))) 479 except AttributeError: 480 self.log.warning("%s does not have test case %s.", self.TAG, 481 test_name) 482 except BaseTestError as e: 483 self.log.warning(str(e)) 484 return test_funcs 485 486 def run(self, test_names=None): 487 """Runs test cases within a test class by the order they appear in the 488 execution list. 489 490 One of these test cases lists will be executed, shown here in priority 491 order: 492 1. The test_names list, which is passed from cmd line. Invalid names 493 are guarded by cmd line arg parsing. 494 2. The self.tests list defined in test class. Invalid names are 495 ignored. 496 3. All function that matches test case naming convention in the test 497 class. 498 499 Args: 500 test_names: A list of string that are test case names requested in 501 cmd line. 502 503 Returns: 504 The test results object of this class. 505 """ 506 self.log.info("==========> %s <==========", self.TAG) 507 # Devise the actual test cases to run in the test class. 508 if not test_names: 509 if self.tests: 510 # Specified by run list in class. 511 test_names = list(self.tests) 512 else: 513 # No test case specified by user, execute all in the test class 514 test_names = self._get_all_test_names() 515 self.results.requested = test_names 516 tests = self._get_test_funcs(test_names) 517 # Setup for the class. 518 try: 519 if self._setup_class() is False: 520 raise signals.TestFailure("Failed to setup %s." % self.TAG) 521 except Exception as e: 522 self.log.exception("Failed to setup %s.", self.TAG) 523 self.results.fail_class(self.TAG, e) 524 self._exec_func(self.teardown_class) 525 return self.results 526 # Run tests in order. 527 try: 528 for test_name, test_func in tests: 529 self.exec_one_testcase(test_name, test_func, self.cli_args) 530 return self.results 531 except signals.TestAbortClass: 532 return self.results 533 except signals.TestAbortAll as e: 534 # Piggy-back test results on this exception object so we don't lose 535 # results from this test class. 536 setattr(e, "results", self.results) 537 raise e 538 finally: 539 self._exec_func(self.teardown_class) 540 self.log.info("Summary for test class %s: %s", self.TAG, 541 self.results.summary_str()) 542 543 def clean_up(self): 544 """A function that is executed upon completion of all tests cases 545 selected in the test class. 546 547 This function should clean up objects initialized in the constructor by 548 user. 549 """ 550