Repository: google/mobly

Issue:
`current_test_info` should exist between `setup_class` and `setup_test`
Right now `current_test_info` is None between `setup_class` and `setup_test`, which makes it difficult to use this field consistently.

E.g. if a test relies on this field in `on_fail`, if `setup_class` fails, the logic in `on_fail` would raise an exception for any call to `current_test_info`.

Discussion:

Patch:
diff --git a/mobly/controllers/android_device_lib/adb.py b/mobly/controllers/android_device_lib/adb.py
index 12c14bd..cb5b6b6 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.


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.