1# Copyright 2017 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 logging 16 17from collections import defaultdict 18from enum import Enum 19from mobly import base_test 20from mobly import records 21from mobly import signals 22from mobly import utils 23 24 25class _InstrumentationStructurePrefixes: 26 """Class containing prefixes that structure insturmentation output. 27 28 Android instrumentation generally follows the following format: 29 30 .. code-block:: none 31 32 INSTRUMENTATION_STATUS: ... 33 ... 34 INSTRUMENTATION_STATUS: ... 35 INSTRUMENTATION_STATUS_CODE: ... 36 INSTRUMENTATION_STATUS: ... 37 ... 38 INSTRUMENTATION_STATUS: ... 39 INSTRUMENTATION_STATUS_CODE: ... 40 ... 41 INSTRUMENTATION_RESULT: ... 42 ... 43 INSTRUMENTATION_RESULT: ... 44 ... 45 INSTRUMENTATION_CODE: ... 46 47 This means that these prefixes can be used to guide parsing 48 the output of the instrumentation command into the different 49 instrumetnation test methods. 50 51 Refer to the following Android Framework package for more details: 52 53 .. code-block:: none 54 55 com.android.commands.am.AM 56 57 """ 58 59 STATUS = 'INSTRUMENTATION_STATUS:' 60 STATUS_CODE = 'INSTRUMENTATION_STATUS_CODE:' 61 RESULT = 'INSTRUMENTATION_RESULT:' 62 CODE = 'INSTRUMENTATION_CODE:' 63 FAILED = 'INSTRUMENTATION_FAILED:' 64 65 66class _InstrumentationKnownStatusKeys: 67 """Commonly used keys used in instrumentation output for listing 68 instrumentation test method result properties. 69 70 An instrumenation status line usually contains a key-value pair such as 71 the following: 72 73 .. code-block:: none 74 75 INSTRUMENTATION_STATUS: <key>=<value> 76 77 Some of these key-value pairs are very common and represent test case 78 properties. This mapping is used to handle each of the corresponding 79 key-value pairs different than less important key-value pairs. 80 81 Refer to the following Android Framework packages for more details: 82 83 .. code-block:: none 84 85 android.app.Instrumentation 86 android.support.test.internal.runner.listener.InstrumentationResultPrinter 87 88 TODO: Convert android.support.* to androidx.*, 89 (https://android-developers.googleblog.com/2018/05/hello-world-androidx.html). 90 """ 91 92 CLASS = 'class' 93 ERROR = 'Error' 94 STACK = 'stack' 95 TEST = 'test' 96 STREAM = 'stream' 97 98 99class _InstrumentationStatusCodes: 100 """A mapping of instrumentation status codes to test method results. 101 102 When instrumentation runs, at various points output is created in a series 103 of blocks that terminate as follows: 104 105 .. code-block:: none 106 107 INSTRUMENTATION_STATUS_CODE: 1 108 109 These blocks typically have several status keys in them, and they indicate 110 the progression of a particular instrumentation test method. When the 111 corresponding instrumentation test method finishes, there is generally a 112 line which includes a status code that gives thes the test result. 113 114 The UNKNOWN status code is not an actual status code and is only used to 115 represent that a status code has not yet been read for an instrumentation 116 block. 117 118 Refer to the following Android Framework package for more details: 119 120 .. code-block:: none 121 122 android.support.test.internal.runner.listener.InstrumentationResultPrinter 123 124 TODO: Convert android.support.* to androidx.*, 125 (https://android-developers.googleblog.com/2018/05/hello-world-androidx.html). 126 """ 127 128 UNKNOWN = None 129 OK = '0' 130 START = '1' 131 IN_PROGRESS = '2' 132 ERROR = '-1' 133 FAILURE = '-2' 134 IGNORED = '-3' 135 ASSUMPTION_FAILURE = '-4' 136 137 138class _InstrumentationStatusCodeCategories: 139 """A mapping of instrumentation test method results to categories. 140 141 Aside from the TIMING category, these categories roughly map to Mobly 142 signals and are used for determining how a particular instrumentation test 143 method gets recorded. 144 """ 145 146 TIMING = [ 147 _InstrumentationStatusCodes.START, 148 _InstrumentationStatusCodes.IN_PROGRESS, 149 ] 150 PASS = [ 151 _InstrumentationStatusCodes.OK, 152 ] 153 FAIL = [ 154 _InstrumentationStatusCodes.ERROR, 155 _InstrumentationStatusCodes.FAILURE, 156 ] 157 SKIPPED = [ 158 _InstrumentationStatusCodes.IGNORED, 159 _InstrumentationStatusCodes.ASSUMPTION_FAILURE, 160 ] 161 162 163class _InstrumentationKnownResultKeys: 164 """Commonly used keys for outputting instrumentation errors. 165 166 When instrumentation finishes running all of the instrumentation test 167 methods, a result line will appear as follows: 168 169 .. code-block:: none 170 171 INSTRUMENTATION_RESULT: 172 173 If something wrong happened during the instrumentation run such as an 174 application under test crash, the line will appear similarly as thus: 175 176 .. code-block:: none 177 178 INSTRUMENTATION_RESULT: shortMsg=Process crashed. 179 180 Since these keys indicate that something wrong has happened to the 181 instrumentation run, they should be checked for explicitly. 182 183 Refer to the following documentation page for more information: 184 185 .. code-block:: none 186 187 https://developer.android.com/reference/android/app/ActivityManager.ProcessErrorStateInfo.html 188 189 """ 190 191 LONGMSG = 'longMsg' 192 SHORTMSG = 'shortMsg' 193 194 195class _InstrumentationResultSignals: 196 """Instrumenttion result block strings for signalling run completion. 197 198 The final section of the instrumentation output generally follows this 199 format: 200 201 .. code-block:: none 202 203 INSTRUMENTATION_RESULT: stream= 204 ... 205 INSTRUMENTATION_CODE -1 206 207 Inside of the ellipsed section, one of these signaling strings should be 208 present. If they are not present, this usually means that the 209 instrumentation run has failed in someway such as a crash. Because the 210 final instrumentation block simply summarizes information, simply roughly 211 checking for a particilar string should be sufficient to check to a proper 212 run completion as the contents of the instrumentation result block don't 213 really matter. 214 215 Refer to the following JUnit package for more details: 216 217 .. code-block:: none 218 219 junit.textui.ResultPrinter 220 221 """ 222 223 FAIL = 'FAILURES!!!' 224 PASS = 'OK (' 225 226 227class _InstrumentationBlockStates(Enum): 228 """States used for determing what the parser is currently parsing. 229 230 The parse always starts and ends a block in the UNKNOWN state, which is 231 used to indicate that either a method or a result block (matching the 232 METHOD and RESULT states respectively) are valid follow ups, which means 233 that parser should be checking for a structure prefix that indicates which 234 of those two states it should transition to. If the parser is in the 235 METHOD state, then the parser will be parsing input into test methods. 236 Otherwise, the parse can simply concatenate all the input to check for 237 some final run completion signals. 238 """ 239 240 UNKNOWN = 0 241 METHOD = 1 242 RESULT = 2 243 244 245class _InstrumentationBlock: 246 """Container class for parsed instrumentation output for instrumentation 247 test methods. 248 249 Instrumentation test methods typically follow the follwoing format: 250 251 .. code-block:: none 252 253 INSTRUMENTATION_STATUS: <key>=<value> 254 ... 255 INSTRUMENTATION_STATUS: <key>=<value> 256 INSTRUMENTATION_STATUS_CODE: <status code #> 257 258 The main issue with parsing this however is that the key-value pairs can 259 span multiple lines such as this: 260 261 .. code-block:: none 262 263 INSTRUMENTATION_STATUS: stream= 264 Error in ... 265 ... 266 267 Or, such as this: 268 269 .. code-block:: none 270 271 INSTRUMENTATION_STATUS: stack=... 272 ... 273 274 Because these keys are poentially very long, constant string contatention 275 is potentially inefficent. Instead, this class builds up a buffer to store 276 the raw output until it is processed into an actual test result by the 277 _InstrumentationBlockFormatter class. 278 279 Additionally, this class also serves to store the parser state, which 280 means that the BaseInstrumentationTestClass does not need to keep any 281 potentially volatile instrumentation related state, so multiple 282 instrumentation runs should have completely separate parsing states. 283 284 This class is also used for storing result blocks although very little 285 needs to be done for those. 286 287 Attributes: 288 begin_time: string, optional timestamp for when the test corresponding 289 to the instrumentation block began. 290 current_key: string, the current key that is being parsed, default to 291 _InstrumentationKnownStatusKeys.STREAM. 292 error_message: string, an error message indicating that something 293 unexpected happened during a instrumentatoin test method. 294 known_keys: dict, well known keys that are handled uniquely. 295 prefix: string, a prefix to add to the class name of the 296 instrumentation test methods. 297 previous_instrumentation_block: _InstrumentationBlock, the last parsed 298 instrumentation block. 299 state: _InstrumentationBlockStates, the current state of the parser. 300 status_code: string, the state code for an instrumentation method 301 block. 302 unknown_keys: dict, arbitrary keys that are handled generically. 303 """ 304 305 def __init__( 306 self, 307 state=_InstrumentationBlockStates.UNKNOWN, 308 prefix=None, 309 previous_instrumentation_block=None, 310 ): 311 self.state = state 312 self.prefix = prefix 313 self.previous_instrumentation_block = previous_instrumentation_block 314 if previous_instrumentation_block: 315 # The parser never needs lookback for two previous blocks, 316 # so unset to allow previous blocks to get garbage collected. 317 previous_instrumentation_block.previous_instrumentation_block = None 318 319 self._empty = True 320 self.error_message = '' 321 self.status_code = _InstrumentationStatusCodes.UNKNOWN 322 323 self.current_key = _InstrumentationKnownStatusKeys.STREAM 324 self.known_keys = { 325 _InstrumentationKnownStatusKeys.STREAM: [], 326 _InstrumentationKnownStatusKeys.CLASS: [], 327 _InstrumentationKnownStatusKeys.ERROR: [], 328 _InstrumentationKnownStatusKeys.STACK: [], 329 _InstrumentationKnownStatusKeys.TEST: [], 330 _InstrumentationKnownResultKeys.LONGMSG: [], 331 _InstrumentationKnownResultKeys.SHORTMSG: [], 332 } 333 self.unknown_keys = defaultdict(list) 334 335 self.begin_time = None 336 337 @property 338 def is_empty(self): 339 """Deteremines whether or not anything has been parsed with this 340 instrumentation block. 341 342 Returns: 343 A boolean indicating whether or not the this instrumentation block 344 has parsed and contains any output. 345 """ 346 return self._empty 347 348 def set_error_message(self, error_message): 349 """Sets an error message on an instrumentation block. 350 351 This method is used exclusively to indicate that a test method failed 352 to complete, which is usually cause by a crash of some sort such that 353 the test method is marked as error instead of ignored. 354 355 Args: 356 error_message: string, an error message to be added to the 357 TestResultRecord to explain that something wrong happened. 358 """ 359 self._empty = False 360 self.error_message = error_message 361 362 def _remove_structure_prefix(self, prefix, line): 363 """Helper function for removing the structure prefix for parsing. 364 365 Args: 366 prefix: string, a _InstrumentationStructurePrefixes to remove from 367 the raw output. 368 line: string, the raw line from the instrumentation output. 369 370 Returns: 371 A string containing a key value pair descripting some property 372 of the current instrumentation test method. 373 """ 374 return line[len(prefix) :].strip() 375 376 def set_status_code(self, status_code_line): 377 """Sets the status code for the instrumentation test method, used in 378 determining the test result. 379 380 Args: 381 status_code_line: string, the raw instrumentation output line that 382 contains the status code of the instrumentation block. 383 """ 384 self._empty = False 385 self.status_code = self._remove_structure_prefix( 386 _InstrumentationStructurePrefixes.STATUS_CODE, 387 status_code_line, 388 ) 389 if self.status_code == _InstrumentationStatusCodes.START: 390 self.begin_time = utils.get_current_epoch_time() 391 392 def set_key(self, structure_prefix, key_line): 393 """Sets the current key for the instrumentation block. 394 395 For unknown keys, the key is added to the value list in order to 396 better contextualize the value in the output. 397 398 Args: 399 structure_prefix: string, the structure prefix that was matched 400 and that needs to be removed. 401 key_line: string, the raw instrumentation output line that contains 402 the key-value pair. 403 """ 404 self._empty = False 405 key_value = self._remove_structure_prefix( 406 structure_prefix, 407 key_line, 408 ) 409 if '=' in key_value: 410 (key, value) = key_value.split('=', 1) 411 self.current_key = key 412 if key in self.known_keys: 413 self.known_keys[key].append(value) 414 else: 415 self.unknown_keys[key].append(key_value) 416 417 def add_value(self, line): 418 """Adds unstructured or multi-line value output to the current parsed 419 instrumentation block for outputting later. 420 421 Usually, this will add extra lines to the value list for the current 422 key-value pair. However, sometimes, such as when instrumentation 423 failed to start, output does not follow the structured prefix format. 424 In this case, adding all of the output is still useful so that a user 425 can debug the issue. 426 427 Args: 428 line: string, the raw instrumentation line to append to the value 429 list. 430 """ 431 # Don't count whitespace only lines. 432 if line.strip(): 433 self._empty = False 434 435 if self.current_key in self.known_keys: 436 self.known_keys[self.current_key].append(line) 437 else: 438 self.unknown_keys[self.current_key].append(line) 439 440 def transition_state(self, new_state): 441 """Transitions or sets the current instrumentation block to the new 442 parser state. 443 444 Args: 445 new_state: _InstrumentationBlockStates, the state that the parser 446 should transition to. 447 448 Returns: 449 A new instrumentation block set to the new state, representing 450 the start of parsing a new instrumentation test method. 451 Alternatively, if the current instrumentation block represents the 452 start of parsing a new instrumentation block (state UNKNOWN), then 453 this returns the current instrumentation block set to the now 454 known parsing state. 455 """ 456 if self.state == _InstrumentationBlockStates.UNKNOWN: 457 self.state = new_state 458 return self 459 else: 460 next_block = _InstrumentationBlock( 461 state=new_state, 462 prefix=self.prefix, 463 previous_instrumentation_block=self, 464 ) 465 if self.status_code in _InstrumentationStatusCodeCategories.TIMING: 466 next_block.begin_time = self.begin_time 467 return next_block 468 469 470class _InstrumentationBlockFormatter: 471 """Takes an instrumentation block and converts it into a Mobly test 472 result. 473 """ 474 475 DEFAULT_INSTRUMENTATION_METHOD_NAME = 'instrumentation_method' 476 477 def __init__(self, instrumentation_block): 478 self._prefix = instrumentation_block.prefix 479 self._status_code = instrumentation_block.status_code 480 self._error_message = instrumentation_block.error_message 481 self._known_keys = {} 482 self._unknown_keys = {} 483 for key, value in instrumentation_block.known_keys.items(): 484 self._known_keys[key] = '\n'.join( 485 instrumentation_block.known_keys[key] 486 ).rstrip() 487 for key, value in instrumentation_block.unknown_keys.items(): 488 self._unknown_keys[key] = '\n'.join( 489 instrumentation_block.unknown_keys[key] 490 ).rstrip() 491 self._begin_time = instrumentation_block.begin_time 492 493 def _get_name(self): 494 """Gets the method name of the test method for the instrumentation 495 method block. 496 497 Returns: 498 A string containing the name of the instrumentation test method's 499 test or a default name if no name was parsed. 500 """ 501 if self._known_keys[_InstrumentationKnownStatusKeys.TEST]: 502 return self._known_keys[_InstrumentationKnownStatusKeys.TEST] 503 else: 504 return self.DEFAULT_INSTRUMENTATION_METHOD_NAME 505 506 def _get_class(self): 507 """Gets the class name of the test method for the instrumentation 508 method block. 509 510 Returns: 511 A string containing the class name of the instrumentation test 512 method's test or empty string if no name was parsed. If a prefix 513 was specified, then the prefix will be prepended to the class 514 name. 515 """ 516 class_parts = [ 517 self._prefix, 518 self._known_keys[_InstrumentationKnownStatusKeys.CLASS], 519 ] 520 return '.'.join(filter(None, class_parts)) 521 522 def _get_full_name(self): 523 """Gets the qualified name of the test method corresponding to the 524 instrumentation block. 525 526 Returns: 527 A string containing the fully qualified name of the 528 instrumentation test method. If parts are missing, then degrades 529 steadily. 530 """ 531 full_name_parts = [self._get_class(), self._get_name()] 532 return '#'.join(filter(None, full_name_parts)) 533 534 def _get_details(self): 535 """Gets the output for the detail section of the TestResultRecord. 536 537 Returns: 538 A string to set for a TestResultRecord's details. 539 """ 540 detail_parts = [self._get_full_name(), self._error_message] 541 return '\n'.join(filter(None, detail_parts)) 542 543 def _get_extras(self): 544 """Gets the output for the extras section of the TestResultRecord. 545 546 Returns: 547 A string to set for a TestResultRecord's extras. 548 """ 549 # Add empty line to start key-value pairs on a new line. 550 extra_parts = [''] 551 552 for value in self._unknown_keys.values(): 553 extra_parts.append(value) 554 555 extra_parts.append(self._known_keys[_InstrumentationKnownStatusKeys.STREAM]) 556 extra_parts.append( 557 self._known_keys[_InstrumentationKnownResultKeys.SHORTMSG] 558 ) 559 extra_parts.append( 560 self._known_keys[_InstrumentationKnownResultKeys.LONGMSG] 561 ) 562 extra_parts.append(self._known_keys[_InstrumentationKnownStatusKeys.ERROR]) 563 564 if ( 565 self._known_keys[_InstrumentationKnownStatusKeys.STACK] 566 not in self._known_keys[_InstrumentationKnownStatusKeys.STREAM] 567 ): 568 extra_parts.append( 569 self._known_keys[_InstrumentationKnownStatusKeys.STACK] 570 ) 571 572 return '\n'.join(filter(None, extra_parts)) 573 574 def _is_failed(self): 575 """Determines if the test corresponding to the instrumentation block 576 failed. 577 578 This method can not be used to tell if a test method passed and 579 should not be used for such a purpose. 580 581 Returns: 582 A boolean indicating if the test method failed. 583 """ 584 if self._status_code in _InstrumentationStatusCodeCategories.FAIL: 585 return True 586 elif ( 587 self._known_keys[_InstrumentationKnownStatusKeys.STACK] 588 and self._status_code != _InstrumentationStatusCodes.ASSUMPTION_FAILURE 589 ): 590 return True 591 elif self._known_keys[_InstrumentationKnownStatusKeys.ERROR]: 592 return True 593 elif self._known_keys[_InstrumentationKnownResultKeys.SHORTMSG]: 594 return True 595 elif self._known_keys[_InstrumentationKnownResultKeys.LONGMSG]: 596 return True 597 else: 598 return False 599 600 def create_test_record(self, mobly_test_class): 601 """Creates a TestResultRecord for the instrumentation block. 602 603 Args: 604 mobly_test_class: string, the name of the Mobly test case 605 executing the instrumentation run. 606 607 Returns: 608 A TestResultRecord with an appropriate signals exception 609 representing the instrumentation test method's result status. 610 """ 611 details = self._get_details() 612 extras = self._get_extras() 613 614 tr_record = records.TestResultRecord( 615 t_name=self._get_full_name(), 616 t_class=mobly_test_class, 617 ) 618 if self._begin_time: 619 tr_record.begin_time = self._begin_time 620 621 if self._is_failed(): 622 tr_record.test_fail(e=signals.TestFailure(details=details, extras=extras)) 623 elif self._status_code in _InstrumentationStatusCodeCategories.SKIPPED: 624 tr_record.test_skip(e=signals.TestSkip(details=details, extras=extras)) 625 elif self._status_code in _InstrumentationStatusCodeCategories.PASS: 626 tr_record.test_pass(e=signals.TestPass(details=details, extras=extras)) 627 elif self._status_code in _InstrumentationStatusCodeCategories.TIMING: 628 if self._error_message: 629 tr_record.test_error( 630 e=signals.TestError(details=details, extras=extras) 631 ) 632 else: 633 tr_record = None 634 else: 635 tr_record.test_error(e=signals.TestError(details=details, extras=extras)) 636 if self._known_keys[_InstrumentationKnownStatusKeys.STACK]: 637 tr_record.termination_signal.stacktrace = self._known_keys[ 638 _InstrumentationKnownStatusKeys.STACK 639 ] 640 return tr_record 641 642 def has_completed_result_block_format(self, error_message): 643 """Checks the instrumentation result block for a signal indicating 644 normal completion. 645 646 Args: 647 error_message: string, the error message to give if the 648 instrumentation run did not complete successfully.- 649 650 Returns: 651 A boolean indicating whether or not the instrumentation run passed 652 or failed overall. 653 654 Raises: 655 signals.TestError: Error raised if the instrumentation run did not 656 complete because of a crash or some other issue. 657 """ 658 extras = self._get_extras() 659 if _InstrumentationResultSignals.PASS in extras: 660 return True 661 elif _InstrumentationResultSignals.FAIL in extras: 662 return False 663 else: 664 raise signals.TestError(details=error_message, extras=extras) 665 666 667class InstrumentationTestMixin: 668 """A mixin for Mobly test classes to inherit from for instrumentation tests. 669 670 This class should be used in a subclass of both BaseTestClass and this class 671 in order to provide instrumentation test capabilities. This mixin is 672 explicitly for the case where the underlying BaseTestClass cannot be 673 replaced with BaseInstrumentationTestClass. In general, prefer using 674 BaseInstrumentationTestClass instead. 675 676 Attributes: 677 DEFAULT_INSTRUMENTATION_OPTION_PREFIX: string, the default prefix for 678 instrumentation params contained within user params. 679 DEFAULT_INSTRUMENTATION_ERROR_MESSAGE: string, the default error 680 message to set if something has prevented something in the 681 instrumentation test run from completing properly. 682 """ 683 684 DEFAULT_INSTRUMENTATION_OPTION_PREFIX = 'instrumentation_option_' 685 DEFAULT_INSTRUMENTATION_ERROR_MESSAGE = ( 686 'instrumentation run exited unexpectedly' 687 ) 688 689 def _previous_block_never_completed( 690 self, current_block, previous_block, new_state 691 ): 692 """Checks if the previous instrumentation method block completed. 693 694 Args: 695 current_block: _InstrumentationBlock, the current instrumentation 696 block to check for being a different instrumentation test 697 method. 698 previous_block: _InstrumentationBlock, rhe previous 699 instrumentation block to check for an incomplete status. 700 new_state: _InstrumentationBlockStates, the next state for the 701 parser, used to check for the instrumentation run ending 702 with an incomplete test. 703 704 Returns: 705 A boolean indicating whether the previous instrumentation block 706 completed executing. 707 """ 708 if previous_block: 709 previously_timing_block = ( 710 previous_block.status_code 711 in _InstrumentationStatusCodeCategories.TIMING 712 ) 713 currently_new_block = ( 714 current_block.status_code == _InstrumentationStatusCodes.START 715 or new_state == _InstrumentationBlockStates.RESULT 716 ) 717 return all([previously_timing_block, currently_new_block]) 718 else: 719 return False 720 721 def _create_formatters(self, instrumentation_block, new_state): 722 """Creates the _InstrumentationBlockFormatters for outputting the 723 instrumentation method block that have finished parsing. 724 725 Args: 726 instrumentation_block: _InstrumentationBlock, the current 727 instrumentation method block to create formatters based upon. 728 new_state: _InstrumentationBlockState, the next state that the 729 parser will transition to. 730 731 Returns: 732 A list of the formatters tha need to create and add 733 TestResultRecords to the test results. 734 """ 735 formatters = [] 736 if self._previous_block_never_completed( 737 current_block=instrumentation_block, 738 previous_block=instrumentation_block.previous_instrumentation_block, 739 new_state=new_state, 740 ): 741 instrumentation_block.previous_instrumentation_block.set_error_message( 742 self.DEFAULT_INSTRUMENTATION_ERROR_MESSAGE 743 ) 744 formatters.append( 745 _InstrumentationBlockFormatter( 746 instrumentation_block.previous_instrumentation_block 747 ) 748 ) 749 750 if not instrumentation_block.is_empty: 751 formatters.append(_InstrumentationBlockFormatter(instrumentation_block)) 752 return formatters 753 754 def _transition_instrumentation_block( 755 self, instrumentation_block, new_state=_InstrumentationBlockStates.UNKNOWN 756 ): 757 """Transitions and finishes the current instrumentation block. 758 759 Args: 760 instrumentation_block: _InstrumentationBlock, the current 761 instrumentation block to finish. 762 new_state: _InstrumentationBlockState, the next state for the 763 parser to transition to. 764 765 Returns: 766 The new instrumentation block to use for storing parsed 767 instrumentation output. 768 """ 769 formatters = self._create_formatters(instrumentation_block, new_state) 770 for formatter in formatters: 771 test_record = formatter.create_test_record(self.TAG) 772 if test_record: 773 self.results.add_record(test_record) 774 self.summary_writer.dump( 775 test_record.to_dict(), records.TestSummaryEntryType.RECORD 776 ) 777 return instrumentation_block.transition_state(new_state=new_state) 778 779 def _parse_method_block_line(self, instrumentation_block, line): 780 """Parses the instrumnetation method block's line. 781 782 Args: 783 instrumentation_block: _InstrumentationBlock, the current 784 instrumentation method block. 785 line: string, the raw instrumentation output line to parse. 786 787 Returns: 788 The next instrumentation block, which should be used to continue 789 parsing instrumentation output. 790 """ 791 if line.startswith(_InstrumentationStructurePrefixes.STATUS): 792 instrumentation_block.set_key( 793 _InstrumentationStructurePrefixes.STATUS, line 794 ) 795 return instrumentation_block 796 elif line.startswith(_InstrumentationStructurePrefixes.STATUS_CODE): 797 instrumentation_block.set_status_code(line) 798 return self._transition_instrumentation_block(instrumentation_block) 799 elif line.startswith(_InstrumentationStructurePrefixes.RESULT): 800 # Unexpected transition from method block -> result block 801 instrumentation_block.set_key( 802 _InstrumentationStructurePrefixes.RESULT, line 803 ) 804 return self._parse_result_line( 805 self._transition_instrumentation_block( 806 instrumentation_block, 807 new_state=_InstrumentationBlockStates.RESULT, 808 ), 809 line, 810 ) 811 else: 812 instrumentation_block.add_value(line) 813 return instrumentation_block 814 815 def _parse_result_block_line(self, instrumentation_block, line): 816 """Parses the instrumentation result block's line. 817 818 Args: 819 instrumentation_block: _InstrumentationBlock, the instrumentation 820 result block for the instrumentation run. 821 line: string, the raw instrumentation output to add to the 822 instrumenation result block's _InstrumentationResultBlocki 823 object. 824 825 Returns: 826 The instrumentation result block for the instrumentation run. 827 """ 828 instrumentation_block.add_value(line) 829 return instrumentation_block 830 831 def _parse_unknown_block_line(self, instrumentation_block, line): 832 """Parses a line from the instrumentation output from the UNKNOWN 833 parser state. 834 835 Args: 836 instrumentation_block: _InstrumentationBlock, the current 837 instrumenation block, where the correct categorization it noti 838 yet known. 839 line: string, the raw instrumenation output line to be used to 840 deteremine the correct categorization. 841 842 Returns: 843 The next instrumentation block to continue parsing with. Usually, 844 this is the same instrumentation block but with the state 845 transitioned appropriately. 846 """ 847 if line.startswith(_InstrumentationStructurePrefixes.STATUS): 848 return self._parse_method_block_line( 849 self._transition_instrumentation_block( 850 instrumentation_block, 851 new_state=_InstrumentationBlockStates.METHOD, 852 ), 853 line, 854 ) 855 elif ( 856 line.startswith(_InstrumentationStructurePrefixes.RESULT) 857 or _InstrumentationStructurePrefixes.FAILED in line 858 ): 859 return self._parse_result_block_line( 860 self._transition_instrumentation_block( 861 instrumentation_block, 862 new_state=_InstrumentationBlockStates.RESULT, 863 ), 864 line, 865 ) 866 else: 867 # This would only really execute if instrumentation failed to start. 868 instrumentation_block.add_value(line) 869 return instrumentation_block 870 871 def _parse_line(self, instrumentation_block, line): 872 """Parses an arbitrary line from the instrumentation output based upon 873 the current parser state. 874 875 Args: 876 instrumentation_block: _InstrumentationBlock, an instrumentation 877 block with any of the possible parser states. 878 line: string, the raw instrumentation output line to parse 879 appropriately. 880 881 Returns: 882 The next instrumenation block to continue parsing with. 883 """ 884 if instrumentation_block.state == _InstrumentationBlockStates.METHOD: 885 return self._parse_method_block_line(instrumentation_block, line) 886 elif instrumentation_block.state == _InstrumentationBlockStates.RESULT: 887 return self._parse_result_block_line(instrumentation_block, line) 888 else: 889 return self._parse_unknown_block_line(instrumentation_block, line) 890 891 def _finish_parsing(self, instrumentation_block): 892 """Finishes parsing the instrumentation result block for the final 893 instrumentation run status. 894 895 Args: 896 instrumentation_block: _InstrumentationBlock, the instrumentation 897 result block for the instrumenation run. Potentially, thisi 898 could actually be method block if the instrumentation outputi 899 is malformed. 900 901 Returns: 902 A boolean indicating whether the instrumentation run completed 903 with all the tests passing. 904 905 Raises: 906 signals.TestError: Error raised if the instrumentation failed to 907 complete with either a pass or fail status. 908 """ 909 formatter = _InstrumentationBlockFormatter(instrumentation_block) 910 return formatter.has_completed_result_block_format( 911 self.DEFAULT_INSTRUMENTATION_ERROR_MESSAGE 912 ) 913 914 def parse_instrumentation_options(self, parameters=None): 915 """Returns the options for the instrumentation test from user_params. 916 917 By default, this method assume that the correct instrumentation options 918 all start with DEFAULT_INSTRUMENTATION_OPTION_PREFIX. 919 920 Args: 921 parameters: dict, the key value pairs representing an assortment 922 of parameters including instrumentation options. Usually, 923 this argument will be from self.user_params. 924 925 Returns: 926 A dictionary of options/parameters for the instrumentation tst. 927 """ 928 if parameters is None: 929 return {} 930 931 filtered_parameters = {} 932 for parameter_key, parameter_value in parameters.items(): 933 if parameter_key.startswith(self.DEFAULT_INSTRUMENTATION_OPTION_PREFIX): 934 option_key = parameter_key[ 935 len(self.DEFAULT_INSTRUMENTATION_OPTION_PREFIX) : 936 ] 937 filtered_parameters[option_key] = parameter_value 938 return filtered_parameters 939 940 def run_instrumentation_test( 941 self, device, package, options=None, prefix=None, runner=None 942 ): 943 """Runs instrumentation tests on a device and creates test records. 944 945 Args: 946 device: AndroidDevice, the device to run instrumentation tests on. 947 package: string, the package name of the instrumentation tests. 948 options: dict, Instrumentation options for the instrumentation 949 tests. 950 prefix: string, an optional prefix for parser output for 951 distinguishing between instrumentation test runs. 952 runner: string, the runner to use for the instrumentation package, 953 default to DEFAULT_INSTRUMENTATION_RUNNER. 954 955 Returns: 956 A boolean indicating whether or not all the instrumentation test 957 methods passed. 958 959 Raises: 960 TestError if the instrumentation run crashed or if parsing the 961 output failed. 962 """ 963 # Dictionary hack to allow overwriting the instrumentation_block in the 964 # parse_instrumentation closure 965 instrumentation_block = [_InstrumentationBlock(prefix=prefix)] 966 967 def parse_instrumentation(raw_line): 968 line = raw_line.rstrip().decode('utf-8') 969 logging.info(line) 970 instrumentation_block[0] = self._parse_line( 971 instrumentation_block[0], line 972 ) 973 974 device.adb.instrument( 975 package=package, 976 options=options, 977 runner=runner, 978 handler=parse_instrumentation, 979 ) 980 981 return self._finish_parsing(instrumentation_block[0]) 982 983 984class BaseInstrumentationTestClass( 985 InstrumentationTestMixin, base_test.BaseTestClass 986): 987 """Base class for all instrumentation test classes to inherit from. 988 989 This class extends the BaseTestClass to add functionality to run and parse 990 the output of instrumentation runs. 991 992 Attributes: 993 DEFAULT_INSTRUMENTATION_OPTION_PREFIX: string, the default prefix for 994 instrumentation params contained within user params. 995 DEFAULT_INSTRUMENTATION_ERROR_MESSAGE: string, the default error 996 message to set if something has prevented something in the 997 instrumentation test run from completing properly. 998 """ 999