Repository: google/mobly

Issue:
Create utils for executing Android Instrumentation tests
There have been multiple requests to run regular AndroidInstrumentation tests with mobly.
A sample use cases would be running tests on multiple devices and aggregate results across them.
Let's create utils to make such tasks easy.


Discussion:

Resolving patch:
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -79,7 +79,11 @@
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'tutorial.md']
+exclude_patterns = ['_build',
+    'Thumbs.db',
+    '.DS_Store',
+    'tutorial.md',
+    'instrumentation_tutorial.md']
 
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = 'sphinx'
diff --git a/mobly/controllers/android_device_lib/adb.py b/mobly/controllers/android_device_lib/adb.py
--- a/mobly/controllers/android_device_lib/adb.py
+++ b/mobly/controllers/android_device_lib/adb.py
@@ -27,6 +27,9 @@
 # do with port forwarding must happen under this lock.
 ADB_PORT_LOCK = threading.Lock()
 
+# Qualified class name of the default instrumentation test runner.
+DEFAULT_INSTRUMENTATION_RUNNER = 'com.android.common.support.test.runner.AndroidJUnitRunner'
+
 
 class AdbError(Exception):
     """Raised when there is an error in adb operations."""
@@ -129,7 +132,7 @@ def _exec_cmd(self, args, shell, timeout):
                 raise AdbTimeoutError('Timed out Adb cmd "%s". timeout: %s' %
                                       (args, timeout))
         elif timeout and timeout < 0:
-            raise AdbTimeoutError("Timeout is a negative value: %s" % timeout)
+            raise AdbTimeoutError('Timeout is a negative value: %s' % timeout)
 
         (out, err) = proc.communicate()
         ret = proc.returncode
@@ -178,6 +181,48 @@ def forward(self, args=None, shell=False):
         with ADB_PORT_LOCK:
             return self._exec_adb_cmd('forward', args, shell, timeout=None)
 
