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__(self, 306 state=_InstrumentationBlockStates.UNKNOWN, 307 prefix=None, 308 previous_instrumentation_block=None): 309 self.state = state 310 self.prefix = prefix 311 self.previous_instrumentation_block = previous_instrumentation_block 312 if previous_instrumentation_block: 313 # The parser never needs lookback for two previous blocks, 314 # so unset to allow previous blocks to get garbage collected. 315 previous_instrumentation_block.previous_instrumentation_block = None 316 317 self._empty = True 318 self.error_message = '' 319 self.status_code = _InstrumentationStatusCodes.UNKNOWN 320 321 self.current_key = _InstrumentationKnownStatusKeys.STREAM 322 self.known_keys = { 323 _InstrumentationKnownStatusKeys.STREAM: [], 324 _InstrumentationKnownStatusKeys.CLASS: [], 325 _InstrumentationKnownStatusKeys.ERROR: [], 326 _InstrumentationKnownStatusKeys.STACK: [], 327 _InstrumentationKnownStatusKeys.TEST: [], 328 _InstrumentationKnownResultKeys.LONGMSG: [], 329 _InstrumentationKnownResultKeys.SHORTMSG: [], 330 } 331 self.unknown_keys = defaultdict(list) 332 333 self.begin_time = None 334 335 @property 336 def is_empty(self): 337 """Deteremines whether or not anything has been parsed with this 338 instrumentation block. 339 340 Returns: 341 A boolean indicating whether or not the this instrumentation block 342 has parsed and contains any output. 343 """ 344 return self._empty 345 346 def set_error_message(self, error_message): 347 """Sets an error message on an instrumentation block. 348 349 This method is used exclusively to indicate that a test method failed 350 to complete, which is usually cause by a crash of some sort such that 351 the test method is marked as error instead of ignored. 352 353 Args: 354 error_message: string, an error message to be added to the 355 TestResultRecord to explain that something wrong happened. 356 """ 357 self._empty = False 358 self.error_message = error_message 359 360 def _remove_structure_prefix(self, prefix, line): 361 """Helper function for removing the structure prefix for parsing. 362 363 Args: 364 prefix: string, a _InstrumentationStructurePrefixes to remove from 365 the raw output. 366 line: string, the raw line from the instrumentation output. 367 368 Returns: 369 A string containing a key value pair descripting some property 370 of the current instrumentation test method. 371 """ 372 return line[len(prefix):].strip() 373 374 def set_status_code(self, status_code_line): 375 """Sets the status code for the instrumentation test method, used in 376 determining the test result. 377 378 Args: 379 status_code_line: string, the raw instrumentation output line that 380 contains the status code of the instrumentation block. 381 """ 382 self._empty = False 383 self.status_code = self._remove_structure_prefix( 384 _InstrumentationStructurePrefixes.STATUS_CODE, 385 status_code_line, 386 ) 387 if self.status_code == _InstrumentationStatusCodes.START: 388 self.begin_time = utils.get_current_epoch_time() 389 390 def set_key(self, structure_prefix, key_line): 391 """Sets the current key for the instrumentation block. 392 393 For unknown keys, the key is added to the value list in order to 394 better contextualize the value in the output. 395 396 Args: 397 structure_prefix: string, the structure prefix that was matched 398 and that needs to be removed. 399 key_line: string, the raw instrumentation output line that contains 400 the key-value pair. 401 """ 402 self._empty = False 403 key_value = self._remove_structure_prefix( 404 structure_prefix, 405 key_line, 406 ) 407 if '=' in key_value: 408 (key, value) = key_value.split('=', 1) 409 self.current_key = key 410 if key in self.known_keys: 411 self.known_keys[key].append(value) 412 else: 413 self.unknown_keys[key].append(key_value) 414 415 def add_value(self, line): 416 """Adds unstructured or multi-line value output to the current parsed 417 instrumentation block for outputting later. 418 419 Usually, this will add extra lines to the value list for the current 420 key-value pair. However, sometimes, such as when instrumentation 421 failed to start, output does not follow the structured prefix format. 422 In this case, adding all of the output is still useful so that a user 423 can debug the issue. 424 425 Args: 426 line: string, the raw instrumentation line to append to the value 427 list. 428 """ 429 # Don't count whitespace only lines. 430 if line.strip(): 431 self._empty = False 432 433 if self.current_key in self.known_keys: 434 self.known_keys[self.current_key].append(line) 435 else: 436 self.unknown_keys[self.current_key].append(line) 437 438 def transition_state(self, new_state): 439 """Transitions or sets the current instrumentation block to the new 440 parser state. 441 442 Args: 443 new_state: _InstrumentationBlockStates, the state that the parser 444 should transition to. 445 446 Returns: 447 A new instrumentation block set to the new state, representing 448 the start of parsing a new instrumentation test method. 449 Alternatively, if the current instrumentation block represents the 450 start of parsing a new instrumentation block (state UNKNOWN), then 451 this returns the current instrumentation block set to the now 452 known parsing state. 453 """ 454 if self.state == _InstrumentationBlockStates.UNKNOWN: 455 self.state = new_state 456 return self 457 else: 458 next_block = _InstrumentationBlock( 459 state=new_state, 460 prefix=self.prefix, 461 previous_instrumentation_block=self, 462 ) 463 if self.status_code in _InstrumentationStatusCodeCategories.TIMING: 464 next_block.begin_time = self.begin_time 465 return next_block 466 467 468class _InstrumentationBlockFormatter: 469 """Takes an instrumentation block and converts it into a Mobly test 470 result. 471 """ 472 473 DEFAULT_INSTRUMENTATION_METHOD_NAME = 'instrumentation_method' 474 475 def __init__(self, instrumentation_block): 476 self._prefix = instrumentation_block.prefix 477 self._status_code = instrumentation_block.status_code 478 self._error_message = instrumentation_block.error_message 479 self._known_keys = {} 480 self._unknown_keys = {} 481 for key, value in instrumentation_block.known_keys.items(): 482 self._known_keys[key] = '\n'.join( 483 instrumentation_block.known_keys[key]).rstrip() 484 for key, value in instrumentation_block.unknown_keys.items(): 485 self._unknown_keys[key] = '\n'.join( 486 instrumentation_block.unknown_keys[key]).rstrip() 487 self._begin_time = instrumentation_block.begin_time 488 489 def _get_name(self): 490 """Gets the method name of the test method for the instrumentation 491 method block. 492 493 Returns: 494 A string containing the name of the instrumentation test method's 495 test or a default name if no name was parsed. 496 """ 497 if self._known_keys[_InstrumentationKnownStatusKeys.TEST]: 498 return self._known_keys[_InstrumentationKnownStatusKeys.TEST] 499 else: 500 return self.DEFAULT_INSTRUMENTATION_METHOD_NAME 501 502 def _get_class(self): 503 """Gets the class name of the test method for the instrumentation 504 method block. 505 506 Returns: 507 A string containing the class name of the instrumentation test 508 method's test or empty string if no name was parsed. If a prefix 509 was specified, then the prefix will be prepended to the class 510 name. 511 """ 512 class_parts = [ 513 self._prefix, self._known_keys[_InstrumentationKnownStatusKeys.CLASS] 514 ] 515 return '.'.join(filter(None, class_parts)) 516 517 def _get_full_name(self): 518 """Gets the qualified name of the test method corresponding to the 519 instrumentation block. 520 521 Returns: 522 A string containing the fully qualified name of the 523 instrumentation test method. If parts are missing, then degrades 524 steadily. 525 """ 526 full_name_parts = [self._get_class(), self._get_name()] 527 return '#'.join(filter(None, full_name_parts)) 528 529 def _get_details(self): 530 """Gets the output for the detail section of the TestResultRecord. 531 532 Returns: 533 A string to set for a TestResultRecord's details. 534 """ 535 detail_parts = [self._get_full_name(), self._error_message] 536 return '\n'.join(filter(None, detail_parts)) 537 538 def _get_extras(self): 539 """Gets the output for the extras section of the TestResultRecord. 540 541 Returns: 542 A string to set for a TestResultRecord's extras. 543 """ 544 # Add empty line to start key-value pairs on a new line. 545 extra_parts = [''] 546 547 for value in self._unknown_keys.values(): 548 extra_parts.append(value) 549 550 extra_parts.append(self._known_keys[_InstrumentationKnownStatusKeys.STREAM]) 551 extra_parts.append( 552 self._known_keys[_InstrumentationKnownResultKeys.SHORTMSG]) 553 extra_parts.append( 554 self._known_keys[_InstrumentationKnownResultKeys.LONGMSG]) 555 extra_parts.append(self._known_keys[_InstrumentationKnownStatusKeys.ERROR]) 556 557 if self._known_keys[ 558 _InstrumentationKnownStatusKeys.STACK] not in self._known_keys[ 559 _InstrumentationKnownStatusKeys.STREAM]: 560 extra_parts.append( 561 self._known_keys[_InstrumentationKnownStatusKeys.STACK]) 562 563 return '\n'.join(filter(None, extra_parts)) 564 565 def _is_failed(self): 566 """Determines if the test corresponding to the instrumentation block 567 failed. 568 569 This method can not be used to tell if a test method passed and 570 should not be used for such a purpose. 571 572 Returns: 573 A boolean indicating if the test method failed. 574 """ 575 if self._status_code in _InstrumentationStatusCodeCategories.FAIL: 576 return True 577 elif (self._known_keys[_InstrumentationKnownStatusKeys.STACK] and 578 self._status_code != _InstrumentationStatusCodes.ASSUMPTION_FAILURE): 579 return True 580 elif self._known_keys[_InstrumentationKnownStatusKeys.ERROR]: 581 return True 582 elif self._known_keys[_InstrumentationKnownResultKeys.SHORTMSG]: 583 return True 584 elif self._known_keys[_InstrumentationKnownResultKeys.LONGMSG]: 585 return True 586 else: 587 return False 588 589 def create_test_record(self, mobly_test_class): 590 """Creates a TestResultRecord for the instrumentation block. 591 592 Args: 593 mobly_test_class: string, the name of the Mobly test case 594 executing the instrumentation run. 595 596 Returns: 597 A TestResultRecord with an appropriate signals exception 598 representing the instrumentation test method's result status. 599 """ 600 details = self._get_details() 601 extras = self._get_extras() 602 603 tr_record = records.TestResultRecord( 604 t_name=self._get_full_name(), 605 t_class=mobly_test_class, 606 ) 607 if self._begin_time: 608 tr_record.begin_time = self._begin_time 609 610 if self._is_failed(): 611 tr_record.test_fail(e=signals.TestFailure(details=details, extras=extras)) 612 elif self._status_code in _InstrumentationStatusCodeCategories.SKIPPED: 613 tr_record.test_skip(e=signals.TestSkip(details=details, extras=extras)) 614 elif self._status_code in _InstrumentationStatusCodeCategories.PASS: 615 tr_record.test_pass(e=signals.TestPass(details=details, extras=extras)) 616 elif self._status_code in _InstrumentationStatusCodeCategories.TIMING: 617 if self._error_message: 618 tr_record.test_error( 619 e=signals.TestError(details=details, extras=extras)) 620 else: 621 tr_record = None 622 else: 623 tr_record.test_error(e=signals.TestError(details=details, extras=extras)) 624 if self._known_keys[_InstrumentationKnownStatusKeys.STACK]: 625 tr_record.termination_signal.stacktrace = self._known_keys[ 626 _InstrumentationKnownStatusKeys.STACK] 627 return tr_record 628 629 def has_completed_result_block_format(self, error_message): 630 """Checks the instrumentation result block for a signal indicating 631 normal completion. 632 633 Args: 634 error_message: string, the error message to give if the 635 instrumentation run did not complete successfully.- 636 637 Returns: 638 A boolean indicating whether or not the instrumentation run passed 639 or failed overall. 640 641 Raises: 642 signals.TestError: Error raised if the instrumentation run did not 643 complete because of a crash or some other issue. 644 """ 645 extras = self._get_extras() 646 if _InstrumentationResultSignals.PASS in extras: 647 return True 648 elif _InstrumentationResultSignals.FAIL in extras: 649 return False 650 else: 651 raise signals.TestError(details=error_message, extras=extras) 652 653 654class InstrumentationTestMixin: 655 """A mixin for Mobly test classes to inherit from for instrumentation tests. 656 657 This class should be used in a subclass of both BaseTestClass and this class 658 in order to provide instrumentation test capabilities. This mixin is 659 explicitly for the case where the underlying BaseTestClass cannot be 660 replaced with BaseInstrumentationTestClass. In general, prefer using 661 BaseInstrumentationTestClass instead. 662 663 Attributes: 664 DEFAULT_INSTRUMENTATION_OPTION_PREFIX: string, the default prefix for 665 instrumentation params contained within user params. 666 DEFAULT_INSTRUMENTATION_ERROR_MESSAGE: string, the default error 667 message to set if something has prevented something in the 668 instrumentation test run from completing properly. 669 """ 670 671 DEFAULT_INSTRUMENTATION_OPTION_PREFIX = 'instrumentation_option_' 672 DEFAULT_INSTRUMENTATION_ERROR_MESSAGE = ('instrumentation run exited ' 673 'unexpectedly') 674 675 def _previous_block_never_completed(self, current_block, previous_block, 676 new_state): 677 """Checks if the previous instrumentation method block completed. 678 679 Args: 680 current_block: _InstrumentationBlock, the current instrumentation 681 block to check for being a different instrumentation test 682 method. 683 previous_block: _InstrumentationBlock, rhe previous 684 instrumentation block to check for an incomplete status. 685 new_state: _InstrumentationBlockStates, the next state for the 686 parser, used to check for the instrumentation run ending 687 with an incomplete test. 688 689 Returns: 690 A boolean indicating whether the previous instrumentation block 691 completed executing. 692 """ 693 if previous_block: 694 previously_timing_block = (previous_block.status_code 695 in _InstrumentationStatusCodeCategories.TIMING) 696 currently_new_block = (current_block.status_code 697 == _InstrumentationStatusCodes.START or 698 new_state == _InstrumentationBlockStates.RESULT) 699 return all([previously_timing_block, currently_new_block]) 700 else: 701 return False 702 703 def _create_formatters(self, instrumentation_block, new_state): 704 """Creates the _InstrumentationBlockFormatters for outputting the 705 instrumentation method block that have finished parsing. 706 707 Args: 708 instrumentation_block: _InstrumentationBlock, the current 709 instrumentation method block to create formatters based upon. 710 new_state: _InstrumentationBlockState, the next state that the 711 parser will transition to. 712 713 Returns: 714 A list of the formatters tha need to create and add 715 TestResultRecords to the test results. 716 """ 717 formatters = [] 718 if self._previous_block_never_completed( 719 current_block=instrumentation_block, 720 previous_block=instrumentation_block.previous_instrumentation_block, 721 new_state=new_state): 722 instrumentation_block.previous_instrumentation_block.set_error_message( 723 self.DEFAULT_INSTRUMENTATION_ERROR_MESSAGE) 724 formatters.append( 725 _InstrumentationBlockFormatter( 726 instrumentation_block.previous_instrumentation_block)) 727 728 if not instrumentation_block.is_empty: 729 formatters.append(_InstrumentationBlockFormatter(instrumentation_block)) 730 return formatters 731 732 def _transition_instrumentation_block( 733 self, 734 instrumentation_block, 735 new_state=_InstrumentationBlockStates.UNKNOWN): 736 """Transitions and finishes the current instrumentation block. 737 738 Args: 739 instrumentation_block: _InstrumentationBlock, the current 740 instrumentation block to finish. 741 new_state: _InstrumentationBlockState, the next state for the 742 parser to transition to. 743 744 Returns: 745 The new instrumentation block to use for storing parsed 746 instrumentation output. 747 """ 748 formatters = self._create_formatters(instrumentation_block, new_state) 749 for formatter in formatters: 750 test_record = formatter.create_test_record(self.TAG) 751 if test_record: 752 self.results.add_record(test_record) 753 self.summary_writer.dump(test_record.to_dict(), 754 records.TestSummaryEntryType.RECORD) 755 return instrumentation_block.transition_state(new_state=new_state) 756 757 def _parse_method_block_line(self, instrumentation_block, line): 758 """Parses the instrumnetation method block's line. 759 760 Args: 761 instrumentation_block: _InstrumentationBlock, the current 762 instrumentation method block. 763 line: string, the raw instrumentation output line to parse. 764 765 Returns: 766 The next instrumentation block, which should be used to continue 767 parsing instrumentation output. 768 """ 769 if line.startswith(_InstrumentationStructurePrefixes.STATUS): 770 instrumentation_block.set_key(_InstrumentationStructurePrefixes.STATUS, 771 line) 772 return instrumentation_block 773 elif line.startswith(_InstrumentationStructurePrefixes.STATUS_CODE): 774 instrumentation_block.set_status_code(line) 775 return self._transition_instrumentation_block(instrumentation_block) 776 elif line.startswith(_InstrumentationStructurePrefixes.RESULT): 777 # Unexpected transition from method block -> result block 778 instrumentation_block.set_key(_InstrumentationStructurePrefixes.RESULT, 779 line) 780 return self._parse_result_line( 781 self._transition_instrumentation_block( 782 instrumentation_block, 783 new_state=_InstrumentationBlockStates.RESULT, 784 ), 785 line, 786 ) 787 else: 788 instrumentation_block.add_value(line) 789 return instrumentation_block 790 791 def _parse_result_block_line(self, instrumentation_block, line): 792 """Parses the instrumentation result block's line. 793 794 Args: 795 instrumentation_block: _InstrumentationBlock, the instrumentation 796 result block for the instrumentation run. 797 line: string, the raw instrumentation output to add to the 798 instrumenation result block's _InstrumentationResultBlocki 799 object. 800 801 Returns: 802 The instrumentation result block for the instrumentation run. 803 """ 804 instrumentation_block.add_value(line) 805 return instrumentation_block 806 807 def _parse_unknown_block_line(self, instrumentation_block, line): 808 """Parses a line from the instrumentation output from the UNKNOWN 809 parser state. 810 811 Args: 812 instrumentation_block: _InstrumentationBlock, the current 813 instrumenation block, where the correct categorization it noti 814 yet known. 815 line: string, the raw instrumenation output line to be used to 816 deteremine the correct categorization. 817 818 Returns: 819 The next instrumentation block to continue parsing with. Usually, 820 this is the same instrumentation block but with the state 821 transitioned appropriately. 822 """ 823 if line.startswith(_InstrumentationStructurePrefixes.STATUS): 824 return self._parse_method_block_line( 825 self._transition_instrumentation_block( 826 instrumentation_block, 827 new_state=_InstrumentationBlockStates.METHOD, 828 ), 829 line, 830 ) 831 elif (line.startswith(_InstrumentationStructurePrefixes.RESULT) or 832 _InstrumentationStructurePrefixes.FAILED in line): 833 return self._parse_result_block_line( 834 self._transition_instrumentation_block( 835 instrumentation_block, 836 new_state=_InstrumentationBlockStates.RESULT, 837 ), 838 line, 839 ) 840 else: 841 # This would only really execute if instrumentation failed to start. 842 instrumentation_block.add_value(line) 843 return instrumentation_block 844 845 def _parse_line(self, instrumentation_block, line): 846 """Parses an arbitrary line from the instrumentation output based upon 847 the current parser state. 848 849 Args: 850 instrumentation_block: _InstrumentationBlock, an instrumentation 851 block with any of the possible parser states. 852 line: string, the raw instrumentation output line to parse 853 appropriately. 854 855 Returns: 856 The next instrumenation block to continue parsing with. 857 """ 858 if instrumentation_block.state == _InstrumentationBlockStates.METHOD: 859 return self._parse_method_block_line(instrumentation_block, line) 860 elif instrumentation_block.state == _InstrumentationBlockStates.RESULT: 861 return self._parse_result_block_line(instrumentation_block, line) 862 else: 863 return self._parse_unknown_block_line(instrumentation_block, line) 864 865 def _finish_parsing(self, instrumentation_block): 866 """Finishes parsing the instrumentation result block for the final 867 instrumentation run status. 868 869 Args: 870 instrumentation_block: _InstrumentationBlock, the instrumentation 871 result block for the instrumenation run. Potentially, thisi 872 could actually be method block if the instrumentation outputi 873 is malformed. 874 875 Returns: 876 A boolean indicating whether the instrumentation run completed 877 with all the tests passing. 878 879 Raises: 880 signals.TestError: Error raised if the instrumentation failed to 881 complete with either a pass or fail status. 882 """ 883 formatter = _InstrumentationBlockFormatter(instrumentation_block) 884 return formatter.has_completed_result_block_format( 885 self.DEFAULT_INSTRUMENTATION_ERROR_MESSAGE) 886 887 def parse_instrumentation_options(self, parameters=None): 888 """Returns the options for the instrumentation test from user_params. 889 890 By default, this method assume that the correct instrumentation options 891 all start with DEFAULT_INSTRUMENTATION_OPTION_PREFIX. 892 893 Args: 894 parameters: dict, the key value pairs representing an assortment 895 of parameters including instrumentation options. Usually, 896 this argument will be from self.user_params. 897 898 Returns: 899 A dictionary of options/parameters for the instrumentation tst. 900 """ 901 if parameters is None: 902 return {} 903 904 filtered_parameters = {} 905 for parameter_key, parameter_value in parameters.items(): 906 if parameter_key.startswith(self.DEFAULT_INSTRUMENTATION_OPTION_PREFIX): 907 option_key = parameter_key[len(self. 908 DEFAULT_INSTRUMENTATION_OPTION_PREFIX):] 909 filtered_parameters[option_key] = parameter_value 910 return filtered_parameters 911 912 def run_instrumentation_test(self, 913 device, 914 package, 915 options=None, 916 prefix=None, 917 runner=None): 918 """Runs instrumentation tests on a device and creates test records. 919 920 Args: 921 device: AndroidDevice, the device to run instrumentation tests on. 922 package: string, the package name of the instrumentation tests. 923 options: dict, Instrumentation options for the instrumentation 924 tests. 925 prefix: string, an optional prefix for parser output for 926 distinguishing between instrumentation test runs. 927 runner: string, the runner to use for the instrumentation package, 928 default to DEFAULT_INSTRUMENTATION_RUNNER. 929 930 Returns: 931 A boolean indicating whether or not all the instrumentation test 932 methods passed. 933 934 Raises: 935 TestError if the instrumentation run crashed or if parsing the 936 output failed. 937 """ 938 # Dictionary hack to allow overwriting the instrumentation_block in the 939 # parse_instrumentation closure 940 instrumentation_block = [_InstrumentationBlock(prefix=prefix)] 941 942 def parse_instrumentation(raw_line): 943 line = raw_line.rstrip().decode('utf-8') 944 logging.info(line) 945 instrumentation_block[0] = self._parse_line(instrumentation_block[0], 946 line) 947 948 device.adb.instrument(package=package, 949 options=options, 950 runner=runner, 951 handler=parse_instrumentation) 952 953 return self._finish_parsing(instrumentation_block[0]) 954 955 956class BaseInstrumentationTestClass(InstrumentationTestMixin, 957 base_test.BaseTestClass): 958 """Base class for all instrumentation test classes to inherit from. 959 960 This class extends the BaseTestClass to add functionality to run and parse 961 the output of instrumentation runs. 962 963 Attributes: 964 DEFAULT_INSTRUMENTATION_OPTION_PREFIX: string, the default prefix for 965 instrumentation params contained within user params. 966 DEFAULT_INSTRUMENTATION_ERROR_MESSAGE: string, the default error 967 message to set if something has prevented something in the 968 instrumentation test run from completing properly. 969 """ 970