diff --git a/mobly/controller_manager.py b/mobly/controller_manager.py
new file mode 100644
index 0000000..db23688
--- /dev/null
+++ b/mobly/controller_manager.py
@@ -0,0 +1,207 @@
+# Copyright 2018 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.
+""" Module for Mobly controller management."""
+import collections
+import copy
+import logging
+import yaml
+
+from mobly import records
+from mobly import signals
+
+
+def verify_controller_module(module):
+    """Verifies a module object follows the required interface for
+    controllers.
+
+    The interface is explained in the docstring of
+    `base_test.BaseTestClass.register_controller`.
+
+    Args:
+        module: An object that is a controller module. This is usually
+            imported with import statements or loaded by importlib.
+
+    Raises:
+        ControllerError: if the module does not match the Mobly controller
+            interface, or one of the required members is null.
+    """
+    required_attributes = ('create', 'destroy', 'MOBLY_CONTROLLER_CONFIG_NAME')
+    for attr in required_attributes:
+        if not hasattr(module, attr):
+            raise signals.ControllerError(
+                'Module %s missing required controller module attribute'
+                ' %s.' % (module.__name__, attr))
+        if not getattr(module, attr):
+            raise signals.ControllerError(
+                'Controller interface %s in %s cannot be null.' %
+                (attr, module.__name__))
+
+
+class ControllerManager(object):
+    """Manages the controller objects for Mobly.
+
+    This manages the life cycles and info retrieval of all controller objects
+    used in a test.
+    """
+
+    def __init__(self, class_name, controller_configs):
+        # Controller object management.
+        self._controller_objects = collections.OrderedDict(
+        )  # controller_name: objects
+        self._controller_modules = {}  # controller_name: module
+        self._class_name = class_name
+        self._controller_configs = controller_configs
+
+    def register_controller(self, module, required=True, min_number=1):
+        """Loads a controller module and returns its loaded devices.
+
+        This is to be used in a mobly test class.
+
+        Args:
+            module: A module that follows the controller module interface.
+            required: A bool. If True, failing to register the specified
+                controller module raises exceptions. If False, the objects
+                failed to instantiate will be skipped.
+            min_number: An integer that is the minimum number of controller
+                objects to be created. Default is one, since you should not
+                register a controller module without expecting at least one
+                object.
+
+        Returns:
+            A list of controller objects instantiated from controller_module, or
+            None if no config existed for this controller and it was not a
+            required controller.
+
+        Raises:
+            ControllerError:
+                * The controller module has already been registered.
+                * 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.
+        """
+        verify_controller_module(module)
+        # Use the module's name as the ref name
+        module_ref_name = module.__name__.split('.')[-1]
+        if module_ref_name in self._controller_objects:
+            raise signals.ControllerError(
+                'Controller module %s has already been registered. It cannot '
+                'be registered again.' % module_ref_name)
+        # Create controller objects.
+        module_config_name = module.MOBLY_CONTROLLER_CONFIG_NAME
+        if module_config_name not in self._controller_configs:
+            if required:
+                raise signals.ControllerError(
+                    'No corresponding config found for %s' %
+                    module_config_name)
+            logging.warning(
+                'No corresponding config found for optional controller %s',
+                module_config_name)
+            return None
+        try:
+            # Make a deep copy of the config to pass to the controller module,
+            # in case the controller module modifies the config internally.
+            original_config = self._controller_configs[module_config_name]
+            controller_config = copy.deepcopy(original_config)
+            objects = module.create(controller_config)
+        except:
+            logging.exception(
+                'Failed to initialize objects for controller %s, abort!',
+                module_config_name)
+            raise
+        if not isinstance(objects, list):
+            raise signals.ControllerError(
+                'Controller module %s did not return a list of objects, abort.'
+                % module_ref_name)
+        # Check we got enough controller objects to continue.
+        actual_number = len(objects)
+        if actual_number < min_number:
+            module.destroy(objects)
+            raise signals.ControllerError(
+                'Expected to get at least %d controller objects, got %d.' %
+                (min_number, actual_number))
+        # Save a shallow copy of the list for internal usage, so tests can't
+        # affect internal registry by manipulating the object list.
+        self._controller_objects[module_ref_name] = copy.copy(objects)
+        logging.debug('Found %d objects for controller %s', len(objects),
+                      module_config_name)
+        self._controller_modules[module_ref_name] = module
+        return objects
+
+    def unregister_controllers(self):
+        """Destroy controller objects and clear internal registry.
+
+        This will be called after each test class.
+        """
+        # TODO(xpconanfan): actually record these errors instead of just
+        # logging them.
+        for name, module in self._controller_modules.items():
+            logging.debug('Destroying %s.', name)
+            try:
+                module.destroy(self._controller_objects[name])
+            except:
+                logging.exception('Exception occurred destroying %s.', name)
+        self._controller_objects = collections.OrderedDict()
+        self._controller_modules = {}
+
+    def _create_controller_info_record(self, controller_module_name):
+        """Creates controller info record for a particular controller type.
+
+        Info is retrieved from all the controller objects spawned from the
+        specified module, using the controller module's `get_info` function.
+
+        Args:
+            controller_module_name: string, the name of the controller module
+                to retrieve info from.
+
+        Returns:
+            A records.ControllerInfoRecord object.
+        """
+        module = self._controller_modules[controller_module_name]
+        controller_info = None
+        try:
+            controller_info = module.get_info(
+                copy.copy(self._controller_objects[controller_module_name]))
+        except AttributeError:
+            logging.warning('No optional debug info found for controller '
+                            '%s. To provide it, implement `get_info`.',
+                            controller_module_name)
+        try:
+            yaml.dump(controller_info)
+        except TypeError:
+            logging.warning('The info of controller %s in class "%s" is not '
+                            'YAML serializable! Coercing it to string.',
+                            controller_module_name, self._class_name)
+            controller_info = str(controller_info)
+        return records.ControllerInfoRecord(
+            self._class_name, module.MOBLY_CONTROLLER_CONFIG_NAME,
+            controller_info)
+
+    def get_controller_info_records(self):
+        """Get the info records for all the controller objects in the manager.
+
+        New info records for each controller object are created for every call
+        so the latest info is included.
+
+        Returns:
+            List of records.ControllerInfoRecord objects. Each opject conatins
+            the info of a type of controller
+        """
+        info_records = []
+        for controller_module_name in self._controller_objects.keys():
+            record = self._create_controller_info_record(
+                controller_module_name)
+            if record:
+                info_records.append(record)
+        return info_records
diff --git a/mobly/records.py b/mobly/records.py
index 171c0ca..1dec134 100644
--- a/mobly/records.py
+++ b/mobly/records.py
@@ -539,28 +539,16 @@ class TestResult(object):
         else:
             self.error.append(record)
 