+    def instrument(self, package, options=None, runner=None):
+        """Runs an instrumentation command on the device.
+
+        This is a convenience wrapper to avoid parameter formatting.
+
+        Example:
+            device.instrument(
+                'com.my.package.test',
+                options = {
+                    'class': 'com.my.package.test.TestSuite',
+                },
+            )
+
+        Args:
+            package: string, the package of the instrumentation tests.
+            options: dict, the instrumentation options including the test
+                class.
+            runner: string, the test runner name, which defaults to
+                DEFAULT_INSTRUMENTATION_RUNNER.
+
+        Returns:
+            The output of instrumentation command.
+        """
+        if runner is None:
+            runner = DEFAULT_INSTRUMENTATION_RUNNER
+        if options is None:
+            options = {}
+
+        options_list = []
+        for option_key, option_value in options.items():
+            options_list.append('-e %s %s' % (option_key, option_value))
+        options_string = ' '.join(options_list)
+
+        instrumentation_command = 'am instrument -r -w %s %s/%s' % (
+            options_string,
+            package,
+            runner,
+        )
+        logging.info('AndroidDevice|%s: Executing adb shell %s', self.serial,
+                     instrumentation_command)
+        return self.shell(instrumentation_command)
+
     def __getattr__(self, name):
         def adb_call(args=None, shell=False, timeout=None):
             """Wrapper for an ADB command.


Test patch:
diff --git a/mobly/base_instrumentation_test.py b/mobly/base_instrumentation_test.py
new file mode 100644
--- /dev/null
+++ b/mobly/base_instrumentation_test.py
@@ -0,0 +1,936 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+from collections import defaultdict
+from enum import Enum
+from mobly import base_test
+from mobly import records
+from mobly import signals
+
+
+class _InstrumentationStructurePrefixes(object):
+    """Class containing prefixes that structure insturmentation output.
+
+    Android instrumentation generally follows the following format:
+
+    .. code-block:: none
+
+        INSTRUMENTATION_STATUS: ...
+        ...
+        INSTRUMENTATION_STATUS: ...
+        INSTRUMENTATION_STATUS_CODE: ...
+        INSTRUMENTATION_STATUS: ...
+        ...
+        INSTRUMENTATION_STATUS: ...
+        INSTRUMENTATION_STATUS_CODE: ...
+        ...
+        INSTRUMENTATION_RESULT: ...
+        ...
+        INSTRUMENTATION_RESULT: ...
+        ...
+        INSTRUMENTATION_CODE: ...
+
+    This means that these prefixes can be used to guide parsing
+    the output of the instrumentation command into the different
+    instrumetnation test methods.
+
+    Refer to the following Android Framework package for more details:
+
+    .. code-block:: none
+
+        com.android.commands.am.AM
+
+    """
+
+    STATUS = 'INSTRUMENTATION_STATUS:'
+    STATUS_CODE = 'INSTRUMENTATION_STATUS_CODE:'
+    RESULT = 'INSTRUMENTATION_RESULT:'
+    CODE = 'INSTRUMENTATION_CODE:'
+    FAILED = 'INSTRUMENTATION_FAILED:'
+
+
+class _InstrumentationKnownStatusKeys(object):
+    """Commonly used keys used in instrumentation output for listing
+    instrumentation test method result properties.
+
+    An instrumenation status line usually contains a key-value pair such as
+    the following:
+
+    .. code-block:: none
+
+        INSTRUMENTATION_STATUS: <key>=<value>
+
+    Some of these key-value pairs are very common and represent test case
+    properties. This mapping is used to handle each of the corresponding
+    key-value pairs different than less important key-value pairs.
+
+    Refer to the following Android Framework packages for more details:
+
+    .. code-block:: none
+
+        android.app.Instrumentation
+        android.support.test.internal.runner.listener.InstrumentationResultPrinter
+
+    """
+
+    CLASS = 'class'
+    ERROR = 'Error'
+    STACK = 'stack'
+    TEST = 'test'
+    STREAM = 'stream'
+
+
+class _InstrumentationStatusCodes(object):
+    """A mapping of instrumentation status codes to test method results.
+
+    When instrumentation runs, at various points ouput is created in a serias
+    of blocks that terminate as follows:
+
+    .. code-block:: none
+
+        INSTRUMENTATION_STATUS_CODE: 1
+
+    These blocks typically have several status keys in them, and they indicate
+    the progression of a particular instrumentation test method. When the
+    corresponding instrumentation test method finishes, there is generally a
+    line which includes a status code that gives thes the test result.
+
+    The UNKNOWN status code is not an actual status code and is only used to
+    represent that a status code has not yet been read for an instrumentation
+    block.
+
+    Refer to the following Android Framework package for more details:
+
+    .. code-block:: none
+
+        android.support.test.internal.runner.listener.InstrumentationResultPrinter
+
+    """
+
+    UNKNOWN = None
+    OK = '0'
+    START = '1'
+    IN_PROGRESS = '2'
+    ERROR = '-1'
+    FAILURE = '-2'
+    IGNORED = '-3'
+    ASSUMPTION_FAILURE = '-4'
+
+
+class _InstrumentationStatusCodeCategories(object):
+    """A mapping of instrumentation test method results to categories.
+
+    Aside from the TIMING category, these categories roughly map to Mobly
+    signals and are used for determining how a particular instrumentation test
+    method gets recorded.
+    """
+
+    TIMING = [
+        _InstrumentationStatusCodes.START,
+        _InstrumentationStatusCodes.IN_PROGRESS,
+    ]
+    PASS = [
+        _InstrumentationStatusCodes.OK,
+    ]
+    FAIL = [
+        _InstrumentationStatusCodes.ERROR,
+        _InstrumentationStatusCodes.FAILURE,
+    ]
+    SKIPPED = [
+        _InstrumentationStatusCodes.IGNORED,
+        _InstrumentationStatusCodes.ASSUMPTION_FAILURE,
+    ]
+
+
+class _InstrumentationKnownResultKeys(object):
+    """Commonly used keys for outputting instrumentation errors.
+
+    When instrumentation finishes running all of the instrumentation test
+    methods, a result line will appear as follows:
+
+    .. code-block:: none
+
+        INSTRUMENTATION_RESULT:
+
+    If something wrong happened during the instrumentation run such as an
+    application under test crash, the the line will appear similarly as thus:
+
+    .. code-block:: none
+
+        INSTRUMENTATION_RESULT: shortMsg=Process crashed.
+
+    Since these keys indicate that something wrong has happened to the
+    instrumentation run, they should be checked for explicitly.
+
+    Refer to the following documentation page for more information:
+
+    .. code-block:: none
+
+        https://developer.android.com/reference/android/app/ActivityManager.ProcessErrorStateInfo.html
+
+    """
+
+    LONGMSG = 'longMsg'
+    SHORTMSG = 'shortMsg'
+
+
+class _InstrumentationResultSignals(object):
+    """Instrumenttion result block strings for signalling run completion.
+
+    The final section of the instrumentation output generally follows this
+    format:
+
+    .. code-block:: none
+
+        INSTRUMENTATION_RESULT: stream=
+        ...
+        INSTRUMENTATION_CODE -1
+
+    Inside of the ellipsed section, one of these signaling strings should be
+    present. If they are not present, this usually means that the
+    instrumentation run has failed in someway such as a crash. Because the
+    final instrumentation block simply summarizes information, simply roughly
+    checking for a particilar string should be sufficient to check to a proper
+    run completion as the contents of the instrumentation result block don't
+    really matter.
+
+    Refer to the following JUnit package for more details:
+
+    .. code-block:: none
+
+        junit.textui.ResultPrinter
+
+    """
+
+    FAIL = 'FAILURES!!!'
+    PASS = 'OK ('
+
+
+class _InstrumentationBlockStates(Enum):
+    """States used for determing what the parser is currently parsing.
+
+    The parse always starts and ends a block in the UKNOWN state, which is
+    used to indicate that either a method or a result block (matching the
+    METHOD and RESULT states respectively) are valid follow ups, which means
+    that parser should be checking for a structure prefix that indicates which
+    of those two states it should transition to. If the parser is in the
+    METHOD state, then the parser will be parsing input into test methods.
+    Otherwise, the parse can simply concatenate all the input to check for
+    some final run completion signals.
+    """
+
+    UNKNOWN = 0
+    METHOD = 1
+    RESULT = 2
+
+
+class _InstrumentationBlock(object):
+    """Container class for parsed instrumentation output for instrumentation
+    test methods.
+
+    Instrumentation test methods typically follow the follwoing format:
+
+    .. code-block:: none
+
+        INSTRUMENTATION_STATUS: <key>=<value>
+        ...
+        INSTRUMENTATION_STATUS: <key>=<value>
+        INSTRUMENTATION_STATUS_CODE: <status code #>
+
+    The main issue with parsing this however is that the key-value pairs can
+    span multiple lines such as this:
+
+    .. code-block:: none
+
+      INSTRUMENTATION_STATUS: stream=
+      Error in ...
+      ...
+
+    Or, such as this:
+
+    .. code-block:: none
+
+      INSTRUMENTATION_STATUS: stack=...
+      ...
+
+    Because these keys are poentially very long, constant string contatention
+    is potentially inefficent. Instead, this class builds up a buffer to store
+    the raw ouput until it is processed into an actual test result by the
+    _InstrumentationBlockFormatter class.
+
+    Additionally, this class also serves to store the parser state, which
+    means that the BaseInstrumentationTestClass does not need to keep any
+    potentially volatile instrumentation related state, so multiple
+    instrumentation runs should have completely separate parsing states.
+
+    This class is also used for storing result blocks although very little
+    needs to be done for those.
+
+    Attributes:
+        current_key: string, the current key that is being parsed, default to
+            _InstrumentationKnownStatusKeys.STREAM.
+        error_message: string, an error message indicating that something
+            unexpected happened during a instrumentatoin test method.
+        known_keys: dict, well known keys that are handled uniquely.
+        prefix: string, a prefix to add to the class name of the
+            instrumentation test methods.
+        previous_instrumentation_block: _InstrumentationBlock, the last parsed
+            instrumentation block.
+        state: _InstrumentationBlockStates, the current state of the parser.
+        status_code: string, the state code for an instrumentation method
+            block.
+        unknown_keys: dict, arbitrary keys that are handled generically.
+    """
+
+    def __init__(self,
+                 state=_InstrumentationBlockStates.UNKNOWN,
+                 prefix=None,
+                 previous_instrumentation_block=None):
+        self.state = state
+        self.prefix = prefix
+        self.previous_instrumentation_block = previous_instrumentation_block
+
+        self._empty = True
+        self.error_message = ''
+        self.status_code = _InstrumentationStatusCodes.UNKNOWN
+
+        self.current_key = _InstrumentationKnownStatusKeys.STREAM
+        self.known_keys = {
+            _InstrumentationKnownStatusKeys.STREAM: [],
+            _InstrumentationKnownStatusKeys.CLASS: [],
+            _InstrumentationKnownStatusKeys.ERROR: [],
+            _InstrumentationKnownStatusKeys.STACK: [],
+            _InstrumentationKnownStatusKeys.TEST: [],
+            _InstrumentationKnownResultKeys.LONGMSG: [],
+            _InstrumentationKnownResultKeys.SHORTMSG: [],
+        }
+        self.unknown_keys = defaultdict(list)
+
+    @property
+    def is_empty(self):
+        """Deteremines whether or not anything has been parsed with this
+        instrumentation block.
+
+        Returns:
+            A boolean indicating whether or not the this instrumentation block
+            has parsed and contains any output.
+        """
+        return self._empty
+
+    def set_error_message(self, error_message):
+        """Sets an error message on an instrumentation block.
+
+        This method is used exclusively to indicate that a test method failed
+        to complete, which is usually cause by a crash of some sort such that
+        the test method is marked as error instead of ignored.
+
+        Args:
+            error_message: string, an error message to be added to the
+                TestResultRecord to explain that something wrong happened.
+        """
+        self._empty = False
+        self.error_message = error_message
+
+    def _remove_structure_prefix(self, prefix, line):
+        """Helper function for removing the structure prefix for parsing.
+
+        Args:
+            prefix: string, a _InstrumentationStructurePrefixes to remove from
+                the raw output.
+            line: string, the raw line from the instrumentation output.
+
+        Returns:
+            A string containing a key value pair descripting some property
+            of the current instrumentation test method.
+        """
+        return line[len(prefix):].strip()
+
+    def set_status_code(self, status_code_line):
+        """Sets the status code for the instrumentation test method, used in
+        determining the test result.
+
+        Args:
+            status_code_line: string, the raw instrumentation output line that
+                contains the status code of the instrumentation block.
+        """
+        self._empty = False
+        self.status_code = self._remove_structure_prefix(
+            _InstrumentationStructurePrefixes.STATUS_CODE,
+            status_code_line,
+        )
+
+    def set_key(self, structure_prefix, key_line):
+        """Sets the current key for the instrumentation block.
+
+        For unknown keys, the key is added to the value list in order to
+        better contextualize the value in the output.
+
+        Args:
+            structure_prefix: string, the structure prefix that was matched
+                and that needs to be removed.
+            key_line: string, the raw instrumentation ouput line that contains
+                the key-value pair.
+        """
+        self._empty = False
+        key_value = self._remove_structure_prefix(
+            structure_prefix,
+            key_line,
+        )
+        if '=' in key_value:
+            (key, value) = key_value.split('=')
+            self.current_key = key
+            if key in self.known_keys:
+                self.known_keys[key].append(value)
+            else:
+                self.unknown_keys[key].append(key_value)
+
+    def add_value(self, line):
+        """Adds unstructured or multi-line value output to the current parsed
+        instrumentation block for outputting later.
+
+        Usually, this will add extra lines to the value list for the current
+        key-value pair. However, sometimes, such as when instrumentation
+        failed to start, output does not follow the structured prefix format.
+        In this case, adding all of the output is still useful so that a user
+        can debug the issue.
+
+        Args:
+            line: string, the raw instrumentation line to append to the value
+                list.
+        """
+        self._empty = False
+        if self.current_key in self.known_keys:
+            self.known_keys[self.current_key].append(line)
+        else:
+            self.unknown_keys[self.current_key].append(line)
+
+    def transition_state(self, new_state):
+        """Transitions or sets the current instrumentation block to the new
+        parser state.
+
+        Args:
+            new_state: _InstrumentationBlockStates, the state that the parser
+                should transition to.
+
+        Returns:
+            A new instrumentation block set to the new state, representing
+            the start of parsing a new instrumentation test method.
+            Alternatively, if the current instrumentation block represents the
+            start of parsing a new instrumentation block (state UKNOWN), then
+            this returns the current instrumentation block set to the now
+            known parsing state.
+        """
+        if self.state == _InstrumentationBlockStates.UNKNOWN:
+            self.state = new_state
+            return self
+        else:
+            return _InstrumentationBlock(
+                state=new_state,
+                prefix=self.prefix,
+                previous_instrumentation_block=self,
+            )
+
+
+class _InstrumentationBlockFormatter(object):
+    """Takes an instrumentation block and converts it into a Mobly test
+    result.
+    """
+
+    DEFAULT_INSTRUMENTATION_METHOD_NAME = 'instrumentation_method'
+
+    def __init__(self, instrumentation_block):
+        self._prefix = instrumentation_block.prefix
+        self._status_code = instrumentation_block.status_code
+        self._error_message = instrumentation_block.error_message
+        self._known_keys = {}
+        self._unknown_keys = {}
+        for key, value in instrumentation_block.known_keys.items():
+            self._known_keys[key] = '\n'.join(
+                instrumentation_block.known_keys[key])
+        for key, value in instrumentation_block.unknown_keys.items():
+            self._unknown_keys[key] = '\n'.join(
+                instrumentation_block.unknown_keys[key])
+
+    def _get_name(self):
+        """Gets the method name of the test method for the instrumentation
+        method block.
+
+        Returns:
+            A string containing the name of the instrumentation test method's
+            test or a default name if no name was parsed.
+        """
+        if self._known_keys[_InstrumentationKnownStatusKeys.TEST]:
+            return self._known_keys[_InstrumentationKnownStatusKeys.TEST]
+        else:
+            return self.DEFAULT_INSTRUMENTATION_METHOD_NAME
+
+    def _get_class(self):
+        """Gets the class name of the test method for the instrumentation
+        method block.
+
+        Returns:
+            A string containing the class name of the instrumentation test
+            method's test or empty string if no name was parsed. If a prefix
+            was specified, then the prefix will be prepended to the class
+            name.
+        """
+        class_parts = [
+            self._prefix,
+            self._known_keys[_InstrumentationKnownStatusKeys.CLASS]
+        ]
+        return '.'.join(filter(None, class_parts))
+
+    def _get_full_name(self):
+        """Gets the qualified name of the test method corresponding to the
+        instrumentation block.
+
+        Returns:
+            A string containing the fully qualified name of the
+            instrumentation test method. If parts are missing, then degrades
+            steadily.
+        """
+        full_name_parts = [self._get_class(), self._get_name()]
+        return '#'.join(filter(None, full_name_parts))
+
+    def _get_details(self):
+        """Gets the ouput for the detail section of the TestResultRecord.
+
+        Returns:
+            A string to set for a TestResultRecord's details.
+        """
+        detail_parts = [self._get_full_name(), self._error_message]
+        return '\n'.join(filter(None, detail_parts))
+
+    def _get_extras(self):
+        """Gets the output for the extras section of the TestResultRecord.
+
+        Returns:
+            A string to set for a TestResultRecord's extras.
+        """
+        # Add empty line to start key-value pairs on a new line.
+        extra_parts = ['']
+
+        for value in self._unknown_keys.values():
+            extra_parts.append(value)
+
+        extra_parts.append(
+            self._known_keys[_InstrumentationKnownStatusKeys.STREAM])
+        extra_parts.append(
+            self._known_keys[_InstrumentationKnownResultKeys.SHORTMSG])
+        extra_parts.append(
+            self._known_keys[_InstrumentationKnownResultKeys.LONGMSG])
+        extra_parts.append(
+            self._known_keys[_InstrumentationKnownStatusKeys.ERROR])
+
+        if self._known_keys[
+                _InstrumentationKnownStatusKeys.STACK] not in self._known_keys[
+                    _InstrumentationKnownStatusKeys.STREAM]:
+            extra_parts.append(
+                self._known_keys[_InstrumentationKnownStatusKeys.STACK])
+
+        return '\n'.join(filter(None, extra_parts))
+
+    def _is_failed(self):
+        """Determines if the test corresponding to the instrumentation block
+        failed.
+
+        This method can not be used to tell if a test method passed and
+        should not be used for such a purpose.
+
+        Returns:
+            A boolean indicating if the test method failed.
+        """
+        if self._status_code in _InstrumentationStatusCodeCategories.FAIL:
+            return True
+        elif (self._known_keys[_InstrumentationKnownStatusKeys.STACK]
+              and self._status_code !=
+              _InstrumentationStatusCodes.ASSUMPTION_FAILURE):
+            return True
+        elif self._known_keys[_InstrumentationKnownStatusKeys.ERROR]:
+            return True
+        elif self._known_keys[_InstrumentationKnownResultKeys.SHORTMSG]:
+            return True
+        elif self._known_keys[_InstrumentationKnownResultKeys.LONGMSG]:
+            return True
+        else:
+            return False
+
+    def create_test_record(self, mobly_test_class):
+        """Creates a TestResultRecord for the instrumentation block.
+
+        Args:
+            mobly_test_class: string, the name of the Mobly test case
+                executing the instrumentation run.
+
+        Returns:
+            A TestResultRecord with an appropriate signals exception
+            representing the instrumentation test method's result status.
+        """
+        details = self._get_details()
+        extras = self._get_extras()
+
+        tr_record = records.TestResultRecord(
+            t_name=self._get_full_name(),
+            t_class=mobly_test_class,
+        )
+        if self._is_failed():
+            tr_record.test_fail(
+                e=signals.TestFailure(details=details, extras=extras))
+        elif self._status_code in _InstrumentationStatusCodeCategories.SKIPPED:
+            tr_record.test_skip(
+                e=signals.TestSkip(details=details, extras=extras))
+        elif self._status_code in _InstrumentationStatusCodeCategories.PASS:
+            tr_record.test_pass(
+                e=signals.TestPass(details=details, extras=extras))
+        elif self._status_code in _InstrumentationStatusCodeCategories.TIMING:
+            if self._error_message:
+                tr_record.test_error(
+                    e=signals.TestError(details=details, extras=extras))
+            else:
+                tr_record = None
+        else:
+            tr_record.test_error(
+                e=signals.TestError(details=details, extras=extras))
+        if self._known_keys[_InstrumentationKnownStatusKeys.STACK]:
+            tr_record.termination_signal.stacktrace = self._known_keys[
+                _InstrumentationKnownStatusKeys.STACK]
+        return tr_record
+
+    def has_completed_result_block_format(self, error_message):
+        """Checks the instrumentation result block for a signal indicating
+        normal completion.
+
+        Args:
+            error_message: string, the error message to give if the
+                instrumentation run did not complete successfully.-
+
+        Returns:
+            A boolean indicating whether or not the instrumentation run passed
+            or failed overall.
+
+        Raises:
+            signals.TestError: Error raised if the instrumentation run did not
+                complete because of a crash or some other issue.
+        """
+        extras = self._get_extras()
+        if _InstrumentationResultSignals.PASS in extras:
+            return True
+        elif _InstrumentationResultSignals.FAIL in extras:
+            return False
+        else:
+            raise signals.TestError(details=error_message, extras=extras)
+
+
+class BaseInstrumentationTestClass(base_test.BaseTestClass):
+    """Base class for all instrumentation test claseses to inherit from.
+
+    This class extends the BaseTestClass to add functionality to run and parse
+    the output of instrumentation runs.
+
+    Attributes:
+        DEFAULT_INSTRUMENTATION_OPTION_PREFIX: string, the default prefix for
+            instrumentation params contained within user params.
+        DEFAULT_INSTRUMENTATION_ERROR_MESSAGE: string, the default error
+            message to set if something has prevented something in the
+            instrumentation test run from completing properly.
+    """
+
+    DEFAULT_INSTRUMENTATION_OPTION_PREFIX = 'instrumentation_option_'
+    DEFAULT_INSTRUMENTATION_ERROR_MESSAGE = ('instrumentation run exited '
+                                             'unexpectedly')
+
+    def _previous_block_never_completed(self, current_block, previous_block,
+                                        new_state):
+        """Checks if the previous instrumentation method block completed.
+
+        Args:
+            current_block: _InstrumentationBlock, the current instrumentation
+                block to check for being a different instrumentation test
+                method.
+            previous_block: _InstrumentationBlock, rhe previous
+                instrumentation block to check for an incomplete status.
+            new_state: _InstrumentationBlockStates, the next state for the
+                parser, used to check for the instrumentation run ending
+                with an incomplete test.
+
+        Returns:
+            A boolean indicating whether the previous instrumentation block
+            completed executing.
+        """
+        if previous_block:
+            previously_timing_block = (
+                previous_block.status_code in
+                _InstrumentationStatusCodeCategories.TIMING)
+            currently_new_block = (
+                current_block.status_code == _InstrumentationStatusCodes.START
+                or new_state == _InstrumentationBlockStates.RESULT)
+            return all([previously_timing_block, currently_new_block])
+        else:
+            return False
+
+    def _create_formatters(self, instrumentation_block, new_state):
+        """Creates the _InstrumentationBlockFormatters for outputting the
+        instrumentation method block that have finished parsing.
+
+        Args:
+            instrumentation_block: _InstrumentationBlock, the current
+                instrumentation method block to create formatters based upon.
+            new_state: _InstrumentationBlockState, the next state that the
+                parser will transition to.
+
+        Returns:
+            A list of the formatters tha need to create and add
+            TestResultRecords to the test results.
+        """
+        formatters = []
+        if self._previous_block_never_completed(
+                current_block=instrumentation_block,
+                previous_block=instrumentation_block.
+                previous_instrumentation_block,
+                new_state=new_state):
+            instrumentation_block.previous_instrumentation_block.set_error_message(
+                self.DEFAULT_INSTRUMENTATION_ERROR_MESSAGE)
+            formatters.append(
+                _InstrumentationBlockFormatter(
+                    instrumentation_block.previous_instrumentation_block))
+
+        if not instrumentation_block.is_empty:
+            formatters.append(
+                _InstrumentationBlockFormatter(instrumentation_block))
+        return formatters
+
+    def _transition_instrumentation_block(
+            self,
+            instrumentation_block,
+            new_state=_InstrumentationBlockStates.UNKNOWN):
+        """Transitions and finishes the current instrumentation block.
+
+        Args:
+            instrumentation_block: _InstrumentationBlock, the current
+                instrumentation block to finish.
+            new_state: _InstrumentationBlockState, the next state for the
+                parser to transition to.
+
+        Returns:
+            The new instrumentation block to use for storing parsed
+            instrumentation ouput.
+        """
+        formatters = self._create_formatters(instrumentation_block, new_state)
+        for formatter in formatters:
+            test_record = formatter.create_test_record(self.TAG)
+            if test_record:
+                self.results.add_record(test_record)
+                self.summary_writer.dump(test_record.to_dict(),
+                                         records.TestSummaryEntryType.RECORD)
+        return instrumentation_block.transition_state(new_state=new_state)
+
+    def _parse_method_block_line(self, instrumentation_block, line):
+        """Parses the instrumnetation method block's line.
+
+        Args:
+            instrumentation_block: _InstrumentationBlock, the current
+                instrumentation method block.
+            line: string, the raw instrumentation output line to parse.
+
+        Returns:
+            The next instrumentation block, which should be used to continue
+            parsing instrumentation output.
+        """
+        if line.startswith(_InstrumentationStructurePrefixes.STATUS):
+            instrumentation_block.set_key(
+                _InstrumentationStructurePrefixes.STATUS, line)
+            return instrumentation_block
+        elif line.startswith(_InstrumentationStructurePrefixes.STATUS_CODE):
+            instrumentation_block.set_status_code(line)
+            return self._transition_instrumentation_block(
+                instrumentation_block)
+        elif line.startswith(_InstrumentationStructurePrefixes.RESULT):
+            # Unexpected transition from method block -> result block
+            instrumentation_block.set_key(
+                _InstrumentationStructurePrefixes.RESULT, line)
+            return self._parse_result_line(
+                self._transition_instrumentation_block(
+                    instrumentation_block,
+                    new_state=_InstrumentationBlockStates.RESULT,
+                ),
+                line,
+            )
+        else:
+            instrumentation_block.add_value(line)
+            return instrumentation_block
+
+    def _parse_result_block_line(self, instrumentation_block, line):
+        """Parses the instrumentation result block's line.
+
+        Args:
+            instrumentation_block: _InstrumentationBlock, the instrumentation
+                result block for the instrumentation run.
+            line: string, the raw instrumentation output to add to the
+                instrumenation result block's _InstrumentationResultBlocki
+                object.
+
+        Returns:
+            The instrumentation result block for the instrumentation run.
+        """
+        instrumentation_block.add_value(line)
+        return instrumentation_block
+
+    def _parse_unknown_block_line(self, instrumentation_block, line):
+        """Parses a line from the instrumentation output from the UNKNOWN
+        parser state.
+
+        Args:
+            instrumentation_block: _InstrumentationBlock, the current
+                instrumenation block, where the correct categorization it noti
+                yet known.
+            line: string, the raw instrumenation output line to be used to
+                deteremine the correct categorization.
+
+        Returns:
+            The next instrumentation block to continue parsing with. Usually,
+            this is the same instrumentation block but with the state
+            transitioned appropriately.
+        """
+        if line.startswith(_InstrumentationStructurePrefixes.STATUS):
+            return self._parse_method_block_line(
+                self._transition_instrumentation_block(
+                    instrumentation_block,
+                    new_state=_InstrumentationBlockStates.METHOD,
+                ),
+                line,
+            )
+        elif (line.startswith(_InstrumentationStructurePrefixes.RESULT)
+              or _InstrumentationStructurePrefixes.FAILED in line):
+            return self._parse_result_block_line(
+                self._transition_instrumentation_block(
+                    instrumentation_block,
+                    new_state=_InstrumentationBlockStates.RESULT,
+                ),
+                line,
+            )
+        else:
+            # This would only really execute if instrumentation failed to start.
+            instrumentation_block.add_value(line)
+            return instrumentation_block
+
+    def _parse_line(self, instrumentation_block, line):
+        """Parses an arbitary line from the instrumentation output based upon
+        the current parser state.
+
+        Args:
+            instrumentation_block: _InstrumentationBlock, an instrumentation
+                block with any of the possible parser states.
+            line: string, the raw instrumentation output line to parse
+                appropriately.
+
+        Returns:
+            The next instrumenation block to continue parsing with.
+        """
+        if instrumentation_block.state == _InstrumentationBlockStates.METHOD:
+            return self._parse_method_block_line(instrumentation_block, line)
+        elif instrumentation_block.state == _InstrumentationBlockStates.RESULT:
+            return self._parse_result_block_line(instrumentation_block, line)
+        else:
+            return self._parse_unknown_block_line(instrumentation_block, line)
+
+    def _finish_parsing(self, instrumentation_block):
+        """Finishes parsing the instrumentation result block for the final
+        instrumentation run status.
+
+        Args:
+            instrumentation_block: _InstrumentationBlock, the instrumentation
+                result block for the instrumenation run. Potentially, thisi
+                could actually be method block if the instrumentation outputi
+                is malformed.
+
+        Returns:
+            A boolean indicating whether the instrumentation run completed
+                with all the tests passing.
+
+        Raises:
+            signals.TestError: Error raised if the instrumentation failed to
+                complete with either a pass or fail status.
+        """
+        formatter = _InstrumentationBlockFormatter(instrumentation_block)
+        return formatter.has_completed_result_block_format(
+            self.DEFAULT_INSTRUMENTATION_ERROR_MESSAGE)
+
+    def parse_instrumentation_options(self, parameters=None):
+        """Returns the options for the instrumentation test from user_params.
+
+        By default, this method assume that the correct instrumentation options
+        all start with DEFAULT_INSTRUMENTATION_OPTION_PREFIX.
+
+        Args:
+            parameters: dict, the key value pairs representing an assortment
+                of parameters including instrumentation options. Usually,
+                this argument will be from self.user_params.
+
+        Returns:
+            A dictionary of options/parameters for the instrumentation tst.
+        """
+        if parameters is None:
+            return {}
+
+        filtered_parameters = {}
+        for parameter_key, parameter_value in parameters.items():
+            if parameter_key.startswith(
+                    self.DEFAULT_INSTRUMENTATION_OPTION_PREFIX):
+                option_key = parameter_key[len(
+                    self.DEFAULT_INSTRUMENTATION_OPTION_PREFIX):]
+                filtered_parameters[option_key] = parameter_value
+        return filtered_parameters
+
+    def run_instrumentation_test(self,
+                                 device,
+                                 package,
+                                 options=None,
+                                 prefix=None,
+                                 runner=None):
+        """Runs instrumentation tests on a device and creates test records.
+
+        Args:
+            device: AndroidDevice, the device to run instrumentation tests on.
+            package: string, the package name of the instrumentation tests.
+            options: dict, Instrumentation options for the instrumentation
+                tests.
+            prefix: string, an optional prefix for parser output for
+                distinguishing between instrumentation test runs.
+            runner: string, the runner to use for the instrumentation package,
+                default to DEFAULT_INSTRUMENTATION_RUNNER.
+
+        Returns:
+            A boolean indicating whether or not all the instrumentation test
+                methods passed.
+
+        Raises:
+            TestError if the instrumentation run crashed or if parsing the
+                output failed.
+        """
+        instrumentation_output = device.adb.instrument(
+            package=package,
+            options=options,
+            runner=runner,
+        )
+        logging.info('Outputting instrumentation test log...')
+        logging.info(instrumentation_output)
+
+        # TODO(winterfrosts): Implement online output generation and parsing.
+        instrumentation_block = _InstrumentationBlock(prefix=prefix)
+        for line in instrumentation_output.splitlines():
+            instrumentation_block = self._parse_line(instrumentation_block,
+                                                     line)
+        return self._finish_parsing(instrumentation_block)
diff --git a/mobly/test_runner.py b/mobly/test_runner.py
--- a/mobly/test_runner.py
+++ b/mobly/test_runner.py
@@ -124,7 +124,7 @@ def main(argv=None):
 def _find_test_class():
     """Finds the test class in a test script.
 
