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