-    def add_controller_info(self, controller_name, controller_info,
-                            test_class):
-        """Adds controller info to results.
+    def add_controller_info_record(self, controller_info_record):
+        """Adds a controller info record to results.
 
-        This can be called multiple times for each
+        This can be called multiple times for each test class.
 
         Args:
-            controller_name: string, name of the controller.
-            controller_info: yaml serializable info about the controller.
-            test_class: string, a tag for identifying a class. This should be
-                the test class's own `TAG` attribute.
+            controller_info_record: ControllerInfoRecord object to be added to
+                the result.
         """
-        info = controller_info
-        try:
-            yaml.dump(controller_info)
-        except TypeError:
-            logging.warning('The info of controller %s in class "%s" is not '
-                            'YAML serializable! Coercing it to string.',
-                            controller_name, test_class)
-            info = str(controller_info)
-        self.controller_info.append(
-            ControllerInfoRecord(test_class, controller_name, info))
+        self.controller_info.append(controller_info_record)
 
     def add_class_error(self, test_record):
         """Add a record to indicate a test class has failed before any test

Create a controller manager
The management of controller object life cycles should probably be entirely self-contained.
Moving registration mechanism from test runner to base test has improved the situation, but the core problem is still not solved.

The management is loosely composed of class vars and methods in base class, which is convoluted with the base class's internal logic. This is difficult to manage.

We should have a proper `ControllerManager`.
diff --git a/mobly/base_test.py b/mobly/base_test.py
index 045fee8..5dc5e8f 100644
--- a/mobly/base_test.py
+++ b/mobly/base_test.py
@@ -21,6 +21,7 @@ import logging
 
 from future.utils import raise_with_traceback
 
+from mobly import controller_manager
 from mobly import expects
 from mobly import records
 from mobly import runtime_test_info
@@ -46,30 +47,6 @@ class Error(Exception):
     """Raised for exceptions that occured in BaseTestClass."""
 
 
-def _verify_controller_module(module):
-    """Verifies a module object follows the required interface for
-    controllers.
-
-    Args:
-        module: An object that is a controller module. This is usually
-            imported with import statements or loaded by importlib.
-
-    Raises:
-        ControllerError: if the module does not match the Mobly controller
-            interface, or one of the required members is null.
-    """
-    required_attributes = ('create', 'destroy', 'MOBLY_CONTROLLER_CONFIG_NAME')
-    for attr in required_attributes:
-        if not hasattr(module, attr):
-            raise signals.ControllerError(
-                'Module %s missing required controller module attribute'
-                ' %s.' % (module.__name__, attr))
-        if not getattr(module, attr):
-            raise signals.ControllerError(
-                'Controller interface %s in %s cannot be null.' %
-                (attr, module.__name__))
-
-
 class BaseTestClass(object):
     """Base class for all test classes to inherit from.
 
@@ -97,8 +74,6 @@ class BaseTestClass(object):
         log_path: string, specifies the root directory for all logs written
             by a test run.
         test_bed_name: string, the name of the test bed used by a test run.
-        controller_configs: dict, configs used for instantiating controller
-            objects.
         user_params: dict, custom parameters from user, to be consumed by
             the test logic.
     """
@@ -125,7 +100,6 @@ class BaseTestClass(object):
             self.TAG = self._class_name
         # Set params.
         self.log_path = configs.log_path
-        self.controller_configs = configs.controller_configs
         self.test_bed_name = configs.test_bed_name
         self.user_params = configs.user_params
         self.results = records.TestResult()
@@ -133,10 +107,8 @@ class BaseTestClass(object):
         # Deprecated, use `self.current_test_info.name`.
         self.current_test_name = None
         self._generated_test_table = collections.OrderedDict()
-        # Controller object management.
-        self._controller_registry = collections.OrderedDict(
-        )  # controller_name: objects
-        self._controller_modules = {}  # controller_name: module
+        self._controller_manager = controller_manager.ControllerManager(
+            class_name=self.TAG, controller_configs=configs.controller_configs)
 
     def __enter__(self):
         return self
