Repository: google/mobly

Issue:
Modules missing from API docs
Our [API docs](http://mobly.readthedocs.io/en/stable/)
Is missing some modules like `mobly.runtime_test_info`

Discussion:

Patch:
diff --git a/docs/mobly.rst b/docs/mobly.rst
index ee4b412..91ecb1b 100644
--- a/docs/mobly.rst
+++ b/docs/mobly.rst
@@ -35,6 +35,14 @@ mobly.config_parser module
     :undoc-members:
     :show-inheritance:
 
+mobly.expects module
+--------------------------
+
+.. automodule:: mobly.expects
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
 mobly.keys module
 -----------------
 
@@ -59,6 +67,14 @@ mobly.records module
     :undoc-members:
     :show-inheritance:
 
+mobly.runtime_test_info module
+------------------------------
+
+.. automodule:: mobly.runtime_test_info
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
 mobly.signals module
 --------------------
 
@@ -67,6 +83,14 @@ mobly.signals module
     :undoc-members:
     :show-inheritance:
 
+mobly.suite_runner module
+-------------------------
+
+.. automodule:: mobly.suite_runner
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
 mobly.test_runner module
 ------------------------
 
diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py
index 14828a4..746f819 100644
--- a/mobly/controllers/android_device.py
+++ b/mobly/controllers/android_device.py
@@ -436,8 +436,9 @@ class AndroidDevice(object):
         self._log_path = os.path.join(self._log_path_base,
                                       'AndroidDevice%s' % self._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
@@ -680,6 +681,9 @@ class AndroidDevice(object):
         execution result after device got reconnected.
 
         Example Usage:
+
+        .. code-block:: python
+
             with ad.handle_usb_disconnect():
                 try:
                   # User action that triggers USB disconnect, could throw
@@ -842,9 +846,12 @@ class AndroidDevice(object):
         """Starts the snippet apk with the given package name and connects.
 
         Examples:
-            >>> ad.load_snippet(
+
+        .. code-block:: python
+
+            ad.load_snippet(
                     name='maps', package='com.google.maps.snippets')
-            >>> ad.maps.activateZoom('3')
+            ad.maps.activateZoom('3')
 
         Args:
             name: The attribute name to which to attach the snippet server.
diff --git a/mobly/controllers/android_device_lib/adb.py b/mobly/controllers/android_device_lib/adb.py
index 12c14bd..db705e7 100644
--- a/mobly/controllers/android_device_lib/adb.py
+++ b/mobly/controllers/android_device_lib/adb.py
@@ -237,7 +237,8 @@ class AdbProxy(object):
 
     def forward(self, args=None, shell=False):
         with ADB_PORT_LOCK:
-            return self._exec_adb_cmd('forward', args, shell, timeout=None)
+            return self._exec_adb_cmd(
+                'forward', args, shell, timeout=None, stderr=None)
 
     def instrument(self, package, options=None, runner=None):
         """Runs an instrumentation command on the device.
@@ -245,6 +246,9 @@ class AdbProxy(object):
         This is a convenience wrapper to avoid parameter formatting.
 
         Example:
+
+        .. code-block:: python
+
             device.instrument(
                 'com.my.package.test',
                 options = {
diff --git a/mobly/suite_runner.py b/mobly/suite_runner.py
index a7e5f16..468b7e7 100644
--- a/mobly/suite_runner.py
+++ b/mobly/suite_runner.py
@@ -16,6 +16,8 @@
 To create a test suite, call suite_runner.run_suite() with one or more
 individual test classes. For example:
 
+.. code-block:: python
+
     from mobly import suite_runner
 
     from my.test.lib import foo_test
@@ -103,7 +105,7 @@ def run_suite(test_classes, argv=None):
         sys.exit(1)
 
 
-def _compute_selected_tests(test_classes, selected_tests):
+def compute_selected_tests(test_classes, selected_tests):
     """Computes tests to run for each class from selector strings.
 
     This function transforms a list of selector strings (such as FooTest or
@@ -112,24 +114,34 @@ def _compute_selected_tests(test_classes, selected_tests):
     that class are selected.
 
     Args:
-        test_classes: (list of class) all classes that are part of this suite.
-        selected_tests: (list of string) list of tests to execute, eg:
-            [
-                'FooTest',
-                'BarTest',
-                'BazTest.test_method_a',
-                'BazTest.test_method_b'
-            ].
-            May be empty, in which case all tests in the class are selected.
+        test_classes: list of strings, names of all the classes that are part
+            of a suite.
+        selected_tests: list of strings, list of tests to execute. If empty,
+            all classes `test_classes` are selected. E.g.
+
+            .. code-block:: python
+
+                [
+                    'FooTest',
+                    'BarTest',
+                    'BazTest.test_method_a',
+                    'BazTest.test_method_b'
+                ]
 
     Returns:
-        dict: test_name class -> list(test_name name):
-        identifiers for TestRunner. For the above example:
-        {
-            FooTest: None,
-            BarTest: None,
-            BazTest: ['test_method_a', 'test_method_b'],
-        }
+        dict: Identifiers for TestRunner. Keys are test class names; valures
+            are lists of test names within class. E.g. the example in
+            `selected_tests` would translate to:
+
+            .. code-block:: python
+
+                {
+                    FooTest: None,
+                    BarTest: None,
+                    BazTest: ['test_method_a', 'test_method_b']
+                }
+
+            This dict is easy to consume for `TestRunner`.
     """
     class_to_tests = collections.OrderedDict()
     if not selected_tests:


Test patch:
diff --git a/mobly/base_test.py b/mobly/base_test.py
index e4e047b..13a79b0 100644
--- a/mobly/base_test.py
+++ b/mobly/base_test.py
@@ -624,6 +624,10 @@ class BaseTestClass(object):
         tests = self._get_test_methods(test_names)
         try:
             # Setup for the class.
+            class_record = records.TestResultRecord('setup_class', self.TAG)
+            class_record.test_begin()
+            self.current_test_info = runtime_test_info.RuntimeTestInfo(
+                'setup_class', self.log_path, class_record)
             try:
                 self._setup_class()
             except signals.TestAbortSignal:
@@ -633,9 +637,6 @@ class BaseTestClass(object):
                 # Setup class failed for unknown reasons.
                 # Fail the class and skip all tests.
                 logging.exception('Error in setup_class %s.', self.TAG)
-                class_record = records.TestResultRecord(
-                    'setup_class', self.TAG)
-                class_record.test_begin()
                 class_record.test_error(e)
                 self._exec_procedure_func(self._on_fail, class_record)
                 self.results.add_class_error(class_record)
diff --git a/mobly/runtime_test_info.py b/mobly/runtime_test_info.py
index f4eea99..57b0742 100644
--- a/mobly/runtime_test_info.py
+++ b/mobly/runtime_test_info.py
@@ -19,10 +19,13 @@ from mobly import utils
 
 
 class RuntimeTestInfo(object):
-    """Container class for runtime information of a test.
+    """Container class for runtime information of a test or test stage.
 
     One object corresponds to one test. This is meant to be a read-only class.
 
+    This also applies to test stages like `setup_class`, which has its own
+    runtime info but is not part of any single test.
+
     Attributes:
         name: string, name of the test.
         signature: string, an identifier of the test, a combination of test
diff --git a/tests/mobly/base_test_test.py b/tests/mobly/base_test_test.py
index d78a640..a38b532 100755
--- a/tests/mobly/base_test_test.py
+++ b/tests/mobly/base_test_test.py
@@ -91,6 +91,25 @@ class BaseTestTest(unittest.TestCase):
         self.assertIsNone(actual_record.details)
         self.assertIsNone(actual_record.extras)
 
+    def test_current_test_info_in_setup_class(self):
+        class MockBaseTest(base_test.BaseTestClass):
+            def setup_class(self):
+                asserts.assert_true(
+                    self.current_test_info.name == 'setup_class',
+                    'Got unexpected test name %s.' %
+                    self.current_test_info.name)
+                output_path = self.current_test_info.output_path
+                asserts.assert_true(
+                    os.path.exists(output_path), 'test output path missing')
+                raise Exception(MSG_EXPECTED_EXCEPTION)
+
+        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls.run()
+        actual_record = bt_cls.results.error[0]
+        self.assertEqual(actual_record.test_name, 'setup_class')
+        self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
+        self.assertIsNone(actual_record.extras)
+
     def test_self_tests_list(self):
         class MockBaseTest(base_test.BaseTestClass):
             def __init__(self, controllers):
diff --git a/tests/mobly/controllers/android_device_lib/adb_test.py b/tests/mobly/controllers/android_device_lib/adb_test.py
index 7bf61ab..cf699ce 100755
--- a/tests/mobly/controllers/android_device_lib/adb_test.py
+++ b/tests/mobly/controllers/android_device_lib/adb_test.py
@@ -173,6 +173,10 @@ class AdbTest(unittest.TestCase):
         self.assertEqual(MOCK_DEFAULT_STDERR,
                          stderr_redirect.getvalue().decode('utf-8'))
 
+    def test_forward(self):
+        with mock.patch.object(adb.AdbProxy, '_exec_cmd') as mock_exec_cmd:
+            adb.AdbProxy().forward(MOCK_SHELL_COMMAND)
+
     def test_instrument_without_parameters(self):
         """Verifies the AndroidDevice object's instrument command is correct in
         the basic case.
diff --git a/tests/mobly/suite_runner_test.py b/tests/mobly/suite_runner_test.py
index dacd754..d0a9be4 100755
--- a/tests/mobly/suite_runner_test.py
+++ b/tests/mobly/suite_runner_test.py
@@ -21,7 +21,7 @@ from tests.lib import integration2_test
 
 class SuiteRunnerTest(unittest.TestCase):
     def test_select_no_args(self):
-        identifiers = suite_runner._compute_selected_tests(
+        identifiers = suite_runner.compute_selected_tests(
             test_classes=[
                 integration_test.IntegrationTest,
                 integration2_test.Integration2Test
@@ -33,7 +33,7 @@ class SuiteRunnerTest(unittest.TestCase):
         }, identifiers)
 
     def test_select_by_class(self):
-        identifiers = suite_runner._compute_selected_tests(
+        identifiers = suite_runner.compute_selected_tests(
             test_classes=[
                 integration_test.IntegrationTest,
                 integration2_test.Integration2Test
@@ -42,7 +42,7 @@ class SuiteRunnerTest(unittest.TestCase):
         self.assertEqual({integration_test.IntegrationTest: None}, identifiers)
 
     def test_select_by_method(self):
-        identifiers = suite_runner._compute_selected_tests(
+        identifiers = suite_runner.compute_selected_tests(
             test_classes=[
                 integration_test.IntegrationTest,
                 integration2_test.Integration2Test
@@ -55,7 +55,7 @@ class SuiteRunnerTest(unittest.TestCase):
         }, identifiers)
 
     def test_select_all_clobbers_method(self):
-        identifiers = suite_runner._compute_selected_tests(
+        identifiers = suite_runner.compute_selected_tests(
             test_classes=[
                 integration_test.IntegrationTest,
                 integration2_test.Integration2Test
@@ -63,7 +63,7 @@ class SuiteRunnerTest(unittest.TestCase):
             selected_tests=['IntegrationTest.test_a', 'IntegrationTest'])
         self.assertEqual({integration_test.IntegrationTest: None}, identifiers)
 
-        identifiers = suite_runner._compute_selected_tests(
+        identifiers = suite_runner.compute_selected_tests(
             test_classes=[
                 integration_test.IntegrationTest,
                 integration2_test.Integration2Test