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