@@ -269,86 +241,15 @@ class BaseTestClass(object):
                 * `required` is True and no corresponding config can be found.
                 * Any other error occurred in the registration process.
         """
-        _verify_controller_module(module)
-        # Use the module's name as the ref name
-        module_ref_name = module.__name__.split('.')[-1]
-        if module_ref_name in self._controller_registry:
-            raise signals.ControllerError(
-                'Controller module %s has already been registered. It cannot '
-                'be registered again.' % module_ref_name)
-        # Create controller objects.
-        create = module.create
-        module_config_name = module.MOBLY_CONTROLLER_CONFIG_NAME
-        if module_config_name not in self.controller_configs:
-            if required:
-                raise signals.ControllerError(
-                    'No corresponding config found for %s' %
-                    module_config_name)
-            logging.warning(
-                'No corresponding config found for optional controller %s',
-                module_config_name)
-            return None
-        try:
-            # Make a deep copy of the config to pass to the controller module,
-            # in case the controller module modifies the config internally.
-            original_config = self.controller_configs[module_config_name]
-            controller_config = copy.deepcopy(original_config)
-            objects = create(controller_config)
-        except:
-            logging.exception(
-                'Failed to initialize objects for controller %s, abort!',
-                module_config_name)
-            raise
-        if not isinstance(objects, list):
-            raise signals.ControllerError(
-                'Controller module %s did not return a list of objects, abort.'
-                % module_ref_name)
-        # Check we got enough controller objects to continue.
-        actual_number = len(objects)
-        if actual_number < min_number:
-            module.destroy(objects)
-            raise signals.ControllerError(
-                'Expected to get at least %d controller objects, got %d.' %
-                (min_number, actual_number))
-        # Save a shallow copy of the list for internal usage, so tests can't
-        # affect internal registry by manipulating the object list.
-        self._controller_registry[module_ref_name] = copy.copy(objects)
-        logging.debug('Found %d objects for controller %s', len(objects),
-                      module_config_name)
-        self._controller_modules[module_ref_name] = module
-        return objects
-
-    def _unregister_controllers(self):
-        """Destroy controller objects and clear internal registry.
-
-        This will be called after each test class.
-        """
-        # TODO(xpconanfan): actually record these errors instead of just
-        # logging them.
-        for name, module in self._controller_modules.items():
-            try:
-                logging.debug('Destroying %s.', name)
-                module.destroy(self._controller_registry[name])
-            except:
-                logging.exception('Exception occurred destroying %s.', name)
-        self._controller_registry = collections.OrderedDict()
-        self._controller_modules = {}
+        return self._controller_manager.register_controller(
+            module, required, min_number)
 
     def _record_controller_info(self):
         # Collect controller information and write to test result.
-        for module_ref_name, objects in self._controller_registry.items():
-            module = self._controller_modules[module_ref_name]
-            try:
-                controller_info = module.get_info(copy.copy(objects))
-            except AttributeError:
-                logging.warning('No optional debug info found for controller '
-                                '%s. To provide it, implement `get_info`.',
-                                module_ref_name)
-                continue
-            self.results.add_controller_info(
-                controller_name=module.MOBLY_CONTROLLER_CONFIG_NAME,
-                controller_info=controller_info,
-                test_class=self.TAG)
+        for record in self._controller_manager.get_controller_info_records():
+            self.results.add_controller_info_record(record)
+            self.summary_writer.dump(
+                record.to_dict(), records.TestSummaryEntryType.CONTROLLER_INFO)
 
     def _setup_generated_tests(self):
         """Proxy function to guarantee the base implementation of
@@ -423,6 +324,10 @@ class BaseTestClass(object):
             self.results.add_class_error(record)
             self.summary_writer.dump(record.to_dict(),
                                      records.TestSummaryEntryType.RECORD)
