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:
xpconanfan: 



Review status: 0 of 5 files reviewed at latest revision, 11 unresolved discussions.

---

*[mobly/base_instrumentation_test.py, line 25 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwheO6oVK8o23Q3RxyM:-KwheO6oVK8o23Q3RxyN:b-uxhw2t) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L25)):*
> ```Python
> 
> 
> class _InstrumentationStructurePrefixes(object):
> ```

pls add docstrings for all classes.
For this kind of things, you can provide context on what they map to in Android.
It'll be helpful if there's a link to the actual Android doc where these are referred to.

---

*[mobly/base_instrumentation_test.py, line 35 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhebfkVMPbEfTMw2cT:-KwhebfkVMPbEfTMw2cU:b-3vsjan) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L35)):*
> ```Python
> class _InstrumentationKnownStatusKeys(object):
>     CLASS = "class"
>     ERROR = "Error"
> ```

why is "Error" in different case from the rest?

---

*[mobly/base_instrumentation_test.py, line 41 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhegkTcVBPhP8ZorWc:-KwhegkTcVBPhP8ZorWd:bq4fwyn) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L41)):*
> ```Python
> 
> 
> class _InstrumentationStatusCodes(object):
> ```

Would using `enum` module be helpful for any of these enum classes?

---

*[mobly/base_instrumentation_test.py, line 86 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhetR3v-bREHQ6nurU:-KwhetR3v-bREHQ6nurV:b-b6qkc7) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L86)):*
> ```Python
> 
> 
> class _InstrumentationBlock(object):
> ```

need proper docstring for classes.

E.g. you may want to mention what this maps to in the instrumentation output.

---

*[mobly/base_instrumentation_test.py, line 112 at r1](https://reviewable.io:443/reviews/google/mobly/359#-Kwhf6ZXAbAw6b7fN8Bm:-Kwhf6ZXAbAw6b7fN8Bn:bp8ohav) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L112)):*
> ```Python
> 
>     def is_empty(self):
>         return self.empty
> ```

if this is meant to be accessed via `is_empty` only, the attribute name should be `_empty`?
Also should `is_empty` be a `@property`?

---

*[mobly/base_instrumentation_test.py, line 114 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhfJNZn6oripWinVCV:-KwhfJNZn6oripWinVCW:bfjz8ao) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L114)):*
> ```Python
>         return self.empty
> 
>     def set_error_message(self, error_message):
> ```

need docstring for all methods

---

*[mobly/base_instrumentation_test.py, line 300 at r1](https://reviewable.io:443/reviews/google/mobly/359#-Kwhe3t-_JsjXBpoSHL-:-Kwhe3t-_JsjXBpoSHL0:b-rg0non) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L300)):*
> ```Python
>         formatters = []
>         # If starting a new block and yet the previous block never completed, error the last block.
>         if instrumentation_block.previous_instrumentation_block:
> ```

use local vars to avoid repeating such long names over and over?

These nested `if` statements are difficult to read and maintain.
Consider replacing them with funcs like `all(cond1, cond2, cond3)`

---

*[mobly/controllers/android_device.py, line 411 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhddgAHjpryixc2pK2:-KwhddgAHjpryixc2pK3:b-hdyhn4) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/controllers/android_device.py#L411)):*
> ```Python
>     """
> 
>     DEFAULT_INSTRUMENTATION_RUNNER = 'com.android.common.support.test.runner.AndroidJUnitRunner'
> ```

not used?

---

*[mobly/controllers/android_device.py, line 539 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhdF0dgBwKAud_5uyX:-KwhdF0efx6o4HxoBuLz:b-jeaphc) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/controllers/android_device.py#L539)):*
> ```Python
>         * Any other type of USB disconnection, as long as snippet session can be
>             kept alive while USB disconnected (reboot caused USB disconnection
>             is
> ```

fix line breaks. a line should be as long as possible within 80 char before breaking to the next line.

---

*[mobly/controllers/android_device.py, line 1041 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhdkhXXT2dhMBs2qoF:-KwhdkhXXT2dhMBs2qoG:b-36hodg) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/controllers/android_device.py#L1041)):*
> ```Python
>             self.adb.reboot()
> 
>     def instrument(self, package, options=None, runner=None):
> ```

should this be in `AdbProxy` class instead?

---

*[mobly/controllers/android_device.py, line 1044 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhdPutoMA3kMl50aPx:-KwhdPutoMA3kMl50aPy:b-66crbv) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/controllers/android_device.py#L1044)):*
> ```Python
>         """Runs an instrumentation command on the device.
> 
>             This is a convenience wrapper to avoid parameter formatting.
> ```

seems like there is an extra indentation for all the lines in the body of this doc string.

---


*Comments from [Reviewable](https://reviewable.io:443/reviews/google/mobly/359)*
<!-- Sent from Reviewable.io -->

winterfroststrom: 



Review status: 0 of 7 files reviewed at latest revision, 11 unresolved discussions.

---

*[mobly/base_instrumentation_test.py, line 35 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhebfkVMPbEfTMw2cT:-KwiC0Q12UB-Ppl5tpfC:bvwjhn0) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L35)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

why is "Error" in different case from the rest?
</blockquote></details>

I see reference to this in other test cases, so presumably there's some version of Adb/JUnit that outputs this in some scenario.

---

*[mobly/base_instrumentation_test.py, line 41 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhegkTcVBPhP8ZorWc:-KwiBoG8riW83mTU2WP4:brsmk2i) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L41)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

Would using `enum` module be helpful for any of these enum classes?
</blockquote></details>

I considered that, but these strings actually correspond to raw output, so it would actually make the code clunkier to A.A.value instead of just A.A

---

*[mobly/controllers/android_device.py, line 411 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhddgAHjpryixc2pK2:-KwiBgVUB4NXGWhlWt9k:b-n5akmg) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/controllers/android_device.py#L411)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

not used?
</blockquote></details>

This is actually used when no runner is given?

---

*[mobly/controllers/android_device.py, line 539 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhdF0dgBwKAud_5uyX:-KwiBLEFjlUzNu-WfFjM:b-896fix) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/controllers/android_device.py#L539)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