-    Walk through module memebers and find the subclass of BaseTestClass. Only
+    Walk through module members and find the subclass of BaseTestClass. Only
     one subclass is allowed in a test script.
 
     Returns:
@@ -392,7 +392,8 @@ def _register_controller(self, config, module, required=True,
         Raises:
             ControllerError:
                 * The controller module has already been registered.
-                * The actual number of objects instantiated is less than the `min_number`.
+                * The actual number of objects instantiated is less than the
+                * `min_number`.
                 * `required` is True and no corresponding config can be found.
                 * Any other error occurred in the registration process.
 
diff --git a/tests/mobly/base_instrumentation_test_test.py b/tests/mobly/base_instrumentation_test_test.py
new file mode 100755
--- /dev/null
+++ b/tests/mobly/base_instrumentation_test_test.py
@@ -0,0 +1,991 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import mock
+import shutil
+import tempfile
+
+from future.tests.base import unittest
+
+from mobly.base_instrumentation_test import BaseInstrumentationTestClass
+from mobly import config_parser
+from mobly import signals
+
+# A mock test package for instrumentation.
+MOCK_TEST_PACKAGE = 'com.my.package.test'
+# A random prefix to test that prefixes are added properly.
+MOCK_PREFIX = 'my_prefix'
+# A mock name for the instrumentation test subclass.
+MOCK_INSTRUMENTATION_TEST_CLASS_NAME = 'MockInstrumentationTest'
+
+
+class MockInstrumentationTest(BaseInstrumentationTestClass):
+    def __init__(self, tmp_dir, user_params={}):
+        mock_test_run_configs = config_parser.TestRunConfig()
+        mock_test_run_configs.summary_writer = mock.Mock()
+        mock_test_run_configs.log_path = tmp_dir
+        mock_test_run_configs.user_params = user_params
+        mock_test_run_configs.reporter = mock.MagicMock()
+        super(MockInstrumentationTest, self).__init__(mock_test_run_configs)
+
+    def run_mock_instrumentation_test(self, instrumentation_output, prefix):
+        mock_device = mock.Mock()
+        mock_device.adb = mock.Mock()
+        mock_device.adb.instrument = mock.MagicMock(
+            return_value=instrumentation_output)
+        return self.run_instrumentation_test(
+            mock_device, MOCK_TEST_PACKAGE, prefix=prefix)
+
+
+class InstrumentationResult(object):
+    def __init__(self):
+        self.error = None
+        self.completed_and_passed = False
+        self.executed = []
+        self.skipped = []
+
+
+class BaseInstrumentationTestTest(unittest.TestCase):
+    def setUp(self):
+        self.tmp_dir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.tmp_dir)
+
+    def assert_parse_instrumentation_options(self, user_params,
+                                             expected_instrumentation_options):
+        mit = MockInstrumentationTest(self.tmp_dir, user_params)
+        instrumentation_options = mit.parse_instrumentation_options(
+            mit.user_params)
+        self.assertEqual(instrumentation_options,
+                         expected_instrumentation_options)
+
+    def test_parse_instrumentation_options_with_no_user_params(self):
+        self.assert_parse_instrumentation_options({}, {})
+
+    def test_parse_instrumentation_options_with_no_instrumentation_params(
+            self):
+        self.assert_parse_instrumentation_options(
+            {
+                'param1': 'val1',
+                'param2': 'val2',
+            },
+            {},
+        )
+
+    def test_parse_instrumentation_options_with_only_instrumentation_params(
+            self):
+        self.assert_parse_instrumentation_options(
+            {
+                'instrumentation_option_key1': 'value1',
+                'instrumentation_option_key2': 'value2',
+            },
+            {'key1': 'value1',
+             'key2': 'value2'},
+        )
+
+    def test_parse_instrumentation_options_with_mixed_user_params(self):
+        self.assert_parse_instrumentation_options(
+            {
+                'param1': 'val1',
+                'param2': 'val2',
+                'instrumentation_option_key1': 'value1',
+                'instrumentation_option_key2': 'value2',
+            },
+            {'key1': 'value1',
+             'key2': 'value2'},
+        )
+
+    def run_instrumentation_test(self, instrumentation_output, prefix=None):
+        mit = MockInstrumentationTest(self.tmp_dir)
+        result = InstrumentationResult()
+        try:
+            result.completed_and_passed = mit.run_mock_instrumentation_test(
+                instrumentation_output, prefix=prefix)
+        except signals.TestError as e:
+            result.error = e
+        result.executed = mit.results.executed
+        result.skipped = mit.results.skipped
+        return result
+
+    def assert_equal_test(self, actual_test, expected_test):
+        (expected_test_name, expected_signal) = expected_test
+        self.assertEqual(actual_test.test_class,
+                         MOCK_INSTRUMENTATION_TEST_CLASS_NAME)
+        self.assertEqual(actual_test.test_name, expected_test_name)
+        self.assertIsInstance(actual_test.termination_signal.exception,
+                              expected_signal)
+
+    def assert_run_instrumentation_test(self,
+                                        instrumentation_output,
+                                        expected_executed=[],
+                                        expected_skipped=[],
+                                        expected_completed_and_passed=False,
+                                        expected_has_error=False,
+                                        prefix=None):
+        result = self.run_instrumentation_test(
+            instrumentation_output, prefix=prefix)
+        if expected_has_error:
+            self.assertIsInstance(result.error, signals.TestError)
+        else:
+            self.assertIsNone(result.error)
+            self.assertEquals(result.completed_and_passed,
+                              expected_completed_and_passed)
+        self.assertEqual(len(result.executed), len(expected_executed))
+        for actual_test, expected_test in zip(result.executed,
+                                              expected_executed):
+            self.assert_equal_test(actual_test, expected_test)
+        self.assertEqual(len(result.skipped), len(expected_skipped))
+        for actual_test, expected_test in zip(result.skipped,
+                                              expected_skipped):
+            self.assert_equal_test(actual_test, expected_test)
+
+    def test_run_instrumentation_test_with_invalid_syntax(self):
+        instrumentation_output = """\
+usage: am [subcommand] [options]
+usage: am start [-D] [-N] [-W] [-P <FILE>] [--start-profiler <FILE>]
+               [--sampling INTERVAL] [-R COUNT] [-S]
+
+am start: start an Activity.  Options are:
+    -D: enable debugging
+
+am startservice: start a Service.  Options are:
+    --user <USER_ID> | current: Specify which user to run as; if not
+        specified then run as the current user.
+
+am task lock: bring <TASK_ID> to the front and don't allow other tasks to run.
+
+<INTENT> specifications include these flags and arguments:
+    [-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>]
+    [-c <CATEGORY> [-c <CATEGORY>] ...]
+
+Error: Bad component name: /
+"""
+        self.assert_run_instrumentation_test(
+            instrumentation_output, expected_has_error=True)
+
+    def test_run_instrumentation_test_with_no_output(self):
+        instrumentation_output = """\
+"""
+        self.assert_run_instrumentation_test(
+            instrumentation_output, expected_has_error=True)
+
+    def test_run_instrumentation_test_with_missing_test_package(self):
+        instrumentation_output = """\
+android.util.AndroidException: INSTRUMENTATION_FAILED: com.my.package.test/com.my.package.test.runner.MyRunner
+	at com.android.commands.am.Am.runInstrument(Am.java:897)
+	at com.android.commands.am.Am.onRun(Am.java:405)
+	at com.android.internal.os.BaseCommand.run(BaseCommand.java:51)
+	at com.android.commands.am.Am.main(Am.java:124)
+	at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
+	at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:262)
+INSTRUMENTATION_STATUS: id=ActivityManagerService
+INSTRUMENTATION_STATUS: Error=Unable to find instrumentation info for: ComponentInfo{com.my.package.test/com.my.package.test.runner.MyRunner}
+INSTRUMENTATION_STATUS_CODE: -1"""
+        self.assert_run_instrumentation_test(
+            instrumentation_output, expected_has_error=True)
+
+    def test_run_instrumentation_test_with_missing_runner(self):
+        instrumentation_output = """\
+android.util.AndroidException: INSTRUMENTATION_FAILED: com.my.package.test/com.my.package.test.runner
+INSTRUMENTATION_STATUS: id=ActivityManagerService
+INSTRUMENTATION_STATUS: Error=Unable to find instrumentation info for: ComponentInfo{com.my.package.test/com.my.package.test.runner}
+INSTRUMENTATION_STATUS_CODE: -1
+	at com.android.commands.am.Am.runInstrument(Am.java:897)
+	at com.android.commands.am.Am.onRun(Am.java:405)
+	at com.android.internal.os.BaseCommand.run(BaseCommand.java:51)
+	at com.android.commands.am.Am.main(Am.java:124)
+	at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
+	at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:262)"""
+        self.assert_run_instrumentation_test(
+            instrumentation_output, expected_has_error=True)
+
+    def test_run_instrumentation_test_with_no_tests(self):
+        instrumentation_output = """\
+INSTRUMENTATION_RESULT: stream=
+
+Time: 0.001
+
+OK (0 tests)
+
+
+INSTRUMENTATION_CODE: -1
+"""
+        self.assert_run_instrumentation_test(
+            instrumentation_output, expected_completed_and_passed=True)
+
+    def test_run_instrumentation_test_with_passing_test(self):
+        instrumentation_output = """\
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: test=basicTest
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=.
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: test=basicTest
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS_CODE: 0
+INSTRUMENTATION_RESULT: stream=
+
+Time: 0.214
+
+OK (1 test)
+
+
+INSTRUMENTATION_CODE: -1
+"""
+        expected_executed = [
+            ('com.my.package.test.BasicTest#basicTest', signals.TestPass),
+        ]
+        self.assert_run_instrumentation_test(
+            instrumentation_output,
+            expected_executed=expected_executed,
+            expected_completed_and_passed=True)
+
+    def test_run_instrumentation_test_with_prefix_test(self):
+        instrumentation_output = """\
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: test=basicTest
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=.
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: test=basicTest
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS_CODE: 0
+INSTRUMENTATION_RESULT: stream=
+
+Time: 0.214
+
+OK (1 test)
+
+
+INSTRUMENTATION_CODE: -1
+"""
+        expected_executed = [
+            ('%s.com.my.package.test.BasicTest#basicTest' % MOCK_PREFIX,
+             signals.TestPass),
+        ]
+        self.assert_run_instrumentation_test(
+            instrumentation_output,
+            expected_executed=expected_executed,
+            expected_completed_and_passed=True,
+            prefix=MOCK_PREFIX)
+
+    def test_run_instrumentation_test_with_failing_test(self):
+        instrumentation_output = """\
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: test=failingTest
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stack=java.lang.UnsupportedOperationException: dummy failing test
+	at com.my.package.test.BasicTest.failingTest(BasicTest.java:38)
+	at java.lang.reflect.Method.invoke(Native Method)
+	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:57)
+	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
+	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:59)
+	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
+	at android.support.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
+	at android.support.test.internal.runner.junit4.statement.RunAfters.evaluate(RunAfters.java:61)
+	at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:433)
+	at com.my.package.test.BaseTest$3.evaluate(BaseTest.java:96)
+	at com.my.package.test.BaseTest$4.evaluate(BaseTest.java:109)
+	at com.my.package.test.BaseTest$2.evaluate(BaseTest.java:77)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
+	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:81)
+	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:327)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:84)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at android.support.test.runner.AndroidJUnit4.run(AndroidJUnit4.java:99)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
+	at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
+	at com.my.package.test.BaseRunner.runTests(BaseRunner.java:344)
+	at com.my.package.test.BaseRunner.onStart(BaseRunner.java:330)
+	at com.my.package.test.runner.MyRunner.onStart(MyRunner.java:253)
+	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074)
+
+INSTRUMENTATION_STATUS: stream=
+Error in failingTest(com.my.package.test.BasicTest):
+java.lang.UnsupportedOperationException: dummy failing test
+	at com.my.package.test.BasicTest.failingTest(BasicTest.java:38)
+	at java.lang.reflect.Method.invoke(Native Method)
+	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:57)
+	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
+	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:59)
+	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
+	at android.support.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
+	at android.support.test.internal.runner.junit4.statement.RunAfters.evaluate(RunAfters.java:61)
+	at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:433)
+	at com.my.package.test.BaseTest$3.evaluate(BaseTest.java:96)
+	at com.my.package.test.BaseTest$4.evaluate(BaseTest.java:109)
+	at com.my.package.test.BaseTest$2.evaluate(BaseTest.java:77)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
+	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:81)
+	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:327)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:84)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at android.support.test.runner.AndroidJUnit4.run(AndroidJUnit4.java:99)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
+	at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
+	at com.my.package.test.BaseRunner.runTests(BaseRunner.java:344)
+	at com.my.package.test.BaseRunner.onStart(BaseRunner.java:330)
+	at com.my.package.test.runner.MyRunner.onStart(MyRunner.java:253)
+	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074)
+
+INSTRUMENTATION_STATUS: test=failingTest
+INSTRUMENTATION_STATUS_CODE: -2
+INSTRUMENTATION_RESULT: stream=
+
+Time: 1.92
+There was 1 failure:
+1) failingTest(com.my.package.test.BasicTest)
+java.lang.UnsupportedOperationException: dummy failing test
+	at com.my.package.test.BasicTest.failingTest(BasicTest.java:38)
+	at java.lang.reflect.Method.invoke(Native Method)
+	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:57)
+	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
+	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:59)
+	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
+	at android.support.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
+	at android.support.test.internal.runner.junit4.statement.RunAfters.evaluate(RunAfters.java:61)
+	at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:433)
+	at com.my.package.test.BaseTest$3.evaluate(BaseTest.java:96)
+	at com.my.package.test.BaseTest$4.evaluate(BaseTest.java:109)
+	at com.my.package.test.BaseTest$2.evaluate(BaseTest.java:77)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
+	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:81)
+	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:327)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:84)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at android.support.test.runner.AndroidJUnit4.run(AndroidJUnit4.java:99)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
+	at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
+	at com.my.package.test.BaseRunner.runTests(BaseRunner.java:344)
+	at com.my.package.test.BaseRunner.onStart(BaseRunner.java:330)
+	at com.my.package.test.runner.MyRunner.onStart(MyRunner.java:253)
+	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074)
+
+FAILURES!!!
+Tests run: 1,  Failures: 1
+
+
+INSTRUMENTATION_CODE: -1"""
+        expected_executed = [
+            ('com.my.package.test.BasicTest#failingTest', signals.TestFailure),
+        ]
+        self.assert_run_instrumentation_test(
+            instrumentation_output, expected_executed=expected_executed)
+
+    def test_run_instrumentation_test_with_assumption_failure_test(self):
+        instrumentation_output = """\
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: test=assumptionFailureTest
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stack=org.junit.AssumptionViolatedException: Assumption failure reason
+	at org.junit.Assume.assumeTrue(Assume.java:59)
+	at org.junit.Assume.assumeFalse(Assume.java:66)
+	at com.my.package.test.BasicTest.assumptionFailureTest(BasicTest.java:63)
+	at java.lang.reflect.Method.invoke(Native Method)
+	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:57)
+	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
+	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:59)
+	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
+	at android.support.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
+	at android.support.test.internal.runner.junit4.statement.RunAfters.evaluate(RunAfters.java:61)
+	at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:433)
+	at com.my.package.test.MyBaseTest$3.evaluate(MyBaseTest.java:96)
+	at com.my.package.test.MyBaseTest$4.evaluate(MyBaseTest.java:109)
+	at com.my.package.test.MyBaseTest$2.evaluate(MyBaseTest.java:77)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
+	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:81)
+	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:327)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:84)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at android.support.test.runner.AndroidJUnit4.run(AndroidJUnit4.java:99)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
+	at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
+	at com.my.package.test.runner.BaseRunner.runTests(BaseRunner.java:344)
+	at com.my.package.test.runner.BaseRunner.onStart(BaseRunner.java:330)
+	at com.my.package.test.runner.BaseRunner.onStart(BaseRunner.java:253)
+	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074)
+
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: test=assumptionFailureTest
+INSTRUMENTATION_STATUS_CODE: -4
+INSTRUMENTATION_RESULT: stream=
+
+Time: 3.139
+
+OK (1 test)
+
+
+INSTRUMENTATION_CODE: -1"""
+        expected_skipped = [
+            ('com.my.package.test.BasicTest#assumptionFailureTest',
+             signals.TestSkip),
+        ]
+        self.assert_run_instrumentation_test(
+            instrumentation_output,
+            expected_skipped=expected_skipped,
+            expected_completed_and_passed=True)
+
+    def test_run_instrumentation_test_with_ignored_test(self):
+        instrumentation_output = """\
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: test=ignoredTest
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: test=ignoredTest
+INSTRUMENTATION_STATUS_CODE: -3
+INSTRUMENTATION_RESULT: stream=
+
+Time: 0.007
+
+OK (0 tests)
+
+
+INSTRUMENTATION_CODE: -1"""
+        expected_skipped = [
+            ('com.my.package.test.BasicTest#ignoredTest', signals.TestSkip),
+        ]
+        self.assert_run_instrumentation_test(
+            instrumentation_output,
+            expected_skipped=expected_skipped,
+            expected_completed_and_passed=True)
+
+    def test_run_instrumentation_test_with_crashed_test(self):
+        instrumentation_output = """\
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: test=crashTest
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_RESULT: shortMsg=Process crashed.
+INSTRUMENTATION_CODE: 0"""
+        expected_executed = [
+            ('com.my.package.test.BasicTest#crashTest', signals.TestError),
+        ]
+        self.assert_run_instrumentation_test(
+            instrumentation_output,
+            expected_executed=expected_executed,
+            expected_has_error=True)
+
+    def test_run_instrumentation_test_with_crashing_test(self):
+        instrumentation_output = """\
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=2
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: test=crashAndRecover1Test
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=2
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=2
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: test=crashAndRecover2Test
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_RESULT: stream=
+
+Time: 6.342
+
+OK (2 tests)
+
+
+INSTRUMENTATION_CODE: -1"""
+        expected_executed = [
+            ('com.my.package.test.BasicTest#crashAndRecover1Test',
+             signals.TestError),
+            ('com.my.package.test.BasicTest#crashAndRecover2Test',
+             signals.TestError),
+        ]
+        self.assert_run_instrumentation_test(
+            instrumentation_output,
+            expected_executed=expected_executed,
+            expected_completed_and_passed=True)
+
+    def test_run_instrumentation_test_with_runner_setup_crash(self):
+        instrumentation_output = """\
+INSTRUMENTATION_RESULT: shortMsg=Process crashed.
+INSTRUMENTATION_CODE: 0"""
+        self.assert_run_instrumentation_test(
+            instrumentation_output, expected_has_error=True)
+
+    def test_run_instrumentation_test_with_runner_teardown_crash(self):
+        instrumentation_output = """\
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: test=basicTest
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: stream=.
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: test=basicTest
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS_CODE: 0
+INSTRUMENTATION_RESULT: shortMsg=Process crashed.
+INSTRUMENTATION_CODE: 0
+"""
+        expected_executed = [
+            ('com.my.package.test.BasicTest#basicTest', signals.TestPass),
+        ]
+        self.assert_run_instrumentation_test(
+            instrumentation_output,
+            expected_executed=expected_executed,
+            expected_has_error=True)
+
+    def test_run_instrumentation_test_with_multiple_tests(self):
+        instrumentation_output = """\
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=4
+INSTRUMENTATION_STATUS: stream=
+com.my.package.test.BasicTest:
+INSTRUMENTATION_STATUS: test=failingTest
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=4
+INSTRUMENTATION_STATUS: stack=java.lang.UnsupportedOperationException: dummy failing test
+	at com.my.package.test.BasicTest.failingTest(BasicTest.java:40)
+	at java.lang.reflect.Method.invoke(Native Method)
+	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:57)
+	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
+	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:59)
+	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
+	at android.support.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
+	at android.support.test.internal.runner.junit4.statement.RunAfters.evaluate(RunAfters.java:61)
+	at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:433)
+	at com.my.package.test.BaseTest$3.evaluate(BaseTest.java:96)
+	at com.my.package.test.BaseTest$4.evaluate(BaseTest.java:109)
+	at com.my.package.test.BaseTest$2.evaluate(BaseTest.java:77)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
+	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:81)
+	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:327)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:84)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at android.support.test.runner.AndroidJUnit4.run(AndroidJUnit4.java:99)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
+	at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
+	at com.my.package.test.BaseRunner.runTests(BaseRunner.java:344)
+	at com.my.package.test.BaseRunner.onStart(BaseRunner.java:330)
+	at com.my.package.test.runner.MyRunner.onStart(MyRunner.java:253)
+	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074)
+
+INSTRUMENTATION_STATUS: stream=
+Error in failingTest(com.my.package.test.BasicTest):
+java.lang.UnsupportedOperationException: dummy failing test
+	at com.my.package.test.BasicTest.failingTest(BasicTest.java:40)
+	at java.lang.reflect.Method.invoke(Native Method)
+	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:57)
+	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
+	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:59)
+	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
+	at android.support.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
+	at android.support.test.internal.runner.junit4.statement.RunAfters.evaluate(RunAfters.java:61)
+	at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:433)
+	at com.my.package.test.BaseTest$3.evaluate(BaseTest.java:96)
+	at com.my.package.test.BaseTest$4.evaluate(BaseTest.java:109)
+	at com.my.package.test.BaseTest$2.evaluate(BaseTest.java:77)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
+	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:81)
+	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:327)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:84)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at android.support.test.runner.AndroidJUnit4.run(AndroidJUnit4.java:99)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
+	at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
+	at com.my.package.test.BaseRunner.runTests(BaseRunner.java:344)
+	at com.my.package.test.BaseRunner.onStart(BaseRunner.java:330)
+	at com.my.package.test.runner.MyRunner.onStart(MyRunner.java:253)
+	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074)
+
+INSTRUMENTATION_STATUS: test=failingTest
+INSTRUMENTATION_STATUS_CODE: -2
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=2
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=4
+INSTRUMENTATION_STATUS: stream=
+INSTRUMENTATION_STATUS: test=assumptionFailureTest
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=2
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=4
+INSTRUMENTATION_STATUS: stack=org.junit.AssumptionViolatedException: Assumption failure reason
+	at org.junit.Assume.assumeTrue(Assume.java:59)
+	at com.my.package.test.BasicTest.assumptionFailureTest(BasicTest.java:61)
+	at java.lang.reflect.Method.invoke(Native Method)
+	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:57)
+	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
+	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:59)
+	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
+	at android.support.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
+	at android.support.test.internal.runner.junit4.statement.RunAfters.evaluate(RunAfters.java:61)
+	at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:433)
+	at com.my.package.test.BaseTest$3.evaluate(BaseTest.java:96)
+	at com.my.package.test.BaseTest$4.evaluate(BaseTest.java:109)
+	at com.my.package.test.BaseTest$2.evaluate(BaseTest.java:77)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
+	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:81)
+	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:327)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:84)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at android.support.test.runner.AndroidJUnit4.run(AndroidJUnit4.java:99)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
+	at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
+	at com.my.package.test.BaseRunner.runTests(BaseRunner.java:344)
+	at com.my.package.test.BaseRunner.onStart(BaseRunner.java:330)
+	at com.my.package.test.runner.MyRunner.onStart(MyRunner.java:253)
+	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074)
+
+INSTRUMENTATION_STATUS: stream=
+INSTRUMENTATION_STATUS: test=assumptionFailureTest
+INSTRUMENTATION_STATUS_CODE: -4
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=3
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=4
+INSTRUMENTATION_STATUS: stream=
+INSTRUMENTATION_STATUS: test=ignoredTest
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=3
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=4
+INSTRUMENTATION_STATUS: stream=
+INSTRUMENTATION_STATUS: test=ignoredTest
+INSTRUMENTATION_STATUS_CODE: -3
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=4
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=4
+INSTRUMENTATION_STATUS: stream=
+INSTRUMENTATION_STATUS: test=passingTest
+INSTRUMENTATION_STATUS_CODE: 1
+INSTRUMENTATION_STATUS: class=com.my.package.test.BasicTest
+INSTRUMENTATION_STATUS: current=4
+INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
+INSTRUMENTATION_STATUS: numtests=4
+INSTRUMENTATION_STATUS: stream=.
+INSTRUMENTATION_STATUS: test=passingTest
+INSTRUMENTATION_STATUS_CODE: 0
+INSTRUMENTATION_RESULT: stream=
+
+Time: 4.131
+There was 1 failure:
+1) failingTest(com.my.package.test.BasicTest)
+java.lang.UnsupportedOperationException: dummy failing test
+	at com.my.package.test.BasicTest.failingTest(BasicTest.java:40)
+	at java.lang.reflect.Method.invoke(Native Method)
+	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:57)
+	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
+	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:59)
+	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
+	at android.support.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
+	at android.support.test.internal.runner.junit4.statement.RunAfters.evaluate(RunAfters.java:61)
+	at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:433)
+	at com.my.package.test.BaseTest$3.evaluate(BaseTest.java:96)
+	at com.my.package.test.BaseTest$4.evaluate(BaseTest.java:109)
+	at com.my.package.test.BaseTest$2.evaluate(BaseTest.java:77)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
+	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
+	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:81)
+	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:327)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:84)
+	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at android.support.test.runner.AndroidJUnit4.run(AndroidJUnit4.java:99)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runners.Suite.runChild(Suite.java:128)
+	at org.junit.runners.Suite.runChild(Suite.java:27)
+	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:292)
+	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:73)
+	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:290)
+	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:60)
+	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:270)
+	at org.junit.runners.ParentRunner.run(ParentRunner.java:370)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
+	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
+	at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
+	at com.my.package.test.BaseRunner.runTests(BaseRunner.java:344)
+	at com.my.package.test.BaseRunner.onStart(BaseRunner.java:330)
+	at com.my.package.test.runner.MyRunner.onStart(MyRunner.java:253)
+	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074)
+
+FAILURES!!!
+Tests run: 3,  Failures: 1
+
+
+INSTRUMENTATION_CODE: -1"""
+        expected_executed = [
+            ('com.my.package.test.BasicTest#failingTest', signals.TestFailure),
+            ('com.my.package.test.BasicTest#passingTest', signals.TestPass),
+        ]
+        expected_skipped = [
+            ('com.my.package.test.BasicTest#assumptionFailureTest',
+             signals.TestSkip),
+            ('com.my.package.test.BasicTest#ignoredTest', signals.TestSkip),
+        ]
+        self.assert_run_instrumentation_test(
+            instrumentation_output,
+            expected_executed=expected_executed,
+            expected_skipped=expected_skipped)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/mobly/controllers/android_device_lib/adb_test.py b/tests/mobly/controllers/android_device_lib/adb_test.py
--- a/tests/mobly/controllers/android_device_lib/adb_test.py
+++ b/tests/mobly/controllers/android_device_lib/adb_test.py
@@ -14,10 +14,32 @@
 
 import mock
 