+        finally:
+            # Write controller info and summary to summary file.
+            self._record_controller_info()
+            self._controller_manager.unregister_controllers()
 
     def teardown_class(self):
         """Teardown function that will be called after all the selected tests in
@@ -905,14 +810,7 @@ class BaseTestClass(object):
             setattr(e, 'results', self.results)
             raise e
         finally:
-            # Write controller info and summary to summary file.
-            self._record_controller_info()
-            for controller_info in self.results.controller_info:
-                self.summary_writer.dump(
-                    controller_info.to_dict(),
-                    records.TestSummaryEntryType.CONTROLLER_INFO)
             self._teardown_class()
-            self._unregister_controllers()
             logging.info('Summary for test class %s: %s', self.TAG,
                          self.results.summary_str())
 
diff --git a/tests/mobly/base_test_test.py b/tests/mobly/base_test_test.py
index f6349ea..ff866bb 100755
--- a/tests/mobly/base_test_test.py
+++ b/tests/mobly/base_test_test.py
@@ -267,14 +267,25 @@ class BaseTestTest(unittest.TestCase):
         on_fail_call_check.assert_called_once_with("haha")
 
     def test_teardown_class_fail_by_exception(self):
+        mock_test_config = self.mock_test_cls_configs.copy()
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        mock_ctrlr_2_config_name = mock_second_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        my_config = [{'serial': 'xxxx', 'magic': 'Magic'}]
+        mock_test_config.controller_configs[mock_ctrlr_config_name] = my_config
+        mock_test_config.controller_configs[
+            mock_ctrlr_2_config_name] = copy.copy(my_config)
+
         class MockBaseTest(base_test.BaseTestClass):
+            def setup_class(self):
+                self.register_controller(mock_controller)
+
             def test_something(self):
                 pass
 
             def teardown_class(self):
                 raise Exception(MSG_EXPECTED_EXCEPTION)
 
-        bt_cls = MockBaseTest(self.mock_test_cls_configs)
+        bt_cls = MockBaseTest(mock_test_config)
         bt_cls.run()
         test_record = bt_cls.results.passed[0]
         class_record = bt_cls.results.error[0]
@@ -287,6 +298,53 @@ class BaseTestTest(unittest.TestCase):
         expected_summary = ('Error 1, Executed 1, Failed 0, Passed 1, '
                             'Requested 1, Skipped 0')
         self.assertEqual(bt_cls.results.summary_str(), expected_summary)
+        # Verify the controller info is recorded correctly.
+        info = bt_cls.results.controller_info[0]
+        self.assertEqual(info.test_class, 'MockBaseTest')
+        self.assertEqual(info.controller_name, 'MagicDevice')
+        self.assertEqual(info.controller_info, [{
+            'MyMagic': {
+                'magic': 'Magic'
+            }
+        }])
+
+    def test_teardown_class_raise_abort_all(self):
+        mock_test_config = self.mock_test_cls_configs.copy()
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        mock_ctrlr_2_config_name = mock_second_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        my_config = [{'serial': 'xxxx', 'magic': 'Magic'}]
+        mock_test_config.controller_configs[mock_ctrlr_config_name] = my_config
+        mock_test_config.controller_configs[
+            mock_ctrlr_2_config_name] = copy.copy(my_config)
+
+        class MockBaseTest(base_test.BaseTestClass):
+            def setup_class(self):
+                self.register_controller(mock_controller)
+
+            def test_something(self):
+                pass
+
+            def teardown_class(self):
+                raise asserts.abort_all(MSG_EXPECTED_EXCEPTION)
+
+        bt_cls = MockBaseTest(mock_test_config)
+        with self.assertRaisesRegex(signals.TestAbortAll,
+                                    MSG_EXPECTED_EXCEPTION):
+            bt_cls.run()
+        test_record = bt_cls.results.passed[0]
+        self.assertTrue(bt_cls.results.is_all_pass)
+        expected_summary = ('Error 0, Executed 1, Failed 0, Passed 1, '
+                            'Requested 1, Skipped 0')
+        self.assertEqual(bt_cls.results.summary_str(), expected_summary)
+        # Verify the controller info is recorded correctly.
+        info = bt_cls.results.controller_info[0]
+        self.assertEqual(info.test_class, 'MockBaseTest')
+        self.assertEqual(info.controller_name, 'MagicDevice')
+        self.assertEqual(info.controller_info, [{
+            'MyMagic': {
+                'magic': 'Magic'
+            }
+        }])
 
     def test_setup_test_fail_by_exception(self):
         mock_on_fail = mock.Mock()
@@ -1836,113 +1894,6 @@ class BaseTestTest(unittest.TestCase):
             }
         }])
 
-    def test_register_controller_no_config(self):
-        bt_cls = MockEmptyBaseTest(self.mock_test_cls_configs)
-        with self.assertRaisesRegex(signals.ControllerError,
-                                    'No corresponding config found for'):
-            bt_cls.register_controller(mock_controller)
-
-    def test_register_controller_no_config_for_not_required(self):
-        bt_cls = MockEmptyBaseTest(self.mock_test_cls_configs)
-        self.assertIsNone(
-            bt_cls.register_controller(mock_controller, required=False))
-
-    def test_register_controller_dup_register(self):
-        """Verifies correctness of registration, internal tally of controllers
-        objects, and the right error happen when a controller module is
-        registered twice.
-        """
-        mock_test_config = self.mock_test_cls_configs.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        mock_test_config.controller_configs = {
-            mock_ctrlr_config_name: ['magic1', 'magic2']
-        }
-        bt_cls = MockEmptyBaseTest(mock_test_config)
-        bt_cls.register_controller(mock_controller)
-        registered_name = 'mock_controller'
-        self.assertTrue(registered_name in bt_cls._controller_registry)
-        mock_ctrlrs = bt_cls._controller_registry[registered_name]
-        self.assertEqual(mock_ctrlrs[0].magic, 'magic1')
-        self.assertEqual(mock_ctrlrs[1].magic, 'magic2')
-        self.assertTrue(bt_cls._controller_modules[registered_name])
-        expected_msg = 'Controller module .* has already been registered.'
-        with self.assertRaisesRegex(signals.ControllerError, expected_msg):
-            bt_cls.register_controller(mock_controller)
-
-    def test_register_controller_no_get_info(self):
-        mock_test_config = self.mock_test_cls_configs.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        get_info = getattr(mock_controller, 'get_info')
-        delattr(mock_controller, 'get_info')
-        try:
-            mock_test_config.controller_configs = {
-                mock_ctrlr_config_name: ['magic1', 'magic2']
-            }
-            bt_cls = MockEmptyBaseTest(mock_test_config)
-            bt_cls.register_controller(mock_controller)
-            self.assertEqual(bt_cls.results.controller_info, [])
-        finally:
-            setattr(mock_controller, 'get_info', get_info)
-
-    def test_register_controller_return_value(self):
-        mock_test_config = self.mock_test_cls_configs.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        mock_test_config.controller_configs = {
-            mock_ctrlr_config_name: ['magic1', 'magic2']
-        }
-        bt_cls = MockEmptyBaseTest(mock_test_config)
-        magic_devices = bt_cls.register_controller(mock_controller)
-        self.assertEqual(magic_devices[0].magic, 'magic1')
-        self.assertEqual(magic_devices[1].magic, 'magic2')
-
-    def test_register_controller_change_return_value(self):
-        mock_test_config = self.mock_test_cls_configs.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        mock_test_config.controller_configs = {
-            mock_ctrlr_config_name: ['magic1', 'magic2']
-        }
-        bt_cls = MockEmptyBaseTest(mock_test_config)
-        magic_devices = bt_cls.register_controller(mock_controller)
-        magic1 = magic_devices.pop(0)
-        self.assertIs(magic1,
-                      bt_cls._controller_registry['mock_controller'][0])
-        self.assertEqual(
-            len(bt_cls._controller_registry['mock_controller']), 2)
-
-    def test_register_controller_less_than_min_number(self):
-        mock_test_config = self.mock_test_cls_configs.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        mock_test_config.controller_configs = {
-            mock_ctrlr_config_name: ['magic1', 'magic2']
-        }
-        bt_cls = MockEmptyBaseTest(mock_test_config)
-        expected_msg = 'Expected to get at least 3 controller objects, got 2.'
-        with self.assertRaisesRegex(signals.ControllerError, expected_msg):
-            bt_cls.register_controller(mock_controller, min_number=3)
-
-    def test_verify_controller_module(self):
-        base_test._verify_controller_module(mock_controller)
-
-    def test_verify_controller_module_null_attr(self):
-        try:
-            tmp = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-            mock_controller.MOBLY_CONTROLLER_CONFIG_NAME = None
-            msg = 'Controller interface .* in .* cannot be null.'
-            with self.assertRaisesRegex(signals.ControllerError, msg):
-                base_test._verify_controller_module(mock_controller)
-        finally:
-            mock_controller.MOBLY_CONTROLLER_CONFIG_NAME = tmp
-
-    def test_verify_controller_module_missing_attr(self):
-        try:
-            tmp = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-            delattr(mock_controller, 'MOBLY_CONTROLLER_CONFIG_NAME')
-            msg = 'Module .* missing required controller module attribute'
-            with self.assertRaisesRegex(signals.ControllerError, msg):
-                base_test._verify_controller_module(mock_controller)
-        finally:
-            setattr(mock_controller, 'MOBLY_CONTROLLER_CONFIG_NAME', tmp)
-
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/mobly/controller_manager_test.py b/tests/mobly/controller_manager_test.py
new file mode 100755
index 0000000..7c9f1b6
--- /dev/null
+++ b/tests/mobly/controller_manager_test.py
@@ -0,0 +1,206 @@
+# Copyright 2018 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.
+"""Unit tests for controller manager."""
+import mock
+
+from future.tests.base import unittest
+
+from mobly import controller_manager
+from mobly import signals
+
+from tests.lib import mock_controller
+
+
+class ControllerManagerTest(unittest.TestCase):
+    """Unit tests for Mobly's ControllerManager."""
+
+    def test_verify_controller_module(self):
+        controller_manager.verify_controller_module(mock_controller)
+
+    def test_verify_controller_module_null_attr(self):
+        try:
+            tmp = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+            mock_controller.MOBLY_CONTROLLER_CONFIG_NAME = None
+            msg = 'Controller interface .* in .* cannot be null.'
+            with self.assertRaisesRegex(signals.ControllerError, msg):
+                controller_manager.verify_controller_module(mock_controller)
+        finally:
+            mock_controller.MOBLY_CONTROLLER_CONFIG_NAME = tmp
+
+    def test_verify_controller_module_missing_attr(self):
+        try:
+            tmp = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+            delattr(mock_controller, 'MOBLY_CONTROLLER_CONFIG_NAME')
+            msg = 'Module .* missing required controller module attribute'
+            with self.assertRaisesRegex(signals.ControllerError, msg):
+                controller_manager.verify_controller_module(mock_controller)
+        finally:
+            setattr(mock_controller, 'MOBLY_CONTROLLER_CONFIG_NAME', tmp)
+
+    def test_register_controller_no_config(self):
+        c_manager = controller_manager.ControllerManager('SomeClass', {})
+        with self.assertRaisesRegex(signals.ControllerError,
+                                    'No corresponding config found for'):
+            c_manager.register_controller(mock_controller)
+
+    def test_register_controller_no_config_for_not_required(self):
+        c_manager = controller_manager.ControllerManager('SomeClass', {})
+        self.assertIsNone(
+            c_manager.register_controller(mock_controller, required=False))
+
+    def test_register_controller_dup_register(self):
+        """Verifies correctness of registration, internal tally of controllers
+        objects, and the right error happen when a controller module is
+        registered twice.
+        """
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        c_manager.register_controller(mock_controller)
+        registered_name = 'mock_controller'
+        self.assertTrue(registered_name in c_manager._controller_objects)
+        mock_ctrlrs = c_manager._controller_objects[registered_name]
+        self.assertEqual(mock_ctrlrs[0].magic, 'magic1')
+        self.assertEqual(mock_ctrlrs[1].magic, 'magic2')
+        self.assertTrue(c_manager._controller_modules[registered_name])
+        expected_msg = 'Controller module .* has already been registered.'
+        with self.assertRaisesRegex(signals.ControllerError, expected_msg):
+            c_manager.register_controller(mock_controller)
+
+    def test_register_controller_return_value(self):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        magic_devices = c_manager.register_controller(mock_controller)
+        self.assertEqual(magic_devices[0].magic, 'magic1')
+        self.assertEqual(magic_devices[1].magic, 'magic2')
+
+    def test_register_controller_change_return_value(self):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        magic_devices = c_manager.register_controller(mock_controller)
+        magic1 = magic_devices.pop(0)
+        self.assertIs(magic1,
+                      c_manager._controller_objects['mock_controller'][0])
+        self.assertEqual(
+            len(c_manager._controller_objects['mock_controller']), 2)
+
+    def test_register_controller_less_than_min_number(self):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        expected_msg = 'Expected to get at least 3 controller objects, got 2.'
+        with self.assertRaisesRegex(signals.ControllerError, expected_msg):
+            c_manager.register_controller(mock_controller, min_number=3)
+
+    @mock.patch('yaml.dump', side_effect=TypeError('ha'))
+    def test_get_controller_info_record_not_serializable(self, _):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        c_manager.register_controller(mock_controller)
+        record = c_manager.get_controller_info_records()[0]
+        actual_controller_info = record.controller_info
+        self.assertEqual(actual_controller_info,
+                         "[{'MyMagic': 'magic1'}, {'MyMagic': 'magic2'}]")
+
+    def test_controller_record_exists_without_get_info(self):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        get_info = getattr(mock_controller, 'get_info')
+        delattr(mock_controller, 'get_info')
+        try:
+            c_manager.register_controller(mock_controller)
+            record = c_manager.get_controller_info_records()[0]
+            self.assertIsNone(record.controller_info)
+            self.assertEqual(record.test_class, 'SomeClass')
+            self.assertEqual(record.controller_name, 'MagicDevice')
+        finally:
+            setattr(mock_controller, 'get_info', get_info)
+
+    @mock.patch('tests.lib.mock_controller.get_info')
+    def test_get_controller_info_records_empty(self, mock_get_info_func):
+        mock_get_info_func.return_value = None
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        c_manager.register_controller(mock_controller)
+        record = c_manager.get_controller_info_records()[0]
+        self.assertIsNone(record.controller_info)
+        self.assertEqual(record.test_class, 'SomeClass')
+        self.assertEqual(record.controller_name, 'MagicDevice')
+
+    def test_get_controller_info_records(self):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        c_manager.register_controller(mock_controller)
+        record = c_manager.get_controller_info_records()[0]
+        record_dict = record.to_dict()
+        record_dict.pop('Timestamp')
+        self.assertEqual(
+            record_dict, {
+                'Controller Info': [{
+                    'MyMagic': 'magic1'
+                }, {
+                    'MyMagic': 'magic2'
+                }],
+                'Controller Name': 'MagicDevice',
+                'Test Class': 'SomeClass'
+            })
+
+    def test_get_controller_info_without_registration(self):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        self.assertFalse(c_manager.get_controller_info_records())
+
+    @mock.patch('tests.lib.mock_controller.destroy')
+    def test_unregister_controller(self, mock_destroy_func):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        objects = c_manager.register_controller(mock_controller)
+        c_manager.unregister_controllers()
+        mock_destroy_func.assert_called_once_with(objects)
+        self.assertFalse(c_manager._controller_objects)
+        self.assertFalse(c_manager._controller_modules)
+
+    @mock.patch('tests.lib.mock_controller.destroy')
+    def test_unregister_controller_without_registration(
+            self, mock_destroy_func):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        c_manager.unregister_controllers()
+        mock_destroy_func.assert_not_called()
+        self.assertFalse(c_manager._controller_objects)
+        self.assertFalse(c_manager._controller_modules)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/mobly/records_test.py b/tests/mobly/records_test.py
index a967b8b..9648637 100755
--- a/tests/mobly/records_test.py
+++ b/tests/mobly/records_test.py
@@ -239,15 +239,18 @@ class RecordsTest(unittest.TestCase):
         record1.test_pass(s)
         tr1 = records.TestResult()
         tr1.add_record(record1)