fix line breaks. a line should be as long as possible within 80 char before breaking to the next line.
</blockquote></details>

Done.

---

*[mobly/controllers/android_device.py, line 1041 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhdkhXXT2dhMBs2qoF:-KwiBe725wqssaVdtSJD:b-896fix) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/controllers/android_device.py#L1041)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

should this be in `AdbProxy` class instead?
</blockquote></details>

Done.

---

*[mobly/controllers/android_device.py, line 1044 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhdPutoMA3kMl50aPx:-KwiBMgPrVB_n1l0yIB9:b-896fix) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/controllers/android_device.py#L1044)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

seems like there is an extra indentation for all the lines in the body of this doc string.
</blockquote></details>

Done.

---


*Comments from [Reviewable](https://reviewable.io:443/reviews/google/mobly/359)*
<!-- Sent from Reviewable.io -->

xpconanfan: Pls rebase now that #357 is merged

---

Review status: 0 of 7 files reviewed at latest revision, 10 unresolved discussions, some commit checks failed.

---

*[mobly/controllers/android_device_lib/adb.py, line 100 at r2](https://reviewable.io:443/reviews/google/mobly/359#-KwnCmhITDIty3S4sa3h:-KwnCmhITDIty3S4sa3i:b-ml5w5q) ([raw file](https://github.com/google/mobly/blob/cef533fe9e9eef3a1358ea3b8da0236f76e15424/mobly/controllers/android_device_lib/adb.py#L100)):*
> ```Python
>     """
> 
>     DEFAULT_INSTRUMENTATION_RUNNER = 'com.android.common.support.test.runner.AndroidJUnitRunner'
> ```

Should be a module-level constant?

---


*Comments from [Reviewable](https://reviewable.io:443/reviews/google/mobly/359#-:-KwnCtPI1i08_kpFBZS1:bdb65o5)*
<!-- Sent from Reviewable.io -->

winterfroststrom: I somewhat messed up rebasing, but it's not too terrible.

---

Review status: 0 of 6 files reviewed at latest revision, 11 unresolved discussions.

---

*[mobly/base_instrumentation_test.py, line 25 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwheO6oVK8o23Q3RxyM:-KwnqaHohvKTPcBn3Ep9:b-k92d97) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L25)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

pls add docstrings for all classes.
For this kind of things, you can provide context on what they map to in Android.
It'll be helpful if there's a link to the actual Android doc where these are referred to.
</blockquote></details>

I have not been able to find actual documentation for this stuff. I do know where some of this is generated from the source code level though. Also, technically, if a user is adventurous enough, they could technically just override large parts of the instrumentation runner output, which I have intention of handling.

---

*[mobly/base_instrumentation_test.py, line 86 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhetR3v-bREHQ6nurU:-Kwnsk1I_T9G9E-bNqCF:b-896fix) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L86)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

need proper docstring for classes.

E.g. you may want to mention what this maps to in the instrumentation output.
</blockquote></details>

Done.

---

*[mobly/base_instrumentation_test.py, line 112 at r1](https://reviewable.io:443/reviews/google/mobly/359#-Kwhf6ZXAbAw6b7fN8Bm:-Kwnr37hJoBdRh67xQRD:b-896fix) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L112)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

if this is meant to be accessed via `is_empty` only, the attribute name should be `_empty`?
Also should `is_empty` be a `@property`?
</blockquote></details>

Done.

---

*[mobly/base_instrumentation_test.py, line 114 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwhfJNZn6oripWinVCV:-KwnsmsezeHIa8d2_T-k:b-896fix) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L114)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

need docstring for all methods
</blockquote></details>

Done.

---

*[mobly/base_instrumentation_test.py, line 300 at r1](https://reviewable.io:443/reviews/google/mobly/359#-Kwhe3t-_JsjXBpoSHL-:-Kwnr7CjJ-V_TEPURxG4:b-896fix) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L300)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

use local vars to avoid repeating such long names over and over?

These nested `if` statements are difficult to read and maintain.
Consider replacing them with funcs like `all(cond1, cond2, cond3)`
</blockquote></details>

Done.

---

*[mobly/controllers/android_device_lib/adb.py, line 100 at r2](https://reviewable.io:443/reviews/google/mobly/359#-KwnCmhITDIty3S4sa3h:-Kwnr9A5mnVqc9cqPnM4:b34sxvf) ([raw file](https://github.com/google/mobly/blob/cef533fe9e9eef3a1358ea3b8da0236f76e15424/mobly/controllers/android_device_lib/adb.py#L100)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

Should be a module-level constant?
</blockquote></details>

I kinda want to leave it here because a user could technically specify a default per device runner this way. But I'm thinking of this as a kind of hacky semi-unsupported feature, so I think I could go either way on this.

---


*Comments from [Reviewable](https://reviewable.io:443/reviews/google/mobly/359#-:-KwnsoUuJ1mVxSXA3Q_t:bscr1ph)*
<!-- Sent from Reviewable.io -->

xpconanfan: 



Review status: 0 of 6 files reviewed at latest revision, 10 unresolved discussions.

---

*[mobly/base_instrumentation_test.py, line 25 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwheO6oVK8o23Q3RxyM:-KwtVqR6MwNhovPWUrJo:bck3w2a) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L25)):*
<details><summary><i>Previously, winterfroststrom wrote…</i></summary><blockquote>

I have not been able to find actual documentation for this stuff. I do know where some of this is generated from the source code level though. Also, technically, if a user is adventurous enough, they could technically just override large parts of the instrumentation runner output, which I have intention of handling.
</blockquote></details>

Sad... can we contact the team that owns this for help? maybe they know where it is.
As a last resort we can always directly point to Android source code.
This has to be coded somewhere...

---

*[mobly/base_instrumentation_test.py, line 228 at r3](https://reviewable.io:443/reviews/google/mobly/359#-KwtWU8f27R0-YjqdrIG:-KwtWU8f27R0-YjqdrIH:buavbmd) ([raw file](https://github.com/google/mobly/blob/8e81d458fe8269e236e04af79294e0ac8e44f1a4/mobly/base_instrumentation_test.py#L228)):*
> ```Python
>     This class is also used for storing result blocks although very little
>     needs to be done for those.
>     """
> ```

class docstrings should document public attributes in an "Attributes" section.

---

*[mobly/base_instrumentation_test.py, line 273 at r3](https://reviewable.io:443/reviews/google/mobly/359#-KwtWd5A7s2MTv1xMXua:-KwtWd5A7s2MTv1xMXub:b-wawasb) ([raw file](https://github.com/google/mobly/blob/8e81d458fe8269e236e04af79294e0ac8e44f1a4/mobly/base_instrumentation_test.py#L273)):*
> ```Python
> 
>         Args:
>             error_meessage: A string to add to the TestResultRecord to explain
> ```

Misspelled "message".

For args, let's use the convention:
```
name: type, description.
```

So this would be:

```
error_message: string, the message to add to the TestResultRecord...
```

Change all places.

---

*[mobly/base_instrumentation_test.py, line 397 at r3](https://reviewable.io:443/reviews/google/mobly/359#-KwtX4-ID-BaFwE181-1:-KwtX4-ID-BaFwE181-2:b-9vivo4) ([raw file](https://github.com/google/mobly/blob/8e81d458fe8269e236e04af79294e0ac8e44f1a4/mobly/base_instrumentation_test.py#L397)):*
> ```Python
>                 instrumentation_block.unknown_keys[key])
> 
>     def _add_part(self, parts, part):
> ```

does this really need to be a function?

seems like we can simply append all and not join the None ones

---

*[mobly/controllers/android_device_lib/adb.py, line 100 at r2](https://reviewable.io:443/reviews/google/mobly/359#-KwnCmhITDIty3S4sa3h:-KwtXafdp9UbP0fiTE9D:b-3r2b6g) ([raw file](https://github.com/google/mobly/blob/cef533fe9e9eef3a1358ea3b8da0236f76e15424/mobly/controllers/android_device_lib/adb.py#L100)):*
<details><summary><i>Previously, winterfroststrom wrote…</i></summary><blockquote>

I kinda want to leave it here because a user could technically specify a default per device runner this way. But I'm thinking of this as a kind of hacky semi-unsupported feature, so I think I could go either way on this.
</blockquote></details>

I think module level is better. One less level of reference when using it.

---


*Comments from [Reviewable](https://reviewable.io:443/reviews/google/mobly/359)*
<!-- Sent from Reviewable.io -->

winterfroststrom: 



Review status: 0 of 6 files reviewed at latest revision, 8 unresolved discussions.

---

*[mobly/base_instrumentation_test.py, line 25 at r1](https://reviewable.io:443/reviews/google/mobly/359#-KwheO6oVK8o23Q3RxyM:-Kwv7JxE7KaDmLQmxoYj:bge6sqp) ([raw file](https://github.com/google/mobly/blob/4d8d062614c1553a6bf2be8b94fb458cfc3a0689/mobly/base_instrumentation_test.py#L25)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

Sad... can we contact the team that owns this for help? maybe they know where it is.
As a last resort we can always directly point to Android source code.
This has to be coded somewhere...
</blockquote></details>

The prefixes are mostly in Android source code such as:
https://android.googlesource.com/platform/frameworks/base/+/742a67127366c376fdf188ff99ba30b27d3bf90c/cmds/am/src/com/android/commands/am/Am.java

The key-value stuff should be in the JUnit runner source code.

So, the team that owns this would be JUnit/Android?

---

*[mobly/base_instrumentation_test.py, line 397 at r3](https://reviewable.io:443/reviews/google/mobly/359#-KwtX4-ID-BaFwE181-1:-Kwv5lmByZkcM_CtuqbV:bn6uvk7) ([raw file](https://github.com/google/mobly/blob/8e81d458fe8269e236e04af79294e0ac8e44f1a4/mobly/base_instrumentation_test.py#L397)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

does this really need to be a function?

seems like we can simply append all and not join the None ones
</blockquote></details>

So, it's a difference between this?
a = []
a.append(x)
a.append(y)
a.append(z)
'.'.join(filter(lambda x: x, a)) / self._filtered_join('.', a)

And this?
a = []
self._add_part(a, x)
self._add_part(a, y)
self._add_part(a, z)
'.'.join(a)

Is there much a difference?

---

*[mobly/controllers/android_device_lib/adb.py, line 100 at r2](https://reviewable.io:443/reviews/google/mobly/359#-KwnCmhITDIty3S4sa3h:-Kwv74dy2uC7g3ceXR1-:b-896fix) ([raw file](https://github.com/google/mobly/blob/cef533fe9e9eef3a1358ea3b8da0236f76e15424/mobly/controllers/android_device_lib/adb.py#L100)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

I think module level is better. One less level of reference when using it.
</blockquote></details>

Done.

---


*Comments from [Reviewable](https://reviewable.io:443/reviews/google/mobly/359)*
<!-- Sent from Reviewable.io -->

winterfroststrom: 



Review status: 0 of 6 files reviewed at latest revision, 8 unresolved discussions.

---

*[mobly/base_instrumentation_test.py, line 228 at r3](https://reviewable.io:443/reviews/google/mobly/359#-KwtWU8f27R0-YjqdrIG:-KwvSlkw7hoLoUTTZQH0:b-896fix) ([raw file](https://github.com/google/mobly/blob/8e81d458fe8269e236e04af79294e0ac8e44f1a4/mobly/base_instrumentation_test.py#L228)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

class docstrings should document public attributes in an "Attributes" section.
</blockquote></details>

Done.

---

*[mobly/base_instrumentation_test.py, line 273 at r3](https://reviewable.io:443/reviews/google/mobly/359#-KwtWd5A7s2MTv1xMXua:-KwvSnpKGmiDs_8cMgSP:b-896fix) ([raw file](https://github.com/google/mobly/blob/8e81d458fe8269e236e04af79294e0ac8e44f1a4/mobly/base_instrumentation_test.py#L273)):*
<details><summary><i>Previously, xpconanfan (Ang Li) wrote…</i></summary><blockquote>

Misspelled "message".

For args, let's use the convention:
```
name: type, description.
```

So this would be:

```
error_message: string, the message to add to the TestResultRecord...
```

Change all places.
</blockquote></details>

Done.

---


*Comments from [Reviewable](https://reviewable.io:443/reviews/google/mobly/359)*
<!-- Sent from Reviewable.io -->


Patch:
diff --git a/README.md b/README.md
index e914afe..7d22e5f 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,9 @@ restrictions.
 ## Tutorials
 To get started with some simple tests, see the [Mobly tutorial](docs/tutorial.md).
 
+To get started running single-device Android instrumentation tests with Mobly,
+see the [instrumentation runner tutorial](docs/instrumentation_tutorial.md).
+
 ## Mobly Snippet
 The Mobly Snippet projects let users better control Android devices.
 
diff --git a/docs/conf.py b/docs/conf.py
index 2e12371..044c857 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -79,7 +79,11 @@ language = 'en'
 # 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/docs/instrumentation_tutorial.md b/docs/instrumentation_tutorial.md
new file mode 100644
index 0000000..ff3d258
--- /dev/null
+++ b/docs/instrumentation_tutorial.md
@@ -0,0 +1,190 @@
+# Running Android instrumentation tests with Mobly
+
+This tutorial shows how to write and execute Mobly tests for running Android
+instrumentation tests. For more details about instrumentation tests, please refer to
+https://developer.android.com/studio/test/index.html.
+
+## Setup Requirements
+
+*   A computer with at least 1 USB ports.
+*   Mobly package and its system dependencies installed on the computer.
+*   One Android device that is compatible with your instrumentatation and
+    application apks.
+*   Your instrumentation and applications apks for installing.
+*   A working adb setup. To check, connect one Android device to the computer
+    and make sure it has "USB debugging" enabled. Make sure the device shows up
+    in the list printed by `adb devices`.
+
+## Example Name Substitutions
+
+Here are the names that we use in this tutorial, substitute these names with
+your actual apk package and file names when using your real files:
+
+*   The application apk : `application.apk`
+*   The instrumentation apk : `instrumentation_test.apk`
+*   The instrumentation test package : `com.example.package.test`
+
+## Example 1: Running Instrumentation Tests
+
+Assuming your apks are already installed on devices. You can just subclass the
+instrumentation test class and run against your package.
+
+You will need a configuration file for Mobly to find your devices.
+
+***sample_config.yml***
+
+```yaml
+TestBeds:
+  - Name: BasicTestBed
+    Controllers:
+        AndroidDevice: '*'
+```
+
+***instrumentation_test.py***
+
+```python
+from mobly import base_instrumentation_test
+from mobly import test_runner
+from mobly.controllers import android_device
+
+class InstrumentationTest(base_instrumentation_test.BaseInstrumentationTestClass):
+    def setup_class(self):
+        self.dut = self.register_controller(android_device)[0]
+
+    def test_instrumentation(self):
+        self.run_instrumentation_test(self.dut, 'com.example.package.test')
+
+
+if __name__ == '__main__':
+  test_runner.main()
+```
+
+*To execute:*
+
+```
+$ python instrumentation_test.py -c sample_config.yml
+```
+
+*Expect*:
+
+The output from normally running your instrumentation tests along with a summary
+of the test results.
+
+## Example 2: Specifying Instrumentation Options
+
+If your instrumentation tests use instrumentation options for controlling
+behaviour, then you can put these options into your configuration file and then
+fetch them when you run your instrumentatation tests.
+
+***sample_config.yml***
+
+```yaml
+TestBeds:
+  - Name: BasicTestBed
+    Controllers:
+        AndroidDevice: '*'
+    TestParams:
+        instrumentation_option_annotation: android.support.test.filters.LargeTest
+        instrumentation_option_nonAnnotation: android.support.test.filters.SmallTest
+```
+
+***instrumentation_test.py***
+
+```python
+from mobly import base_instrumentation_test
+from mobly import test_runner
+from mobly.controllers import android_device
+
+class InstrumentationTest(base_instrumentation_test.BaseInstrumentationTestClass):
+    def setup_class(self):
+        self.dut = self.register_controller(android_device)[0]
+        self.options = self.parse_instrumentation_options(self.user_params)
+
+    def test_instrumentation(self):
+        self.run_instrumentation_test(self.dut, 'com.example.package.test',
+            options=self.options)
+
+
+if __name__ == '__main__':
+  test_runner.main()
+```
+
+*To execute:*
+
+```
+$ python instrumentation_test.py -c sample_config.yml
+```
+
+*Expect*:
+
+The output of your *LargeTest* instrumentation tests with no *SmallTest*
+instrumentation test being run.
+
+## Example 3 Using a Custom Runner
+
+If you have a custom runner that you use for instrumentation tests, then you can
+specify it in the *run_instrumentation_test* method call. Replace
+`com.example.package.test.CustomRunner` with the fully quailied package name of
+your real instrumentation runner.
+
+```python
+def test_instrumentation(self):
+  self.run_instrumentation_test(self.dut, 'com.example.package.test',
+      runner='com.example.package.test.CustomRunner')
+```
+
+## Example 4: Multiple Instrumentation Runs
+
+If you have multiple devices that you want to run instrumentation tests
+against, then you can simply call the *run_instrumentation_test* method
+multiple times. If you need to distinguish between runs, then you can specify
+a prefix.
+
+***sample_config.yml***
+
+```yaml
+TestBeds:
+  - Name: TwoDeviceTestBed
+    Controllers:
+        AndroidDevice:
+          - serial: xyz
+            label: dut
+          - serial: abc
+            label: dut
+```
+
+***instrumentation_test.py***
+
+```python
+from mobly import base_instrumentation_test
+from mobly import test_runner
+from mobly.controllers import android_device
+
+class InstrumentationTest(base_instrumentation_test.BaseInstrumentationTestClass):
+    def setup_class(self):
+        self.ads = self.register_controller(android_device)
+        # Get all of the dut devices to run instrumentation tests against.
+        self.duts = android_device.get_devices(self.ads, label='dut')
+
+    def test_instrumentation(self):
+        # Iterate over the dut devices with a corresponding index.
+        for index, dut in zip(range(len(self.duts)), self.duts):
+            # Specify a prefix to help disambiguate the runs.
+            self.run_instrumentation_test(dut, 'com.example.package.tests',
+                prefix='test_run_%s' % index)
+
+
+if __name__ == '__main__':
+  test_runner.main()
+```
+
+*To execute:*
+
+```
+$ python instrumentation_test.py -c sample_config.yml
+```
+
+*Expect*:
+
+The output from both instrumentation runs along with an aggregated summary of
+the results from both runs.
diff --git a/mobly/asserts.py b/mobly/asserts.py
index 9d1a6f2..ae119e5 100644
--- a/mobly/asserts.py
+++ b/mobly/asserts.py
@@ -31,13 +31,14 @@ _pyunit_proxy = _ProxyTest()
 
 
 def assert_equal(first, second, msg=None, extras=None):
-    """Assert an expression evaluates to true, otherwise fail the test.
+    """Assert the equality of objects, otherwise fail the test.
 
     Error message is "first != second" by default. Additional explanation can
     be supplied in the message.
 
     Args:
-        expr: The expression that is evaluated.
+        first: The first object to compare.
+        second: The second object to compare.
         msg: A string that adds additional info about the failure.
         extras: An optional field for extra information to be included in
             test result.
@@ -98,9 +99,8 @@ def assert_raises_regex(expected_exception,
         extras: An optional field for extra information to be included in
             test result.
     """
-    context = _AssertRaisesContext(expected_exception,
-                                   expected_regex,
-                                   extras=extras)
+    context = _AssertRaisesContext(
+        expected_exception, expected_regex, extras=extras)
     return context
 
 
@@ -242,9 +242,12 @@ def fail(msg, extras=None):
 def explicit_pass(msg, extras=None):
     """Explicitly pass a test.
 
-    A test with not uncaught exception will pass implicitly so the usage of
-    this is optional. It is intended for reporting extra information when a
-    test passes.
+    This will pass the test explicitly regardless of any other error happened
+    in the test body. E.g. even if errors have been recorded with `expects`,
+    the test will still be marked pass if this is called.
+
+    A test without uncaught exception will pass implicitly so this should be
+    used scarcely.
 
     Args:
         msg: A string explaining the details of the passed test.
@@ -275,8 +278,8 @@ class _AssertRaisesContext(object):
                 exc_name = self.expected.__name__
             except AttributeError:
                 exc_name = str(self.expected)
-            raise signals.TestFailure('%s not raised' % exc_name,
-                                      extras=self.extras)
+            raise signals.TestFailure(
+                '%s not raised' % exc_name, extras=self.extras)
         if not issubclass(exc_type, self.expected):
             # let unexpected exceptions pass through
             return False
@@ -289,7 +292,7 @@ class _AssertRaisesContext(object):
             expected_regexp = re.compile(expected_regexp)
         if not expected_regexp.search(str(exc_value)):
             raise signals.TestFailure(
-                '"%s" does not match "%s"' %
-                (expected_regexp.pattern, str(exc_value)),
+                '"%s" does not match "%s"' % (expected_regexp.pattern,
+                                              str(exc_value)),
                 extras=self.extras)
         return True
diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py
index 3a76426..5a4066f 100644
--- a/mobly/controllers/android_device.py
+++ b/mobly/controllers/android_device.py
@@ -366,7 +366,7 @@ def get_device(ads, **kwargs):
         raise Error('More than one device matched: %s' % serials)
 
 
-def take_bug_reports(ads, test_name, begin_time):
+def take_bug_reports(ads, test_name, begin_time, destination=None):
     """Takes bug reports on a list of android devices.
 
     If you want to take a bug report, call this function with a list of
@@ -379,13 +379,15 @@ def take_bug_reports(ads, test_name, begin_time):
         test_name: Name of the test method that triggered this bug report.
         begin_time: timestamp taken when the test started, can be either
             string or int.
+        destination: string, path to the directory where the bugreport
+            should be saved.
     """
     begin_time = mobly_logger.normalize_log_line_timestamp(str(begin_time))
 
-    def take_br(test_name, begin_time, ad):
-        ad.take_bug_report(test_name, begin_time)
+    def take_br(test_name, begin_time, ad, destination):
+        ad.take_bug_report(test_name, begin_time, destination=destination)
 
-    args = [(test_name, begin_time, ad) for ad in ads]
+    args = [(test_name, begin_time, ad, destination) for ad in ads]
     utils.concurrent_exec(take_br, args)
 
 
@@ -417,9 +419,8 @@ class AndroidDevice(object):
         log_path_base = getattr(logging, 'log_path', '/tmp/logs')
         self.log_path = os.path.join(log_path_base, 'AndroidDevice%s' % serial)
         self._debug_tag = self.serial
-        self.log = AndroidDeviceLoggerAdapter(logging.getLogger(), {
-            'tag': self.debug_tag
-        })
+        self.log = AndroidDeviceLoggerAdapter(logging.getLogger(),
+                                              {'tag': self.debug_tag})
         self.sl4a = None
         self.ed = None
         self._adb_logcat_process = None
@@ -884,7 +885,11 @@ class AndroidDevice(object):
         utils.stop_standing_subprocess(self._adb_logcat_process)
         self._adb_logcat_process = None
 
-    def take_bug_report(self, test_name, begin_time, timetout=300):
+    def take_bug_report(self,
+                        test_name,
+                        begin_time,
+                        timeout=300,
+                        destination=None):
         """Takes a bug report on the device and stores it in a file.
 
         Args:
@@ -892,6 +897,8 @@ class AndroidDevice(object):
             begin_time: Timestamp of when the test started.
             timeout: float, the number of seconds to wait for bugreport to
                 complete, default is 5min.
+            destination: string, path to the directory where the bugreport
+                should be saved.
         """
         new_br = True
         try:
@@ -902,7 +909,10 @@ class AndroidDevice(object):
                 new_br = False
         except adb.AdbError:
             new_br = False
-        br_path = os.path.join(self.log_path, 'BugReports')
+        if destination:
+            br_path = utils.abs_path(destination)
+        else:
+            br_path = os.path.join(self.log_path, 'BugReports')
         utils.create_dir(br_path)
         base_name = ',%s,%s.txt' % (begin_time, self.serial)
         if new_br:
@@ -914,8 +924,7 @@ class AndroidDevice(object):
         self.wait_for_boot_completion()
         self.log.info('Taking bugreport for %s.', test_name)
         if new_br:
-            out = self.adb.shell(
-                'bugreportz', timeout=timetout).decode('utf-8')
+            out = self.adb.shell('bugreportz', timeout=timeout).decode('utf-8')
             if not out.startswith('OK'):
                 raise DeviceError(self, 'Failed to take bugreport: %s' % out)
             br_out_path = out.split(':')[1].strip()
@@ -924,7 +933,7 @@ class AndroidDevice(object):
             # shell=True as this command redirects the stdout to a local file
             # using shell redirection.
             self.adb.bugreport(
-                ' > %s' % full_out_path, shell=True, timeout=timetout)
+                ' > %s' % full_out_path, shell=True, timeout=timeout)
         self.log.info('Bugreport for %s taken at %s.', test_name,
                       full_out_path)
 
diff --git a/mobly/controllers/android_device_lib/adb.py b/mobly/controllers/android_device_lib/adb.py
index 045f43e..4162caa 100644
--- a/mobly/controllers/android_device_lib/adb.py
+++ b/mobly/controllers/android_device_lib/adb.py
@@ -27,6 +27,9 @@ ADB = 'adb'
 # 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 @@ class AdbProxy(object):
                 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 @@ class AdbProxy(object):
         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.
diff --git a/mobly/expects.py b/mobly/expects.py
new file mode 100644
index 0000000..67f173b
--- /dev/null
+++ b/mobly/expects.py
@@ -0,0 +1,158 @@
+# 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 collections
+import contextlib
+import logging
+import time
+
+from mobly import asserts
+from mobly import records
+from mobly import signals
+
+
+class _ExpectErrorRecorder(object):
+    """Singleton used to store errors caught via `expect_*` functions in test.
+
+    This class is only instantiated once as a singleton. It holds a reference
+    to the record object for the test currently executing.
+    """
+
+    def __init__(self):
+        self.reset_internal_states()
+
+    def reset_internal_states(self, record=None):
+        """Resets the internal state of the recorder.
+
+        Args:
+            record: records.TestResultRecord, the test record for a test.
+        """
+        self._record = None
+        self._count = 0
+        self._record = record
+
+    @property
+    def has_error(self):
+        """If any error has been recorded since the last reset."""
+        return self._count > 0
+
+    @property
+    def error_count(self):
+        """The number of errors that have been recorded since last reset."""
+        return self._count
+
+    def add_error(self, error):
+        """Record an error from expect APIs.
+
+        This method generates a position stamp for the expect. The stamp is
+        composed of a timestamp and the number of errors recorded so far.
+
+        Args:
+            error: Exception or signals.ExceptionRecord, the error to add.
+        """
+        self._count += 1
+        self._record.add_error('expect@%s+%s' % (time.time(), self._count),
+                               error)
+
+
+def expect_true(condition, msg, extras=None):
+    """Expects an expression evaluates to True.
+
+    If the expectation is not met, the test is marked as fail after its
+    execution finishes.
+
+    Args:
+        expr: The expression that is evaluated.
+        msg: A string explaining the details in case of failure.
+        extras: An optional field for extra information to be included in test
+            result.
+    """
+    try:
+        asserts.assert_true(condition, msg, extras)
+    except signals.TestSignal as e:
+        logging.exception('Expected a `True` value, got `False`.')
+        recorder.add_error(e)
+
+
+def expect_false(condition, msg, extras=None):
+    """Expects an expression evaluates to False.
+
+    If the expectation is not met, the test is marked as fail after its
+    execution finishes.
+
+    Args:
+        expr: The expression that is evaluated.
+        msg: A string explaining the details in case of failure.
+        extras: An optional field for extra information to be included in test
+            result.
+    """
+    try:
+        asserts.assert_false(condition, msg, extras)
+    except signals.TestSignal as e:
+        logging.exception('Expected a `False` value, got `True`.')
+        recorder.add_error(e)
+
+
+def expect_equal(first, second, msg=None, extras=None):
+    """Expects the equality of objects, otherwise fail the test.
+
+    If the expectation is not met, the test is marked as fail after its
+    execution finishes.
+
+    Error message is "first != second" by default. Additional explanation can
+    be supplied in the message.
+
+    Args:
+        first: The first object to compare.
+        second: The second object to compare.
+        msg: A string that adds additional info about the failure.
+        extras: An optional field for extra information to be included in test
+            result.
+    """
+    try:
+        asserts.assert_equal(first, second, msg, extras)
+    except signals.TestSignal as e:
+        logging.exception('Expected %s equals to %s, but they are not.', first,
+                          second)
+        recorder.add_error(e)
+
+
+@contextlib.contextmanager
+def expect_no_raises(message=None, extras=None):
+    """Expects no exception is raised in a context.
+
+    If the expectation is not met, the test is marked as fail after its
+    execution finishes.
+
+    A default message is added to the exception `details`.
+
+    Args:
+        message: string, custom message to add to exception's `details`.
+        extras: An optional field for extra information to be included in test
+            result.
+    """
+    try:
+        yield
+    except Exception as e:
+        e_record = records.ExceptionRecord(e)
+        if extras:
+            e_record.extras = extras
+        msg = message or 'Got an unexpected exception'
+        details = '%s: %s' % (msg, e_record.details)
+        logging.exception(details)
+        e_record.details = details
+        recorder.add_error(e_record)
+
+
+recorder = _ExpectErrorRecorder()
diff --git a/mobly/records.py b/mobly/records.py
index e856c33..c708675 100644
--- a/mobly/records.py
+++ b/mobly/records.py
@@ -347,14 +347,17 @@ class TestResultRecord(object):
 
         Args:
             position: string, where this error occurred, e.g. 'teardown_test'.
-            e: An exception object.
+            e: An exception or a `signals.ExceptionRecord` object.
         """
         if self.result != TestResultEnums.TEST_RESULT_FAIL:
             self.result = TestResultEnums.TEST_RESULT_ERROR
         if position in self.extra_errors:
             raise Error('An exception is already recorded with position "%s",'
                         ' cannot reuse.' % position)
-        self.extra_errors[position] = ExceptionRecord(e, position=position)
+        if isinstance(e, ExceptionRecord):
+            self.extra_errors[position] = e
+        else:
+            self.extra_errors[position] = ExceptionRecord(e, position=position)
 
     def __str__(self):
         d = self.to_dict()


Test patch:
diff --git a/mobly/base_instrumentation_test.py b/mobly/base_instrumentation_test.py
new file mode 100644
index 0000000..902fa68
--- /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/base_test.py b/mobly/base_test.py
index a4045df..e9be08c 100644
--- a/mobly/base_test.py
+++ b/mobly/base_test.py
@@ -19,6 +19,7 @@ import inspect
 import logging
 import sys
 
+from mobly import expects
 from mobly import records
 from mobly import signals
 from mobly import runtime_test_info
@@ -345,7 +346,9 @@ class BaseTestClass(object):
         tr_record.test_begin()
         self.current_test_info = runtime_test_info.RuntimeTestInfo(
             test_name, self.log_path, tr_record)
+        expects.recorder.reset_internal_states(tr_record)
         logging.info('%s %s', TEST_CASE_TOKEN, test_name)
+        # Did teardown_test throw an error.
         teardown_test_failed = False
         try:
             try:
@@ -366,6 +369,7 @@ class BaseTestClass(object):
                                   self.current_test_name)
                 raise
             finally:
+                before_count = expects.recorder.error_count
                 try:
                     self._teardown_test(test_name)
                 except signals.TestAbortSignal:
@@ -374,6 +378,10 @@ class BaseTestClass(object):
                     logging.exception(e)
                     tr_record.add_error('teardown_test', e)
                     teardown_test_failed = True
+                else:
+                    # Check if anything failed by `expects`.
+                    if before_count < expects.recorder.error_count:
+                        teardown_test_failed = True
         except (signals.TestFailure, AssertionError) as e:
             tr_record.test_fail(e)
         except signals.TestSkip as e:
@@ -390,7 +398,12 @@ class BaseTestClass(object):
             # Exception happened during test.
             tr_record.test_error(e)
         else:
-            if not teardown_test_failed:
+            # No exception is thrown from test and teardown, if `expects` has
+            # error, the test should fail with the first error in `expects`.
+            if expects.recorder.has_error and not teardown_test_failed:
+                tr_record.test_fail()
+            # Otherwise the test passed.
+            elif not teardown_test_failed:
                 tr_record.test_pass()
         finally:
             tr_record.update_record()
diff --git a/mobly/test_runner.py b/mobly/test_runner.py
index 4cebda7..cf70258 100644
--- 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 @@ class TestRunner(object):
         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
index 0000000..32a5343
--- /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/base_test_test.py b/tests/mobly/base_test_test.py
index 1e8f4b2..f7905d9 100755
--- a/tests/mobly/base_test_test.py
+++ b/tests/mobly/base_test_test.py
@@ -22,6 +22,7 @@ from future.tests.base import unittest
 from mobly import asserts
 from mobly import base_test
 from mobly import config_parser
+from mobly import expects
 from mobly import signals
 
 from tests.lib import utils
@@ -1106,6 +1107,217 @@ class BaseTestTest(unittest.TestCase):
         self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
         self.assertEqual(actual_record.extras, MOCK_EXTRA)
 
+    def test_expect_true(self):
+        must_call = mock.Mock()
+        must_call2 = mock.Mock()
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_func(self):
+                expects.expect_true(
+                    False, MSG_EXPECTED_EXCEPTION, extras=MOCK_EXTRA)
+                must_call('ha')
+
+            def on_fail(self, record):
+                must_call2('on_fail')
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_func'])
+        must_call.assert_called_once_with('ha')
+        must_call2.assert_called_once_with('on_fail')
+        actual_record = bt_cls.results.failed[0]
+        self.assertEqual(actual_record.test_name, 'test_func')
+        self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
+        self.assertEqual(actual_record.extras, MOCK_EXTRA)
+
+    def test_expect_multiple_fails(self):
+        must_call = mock.Mock()
+        must_call2 = mock.Mock()
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_func(self):
+                expects.expect_true(False, 'msg 1', extras='1')
+                expects.expect_true(False, 'msg 2', extras='2')
+                must_call('ha')
+
+            def on_fail(self, record):
+                must_call2('on_fail')
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_func'])
+        must_call.assert_called_once_with('ha')
+        must_call2.assert_called_once_with('on_fail')
+        actual_record = bt_cls.results.failed[0]
+        self.assertEqual(actual_record.test_name, 'test_func')
+        self.assertEqual(actual_record.details, 'msg 1')
+        self.assertEqual(actual_record.extras, '1')
+        self.assertEqual(len(actual_record.extra_errors), 1)
+        second_error = list(actual_record.extra_errors.values())[0]
+        self.assertEqual(second_error.details, 'msg 2')
+        self.assertEqual(second_error.extras, '2')
+
+    def test_expect_two_tests(self):
+        """Errors in `expect` should not leak across tests.
+        """
+        must_call = mock.Mock()
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_1(self):
+                expects.expect_true(
+                    False, MSG_EXPECTED_EXCEPTION, extras=MOCK_EXTRA)
+                must_call('ha')
+
+            def test_2(self):
+                pass
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_1', 'test_2'])
+        must_call.assert_called_once_with('ha')
+        actual_record = bt_cls.results.failed[0]
+        self.assertEqual(actual_record.test_name, 'test_1')
+        self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
+        self.assertEqual(actual_record.extras, MOCK_EXTRA)
+        another_record = bt_cls.results.passed[0]
+        self.assertEqual(another_record.test_name, 'test_2')
+
+    def test_expect_no_op(self):
+        """Tests don't fail when expect is not triggered.
+        """
+        must_call = mock.Mock()
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_1(self):
+                expects.expect_true(
+                    True, MSG_EXPECTED_EXCEPTION, extras=MOCK_EXTRA)
+                must_call('ha')
+
+            def test_2(self):
+                expects.expect_false(
+                    False, MSG_EXPECTED_EXCEPTION, extras=MOCK_EXTRA)
+                must_call('ha')
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_1', 'test_2'])
+        must_call.assert_called_with('ha')
+        self.assertEqual(len(bt_cls.results.passed), 2)
+
+    def test_expect_in_teardown_test(self):
+        must_call = mock.Mock()
+        must_call2 = mock.Mock()
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_func(self):
+                pass
+
+            def teardown_test(self):
+                expects.expect_true(
+                    False, MSG_EXPECTED_EXCEPTION, extras=MOCK_EXTRA)
+                must_call('ha')
+
+            def on_fail(self, record):
+                must_call2('on_fail')
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_func'])
+        must_call.assert_called_once_with('ha')
+        must_call2.assert_called_once_with('on_fail')
+        actual_record = bt_cls.results.error[0]
+        self.assertEqual(actual_record.test_name, 'test_func')
+        self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
+        self.assertEqual(actual_record.extras, MOCK_EXTRA)
+
+    def test_expect_false(self):
+        must_call = mock.Mock()
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_func(self):
+                expects.expect_false(
+                    True, MSG_EXPECTED_EXCEPTION, extras=MOCK_EXTRA)
+                must_call('ha')
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_func'])
+        must_call.assert_called_once_with('ha')
+        actual_record = bt_cls.results.failed[0]
+        self.assertEqual(actual_record.test_name, 'test_func')
+        self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
+        self.assertEqual(actual_record.extras, MOCK_EXTRA)
+
+    def test_expect_equal(self):
+        must_call = mock.Mock()
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_func(self):
+                expects.expect_equal(
+                    1, 2, MSG_EXPECTED_EXCEPTION, extras=MOCK_EXTRA)
+                must_call('ha')
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_func'])
+        must_call.assert_called_once_with('ha')
+        actual_record = bt_cls.results.failed[0]
+        self.assertEqual(actual_record.test_name, 'test_func')
+        self.assertEqual(actual_record.details,
+                         '1 != 2 ' + MSG_EXPECTED_EXCEPTION)
+        self.assertEqual(actual_record.extras, MOCK_EXTRA)
+
+    def test_expect_no_raises_default_msg(self):
+        must_call = mock.Mock()
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_func(self):
+                with expects.expect_no_raises(extras=MOCK_EXTRA):
+                    raise Exception(MSG_EXPECTED_EXCEPTION)
+                must_call('ha')
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_func'])
+        must_call.assert_called_once_with('ha')
+        actual_record = bt_cls.results.failed[0]
+        self.assertEqual(actual_record.test_name, 'test_func')
+        self.assertEqual(
+            actual_record.details,
+            'Got an unexpected exception: %s' % MSG_EXPECTED_EXCEPTION)
+        self.assertEqual(actual_record.extras, MOCK_EXTRA)
+
+    def test_expect_no_raises_custom_msg(self):
+        must_call = mock.Mock()
+        msg = 'Some step unexpected failed'
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_func(self):
+                with expects.expect_no_raises(message=msg, extras=MOCK_EXTRA):
+                    raise Exception(MSG_EXPECTED_EXCEPTION)
+                must_call('ha')
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_func'])
+        must_call.assert_called_once_with('ha')
+        actual_record = bt_cls.results.failed[0]
+        self.assertEqual(actual_record.test_name, 'test_func')
+        self.assertEqual(actual_record.details,
+                         '%s: %s' % (msg, MSG_EXPECTED_EXCEPTION))
+        self.assertEqual(actual_record.extras, MOCK_EXTRA)
+
+    def test_expect_true_and_assert_true(self):
+        """Error thrown by assert_true should be considered the termination.
+        """
+        must_call = mock.Mock()
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def test_func(self):
+                expects.expect_true(
+                    False, MSG_EXPECTED_EXCEPTION, extras=MOCK_EXTRA)
+                must_call('ha')
+                asserts.assert_true(False, 'failed from assert_true')
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run(test_names=['test_func'])
+        must_call.assert_called_once_with('ha')
+        actual_record = bt_cls.results.failed[0]
+        self.assertEqual(actual_record.test_name, 'test_func')
+        self.assertEqual(actual_record.details, 'failed from assert_true')
+        self.assertIsNone(actual_record.extras)
+
     def test_unpack_userparams_required(self):
         """Missing a required param should raise an error."""
         required = ["some_param"]
diff --git a/tests/mobly/controllers/android_device_lib/adb_test.py b/tests/mobly/controllers/android_device_lib/adb_test.py
index 5c5f6a8..382ed01 100755
--- 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 @@ class AdbTest(unittest.TestCase):
         # 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 @@ class AdbTest(unittest.TestCase):
         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 @@ class AdbTest(unittest.TestCase):
         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 @@ class AdbTest(unittest.TestCase):
                                            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 @@ class AdbTest(unittest.TestCase):
             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 @@ class AdbTest(unittest.TestCase):
                                                   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 @@ class AdbTest(unittest.TestCase):
             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
index 949b745..33dd23c 100755
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -17,6 +17,7 @@ import mock
 import os
 import shutil
 import tempfile
+
 from future.tests.base import unittest
 
 from mobly.controllers import android_device
@@ -292,6 +293,22 @@ class AndroidDeviceTest(unittest.TestCase):
         with self.assertRaisesRegex(android_device.Error, expected_msg):
             ad.take_bug_report('test_something', 'sometime')
 
+    @mock.patch(
+        'mobly.controllers.android_device_lib.adb.AdbProxy',
+        return_value=mock_android_device.MockAdbProxy(1))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy(1))
+    @mock.patch('mobly.utils.create_dir')
+    def test_AndroidDevice_take_bug_report_with_destination(
+            self, create_dir_mock, FastbootProxy, MockAdbProxy):
+        mock_serial = 1
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        dest = tempfile.gettempdir()
+        ad.take_bug_report("test_something", "sometime", destination=dest)
+        expected_path = os.path.join(dest)
+        create_dir_mock.assert_called_with(expected_path)
+
     @mock.patch(
         'mobly.controllers.android_device_lib.adb.AdbProxy',
         return_value=mock_android_device.MockAdbProxy(