+from collections import OrderedDict
 from future.tests.base import unittest
 
 from mobly.controllers.android_device_lib import adb
 
+# Mock parameters for instrumentation.
+MOCK_INSTRUMENTATION_PACKAGE = 'com.my.instrumentation.tests'
+MOCK_INSTRUMENTATION_RUNNER = 'com.my.instrumentation.runner'
+MOCK_INSTRUMENTATION_OPTIONS = OrderedDict([
+    ('option1', 'value1'),
+    ('option2', 'value2'),
+])
+# Mock android instrumentation commands.
+MOCK_BASIC_INSTRUMENTATION_COMMAND = ('am instrument -r -w  com.my'
+                                      '.instrumentation.tests/com.android'
+                                      '.common.support.test.runner'
+                                      '.AndroidJUnitRunner')
+MOCK_RUNNER_INSTRUMENTATION_COMMAND = ('am instrument -r -w  com.my'
+                                       '.instrumentation.tests/com.my'
+                                       '.instrumentation.runner')
+MOCK_OPTIONS_INSTRUMENTATION_COMMAND = ('am instrument -r -w -e option1 value1'
+                                        ' -e option2 value2 com.my'
+                                        '.instrumentation.tests/com.android'
+                                        '.common.support.test.runner'
+                                        '.AndroidJUnitRunner')
+
 
 class AdbTest(unittest.TestCase):
     """Unit tests for mobly.controllers.android_device_lib.adb.
@@ -31,8 +53,8 @@ def _mock_process(self, mock_psutil_process, mock_popen):
         # the created process object in adb._exec_cmd()
         mock_psutil_process.return_value = mock.Mock()
 
-        mock_proc.communicate = mock.Mock(return_value=("out".encode('utf-8'),
-                                                        "err".encode('utf-8')))
+        mock_proc.communicate = mock.Mock(
+            return_value=('out'.encode('utf-8'), 'err'.encode('utf-8')))
         mock_proc.returncode = 0
         return (mock_psutil_process, mock_popen)
 
@@ -43,8 +65,8 @@ def test_exec_cmd_no_timeout_success(self, mock_psutil_process,
         self._mock_process(mock_psutil_process, mock_Popen)
 
         reply = adb.AdbProxy()._exec_cmd(
-            ["fake_cmd"], shell=False, timeout=None)
-        self.assertEqual("out", reply.decode('utf-8'))
+            ['fake_cmd'], shell=False, timeout=None)
+        self.assertEqual('out', reply.decode('utf-8'))
 
     @mock.patch('mobly.controllers.android_device_lib.adb.subprocess.Popen')
     @mock.patch('mobly.controllers.android_device_lib.adb.psutil.Process')
@@ -54,8 +76,8 @@ def test_exec_cmd_error_no_timeout(self, mock_psutil_process, mock_popen):
         mock_popen.return_value.returncode = 1
 
         with self.assertRaisesRegex(adb.AdbError,
-                                    "Error executing adb cmd .*"):
-            adb.AdbProxy()._exec_cmd(["fake_cmd"], shell=False, timeout=None)
+                                    'Error executing adb cmd .*'):
+            adb.AdbProxy()._exec_cmd(['fake_cmd'], shell=False, timeout=None)
 
     @mock.patch('mobly.controllers.android_device_lib.adb.subprocess.Popen')
     @mock.patch('mobly.controllers.android_device_lib.adb.psutil.Process')
@@ -63,8 +85,8 @@ def test_exec_cmd_with_timeout_success(self, mock_psutil_process,
                                            mock_popen):
         self._mock_process(mock_psutil_process, mock_popen)
 
-        reply = adb.AdbProxy()._exec_cmd(["fake_cmd"], shell=False, timeout=1)
-        self.assertEqual("out", reply.decode('utf-8'))
+        reply = adb.AdbProxy()._exec_cmd(['fake_cmd'], shell=False, timeout=1)
+        self.assertEqual('out', reply.decode('utf-8'))
 
     @mock.patch('mobly.controllers.android_device_lib.adb.subprocess.Popen')
     @mock.patch('mobly.controllers.android_device_lib.adb.psutil.Process')
@@ -76,8 +98,8 @@ def test_exec_cmd_timed_out(self, mock_psutil_process, mock_popen):
             adb.psutil.TimeoutExpired('Timed out'))
 
         with self.assertRaisesRegex(adb.AdbTimeoutError,
-                                    "Timed out Adb cmd .*"):
-            adb.AdbProxy()._exec_cmd(["fake_cmd"], shell=False, timeout=0.1)
+                                    'Timed out Adb cmd .*'):
+            adb.AdbProxy()._exec_cmd(['fake_cmd'], shell=False, timeout=0.1)
 
     @mock.patch('mobly.controllers.android_device_lib.adb.subprocess.Popen')
     @mock.patch('mobly.controllers.android_device_lib.adb.psutil.Process')
@@ -85,8 +107,8 @@ def test_exec_cmd_with_negative_timeout_value(self, mock_psutil_process,
                                                   mock_popen):
         self._mock_process(mock_psutil_process, mock_popen)
         with self.assertRaisesRegex(adb.AdbError,
-                                    "Timeout is a negative value: .*"):
-            adb.AdbProxy()._exec_cmd(["fake_cmd"], shell=False, timeout=-1)
+                                    'Timeout is a negative value: .*'):
+            adb.AdbProxy()._exec_cmd(['fake_cmd'], shell=False, timeout=-1)
 
     def test_exec_adb_cmd(self):
         with mock.patch.object(adb.AdbProxy, '_exec_cmd') as mock_exec_cmd:
@@ -110,6 +132,43 @@ def test_exec_adb_cmd_with_shell_true(self):
             mock_exec_cmd.assert_called_once_with(
                 '"adb" -s "12345" shell arg1 arg2', shell=True, timeout=None)
 
+    def test_instrument_without_parameters(self):
+        """Verifies the AndroidDevice object's instrument command is correct in
+        the basic case.
+        """
+        with mock.patch.object(adb.AdbProxy, '_exec_cmd') as mock_exec_cmd:
+            adb.AdbProxy().instrument(MOCK_INSTRUMENTATION_PACKAGE)
+            mock_exec_cmd.assert_called_once_with(
+                ['adb', 'shell', MOCK_BASIC_INSTRUMENTATION_COMMAND],
+                shell=False,
+                timeout=None)
+
+    def test_instrument_with_runner(self):
+        """Verifies the AndroidDevice object's instrument command is correct
+        with a runner specified.
+        """
+        with mock.patch.object(adb.AdbProxy, '_exec_cmd') as mock_exec_cmd:
+            adb.AdbProxy().instrument(
+                MOCK_INSTRUMENTATION_PACKAGE,
+                runner=MOCK_INSTRUMENTATION_RUNNER)
+            mock_exec_cmd.assert_called_once_with(
+                ['adb', 'shell', MOCK_RUNNER_INSTRUMENTATION_COMMAND],
+                shell=False,
+                timeout=None)
+
+    def test_instrument_with_options(self):
+        """Verifies the AndroidDevice object's instrument command is correct
+        with options.
+        """
+        with mock.patch.object(adb.AdbProxy, '_exec_cmd') as mock_exec_cmd:
+            adb.AdbProxy().instrument(
+                MOCK_INSTRUMENTATION_PACKAGE,
+                options=MOCK_INSTRUMENTATION_OPTIONS)
+            mock_exec_cmd.assert_called_once_with(
+                ['adb', 'shell', MOCK_OPTIONS_INSTRUMENTATION_COMMAND],
+                shell=False,
+                timeout=None)
+
 
-if __name__ == "__main__":
+if __name__ == '__main__':
     unittest.main()
diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -17,6 +17,7 @@
 import os
 import shutil
 import tempfile
+
 from future.tests.base import unittest
 
 from mobly.controllers import android_device