-        tr1.add_controller_info('SomeClass', 'MockDevice',
-                                ['magicA', 'magicB'])
+        controller_info = records.ControllerInfoRecord(
+            'SomeClass', 'MockDevice', ['magicA', 'magicB'])
+        tr1.add_controller_info_record(controller_info)
         record2 = records.TestResultRecord(self.tn)
         record2.test_begin()
         s = signals.TestPass(self.details, self.json_extra)
         record2.test_pass(s)
         tr2 = records.TestResult()
         tr2.add_record(record2)
-        tr2.add_controller_info('SomeClass', 'MockDevice', ['magicC'])
+        controller_info = records.ControllerInfoRecord(
+            'SomeClass', 'MockDevice', ['magicC'])
+        tr2.add_controller_info_record(controller_info)
         tr2 += tr1
         self.assertTrue(tr2.passed, [tr1, tr2])
         self.assertTrue(tr2.controller_info, {'MockDevice': ['magicC']})
@@ -413,25 +416,17 @@ class RecordsTest(unittest.TestCase):
         self.assertIsNot(er, new_er)
         self.assertDictEqual(er.to_dict(), new_er.to_dict())
 
-    def test_add_controller_info(self):
+    def test_add_controller_info_record(self):
         tr = records.TestResult()
         self.assertFalse(tr.controller_info)
-        tr.add_controller_info('MockDevice', ['magicA', 'magicB'], 'MyTest')
+        controller_info = records.ControllerInfoRecord(
+            'SomeClass', 'MockDevice', ['magicA', 'magicB'])
+        tr.add_controller_info_record(controller_info)
         self.assertTrue(tr.controller_info[0])
         self.assertEqual(tr.controller_info[0].controller_name, 'MockDevice')
         self.assertEqual(tr.controller_info[0].controller_info,
                          ['magicA', 'magicB'])
 
-    @mock.patch('yaml.dump', side_effect=TypeError('ha'))
-    def test_add_controller_info_not_serializable(self, mock_yaml_dump):
-        tr = records.TestResult()
-        self.assertFalse(tr.controller_info)
-        tr.add_controller_info('MockDevice', ['magicA', 'magicB'], 'MyTest')
-        self.assertTrue(tr.controller_info[0])
-        self.assertEqual(tr.controller_info[0].controller_name, 'MockDevice')
-        self.assertEqual(tr.controller_info[0].controller_info,
-                         "['magicA', 'magicB']")
-
 
 if __name__ == '__main__':
     unittest.main()

pip install -e .
parse_log_pytest
requirements.txt
mock pytest pytz
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup==1.2.2
future==1.0.0
iniconfig==2.1.0
-e git+https://github.com/google/mobly.git@5fdd0397ec5b32ac61be7b47e1c35ce84943e87c#egg=mobly
mock==5.2.0
packaging==24.2
pluggy==1.5.0
portpicker==1.6.0
psutil==7.0.0
pyserial==3.5
pytest==8.3.5
pytz==2025.2
PyYAML==6.0.2
timeout-decorator==0.5.0
tomli==2.2.1

name: mobly
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - exceptiongroup==1.2.2
      - future==1.0.0
      - iniconfig==2.1.0
      - mock==5.2.0
      - packaging==24.2
      - pluggy==1.5.0
      - portpicker==1.6.0
      - psutil==7.0.0
      - pyserial==3.5
      - pytest==8.3.5
      - pytz==2025.2
      - pyyaml==6.0.2
      - timeout-decorator==0.5.0
      - tomli==2.2.1
prefix: /opt/conda/envs/mobly

tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_on_fail tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_on_fail_from_setup_class tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_setup_class tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_setup_test tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_teardown_class tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_test tests/mobly/base_test_test.py::BaseTestTest::test_abort_class_in_on_fail tests/mobly/base_test_test.py::BaseTestTest::test_abort_class_in_setup_test tests/mobly/base_test_test.py::BaseTestTest::test_abort_class_in_test tests/mobly/base_test_test.py::BaseTestTest::test_abort_class_setup_class tests/mobly/base_test_test.py::BaseTestTest::test_assert_equal_fail tests/mobly/base_test_test.py::BaseTestTest::test_assert_equal_fail_with_msg tests/mobly/base_test_test.py::BaseTestTest::test_assert_equal_pass tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_fail_with_noop tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_fail_with_wrong_error tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_fail_with_wrong_regex tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_pass tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_regex_fail_with_noop tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_regex_fail_with_wrong_error tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_regex_pass tests/mobly/base_test_test.py::BaseTestTest::test_assert_true tests/mobly/base_test_test.py::BaseTestTest::test_both_teardown_and_test_body_raise_exceptions tests/mobly/base_test_test.py::BaseTestTest::test_cli_test_selection_fail_by_convention tests/mobly/base_test_test.py::BaseTestTest::test_cli_test_selection_override_self_tests_list tests/mobly/base_test_test.py::BaseTestTest::test_current_test_info tests/mobly/base_test_test.py::BaseTestTest::test_current_test_info_in_setup_class tests/mobly/base_test_test.py::BaseTestTest::test_current_test_name tests/mobly/base_test_test.py::BaseTestTest::test_default_execution_of_all_tests tests/mobly/base_test_test.py::BaseTestTest::test_exception_objects_in_record tests/mobly/base_test_test.py::BaseTestTest::test_expect_equal tests/mobly/base_test_test.py::BaseTestTest::test_expect_false tests/mobly/base_test_test.py::BaseTestTest::test_expect_in_teardown_test tests/mobly/base_test_test.py::BaseTestTest::test_expect_multiple_fails tests/mobly/base_test_test.py::BaseTestTest::test_expect_no_op tests/mobly/base_test_test.py::BaseTestTest::test_expect_no_raises_custom_msg tests/mobly/base_test_test.py::BaseTestTest::test_expect_no_raises_default_msg tests/mobly/base_test_test.py::BaseTestTest::test_expect_true tests/mobly/base_test_test.py::BaseTestTest::test_expect_true_and_assert_true tests/mobly/base_test_test.py::BaseTestTest::test_expect_two_tests tests/mobly/base_test_test.py::BaseTestTest::test_explicit_pass tests/mobly/base_test_test.py::BaseTestTest::test_explicit_pass_but_teardown_test_raises_an_exception tests/mobly/base_test_test.py::BaseTestTest::test_fail tests/mobly/base_test_test.py::BaseTestTest::test_failure_in_procedure_functions_is_recorded tests/mobly/base_test_test.py::BaseTestTest::test_failure_to_call_procedure_function_is_recorded tests/mobly/base_test_test.py::BaseTestTest::test_generate_tests_call_outside_of_setup_generated_tests tests/mobly/base_test_test.py::BaseTestTest::test_generate_tests_dup_test_name tests/mobly/base_test_test.py::BaseTestTest::test_generate_tests_run tests/mobly/base_test_test.py::BaseTestTest::test_generate_tests_selected_run tests/mobly/base_test_test.py::BaseTestTest::test_implicit_pass tests/mobly/base_test_test.py::BaseTestTest::test_missing_requested_test_func tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_cannot_modify_original_record tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_both_test_and_teardown_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_setup_class_fails_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_setup_test_fails_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_teardown_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_raise_exception tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_triggered_by_setup_class_failure_then_fail_too tests/mobly/base_test_test.py::BaseTestTest::test_on_pass_cannot_modify_original_record tests/mobly/base_test_test.py::BaseTestTest::test_on_pass_raise_exception tests/mobly/base_test_test.py::BaseTestTest::test_procedure_function_gets_correct_record tests/mobly/base_test_test.py::BaseTestTest::test_promote_extra_errors_to_termination_signal tests/mobly/base_test_test.py::BaseTestTest::test_record_controller_info tests/mobly/base_test_test.py::BaseTestTest::test_self_tests_list tests/mobly/base_test_test.py::BaseTestTest::test_self_tests_list_fail_by_convention tests/mobly/base_test_test.py::BaseTestTest::test_setup_and_teardown_execution_count tests/mobly/base_test_test.py::BaseTestTest::test_setup_class_fail_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_setup_generated_tests_failure tests/mobly/base_test_test.py::BaseTestTest::test_setup_test_fail_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_setup_test_fail_by_test_signal tests/mobly/base_test_test.py::BaseTestTest::test_skip tests/mobly/base_test_test.py::BaseTestTest::test_skip_if tests/mobly/base_test_test.py::BaseTestTest::test_skip_in_setup_test tests/mobly/base_test_test.py::BaseTestTest::test_teardown_class_fail_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_teardown_class_raise_abort_all tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_assert_fail tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_executed_if_setup_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_executed_if_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_executed_if_test_pass tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_raise_exception tests/mobly/base_test_test.py::BaseTestTest::test_uncaught_exception tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_basic tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_default_None tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_default_overwrite tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_default_overwrite_by_optional_param_list tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_default_overwrite_by_required_param_list tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_optional tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_optional_missing tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_optional_with_default tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_required tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_required_missing tests/mobly/controller_manager_test.py::ControllerManagerTest::test_controller_record_exists_without_get_info tests/mobly/controller_manager_test.py::ControllerManagerTest::test_get_controller_info_record_not_serializable tests/mobly/controller_manager_test.py::ControllerManagerTest::test_get_controller_info_records tests/mobly/controller_manager_test.py::ControllerManagerTest::test_get_controller_info_records_empty tests/mobly/controller_manager_test.py::ControllerManagerTest::test_get_controller_info_without_registration tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_change_return_value tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_dup_register tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_less_than_min_number tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_no_config tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_no_config_for_not_required tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_return_value tests/mobly/controller_manager_test.py::ControllerManagerTest::test_unregister_controller tests/mobly/controller_manager_test.py::ControllerManagerTest::test_unregister_controller_without_registration tests/mobly/controller_manager_test.py::ControllerManagerTest::test_verify_controller_module tests/mobly/controller_manager_test.py::ControllerManagerTest::test_verify_controller_module_missing_attr tests/mobly/controller_manager_test.py::ControllerManagerTest::test_verify_controller_module_null_attr tests/mobly/records_test.py::RecordsTest::test_add_controller_info_record tests/mobly/records_test.py::RecordsTest::test_exception_record_deepcopy tests/mobly/records_test.py::RecordsTest::test_is_all_pass tests/mobly/records_test.py::RecordsTest::test_is_all_pass_negative tests/mobly/records_test.py::RecordsTest::test_is_all_pass_with_add_class_error tests/mobly/records_test.py::RecordsTest::test_is_test_executed tests/mobly/records_test.py::RecordsTest::test_result_add_class_error_with_special_error tests/mobly/records_test.py::RecordsTest::test_result_add_class_error_with_test_signal tests/mobly/records_test.py::RecordsTest::test_result_add_operator_success tests/mobly/records_test.py::RecordsTest::test_result_add_operator_type_mismatch tests/mobly/records_test.py::RecordsTest::test_result_record_fail_none tests/mobly/records_test.py::RecordsTest::test_result_record_fail_stacktrace tests/mobly/records_test.py::RecordsTest::test_result_record_fail_with_float_extra tests/mobly/records_test.py::RecordsTest::test_result_record_fail_with_json_extra tests/mobly/records_test.py::RecordsTest::test_result_record_fail_with_unicode_exception tests/mobly/records_test.py::RecordsTest::test_result_record_fail_with_unicode_test_signal tests/mobly/records_test.py::RecordsTest::test_result_record_pass_none tests/mobly/records_test.py::RecordsTest::test_result_record_pass_with_float_extra tests/mobly/records_test.py::RecordsTest::test_result_record_pass_with_json_extra tests/mobly/records_test.py::RecordsTest::test_result_record_skip_none tests/mobly/records_test.py::RecordsTest::test_result_record_skip_with_float_extra tests/mobly/records_test.py::RecordsTest::test_result_record_skip_with_json_extra
tests/mobly/base_test_test.py::BaseTestTest::test_write_user_data tests/mobly/records_test.py::RecordsTest::test_summary_user_data tests/mobly/records_test.py::RecordsTest::test_summary_write_dump tests/mobly/records_test.py::RecordsTest::test_summary_write_dump_with_unicode
mobly/records.py mobly/controller_manager.py
mobly/records.py mobly/controller_manager.py