Fix -r option breakage caused by TestRunner.register_controller.

Unregister controllers at the end of TestRunner.run so controller
objects are properly recreated for each TestRunner.run call.

Renamed clean_up to unregister_controller to better represent its
purpose.

Added a test to guard against this bug. The tests will have less
external dependencies once TestRunner can take test class instances
directly.

Bug=28351141

Change-Id: Ife542b7d92e51c9a6f47fab9ac710840016f64fe
diff --git a/acts/framework/acts/test_runner.py b/acts/framework/acts/test_runner.py
index a965f5b..534ce29 100644
--- a/acts/framework/acts/test_runner.py
+++ b/acts/framework/acts/test_runner.py
@@ -217,6 +217,20 @@
         self.controller_destructors[module_ref_name] = destroy_func
         return objects
 
+    def unregister_controllers(self):
+        """Destroy controller objects and clear internal registry.
+
+        This will be called at the end of each TestRunner.run call.
+        """
+        for name, destroy in self.controller_destructors.items():
+            try:
+                self.log.debug("Destroying %s.", name)
+                destroy(self.controller_registry[name])
+            except:
+                self.log.exception("Exception occurred destroying %s.", name)
+        self.controller_registry = {}
+        self.controller_destructors = {}
+
     def parse_config(self, test_configs):
         """Parses the test configuration and unpacks objects and parameters
         into a dictionary to be passed to test classes.
@@ -298,57 +312,56 @@
                 raise e
 
     def run(self):
-        """Kicks off the test run.
+        """Executes test cases.
 
         This will instantiate controller and test classes, and execute test
-        classes. A call to TestRunner.run should always be accompanied by a
-        call to TestRunner.stop.
+        classes. This can be called multiple times to repeatly execute the
+        requested test cases.
+
+        A call to TestRunner.stop should eventually happen to conclude the life
+        cycle of a TestRunner.
         """
         if not self.running:
             self.running = True
-        # Initialize controller objects and pack appropriate objects/params to
-        # be passed to test class.
+        # Initialize controller objects and pack appropriate objects/params
+        # to be passed to test class.
         self.parse_config(self.test_configs)
         t_configs = self.test_configs[keys.Config.key_test_paths.value]
         self.test_classes = self.import_test_modules(t_configs)
         self.log.debug("Executing run list %s.", self.run_list)
-        for test_cls_name, test_case_names in self.run_list:
-            if not self.running:
-                break
-            if test_case_names:
-                self.log.debug("Executing test cases %s in test class %s.",
-                               test_case_names,
-                               test_cls_name)
-            else:
-                self.log.debug("Executing test class %s", test_cls_name)
-            try:
-                self.run_test_class(test_cls_name, test_case_names)
-            except signals.TestAbortAll as e:
-                self.log.warning(("Abort all subsequent test classes. Reason: "
-                                  "%s"), e)
-                raise
+        try:
+            for test_cls_name, test_case_names in self.run_list:
+                if not self.running:
+                    break
+                if test_case_names:
+                    self.log.debug("Executing test cases %s in test class %s.",
+                                   test_case_names,
+                                   test_cls_name)
+                else:
+                    self.log.debug("Executing test class %s", test_cls_name)
+                try:
+                    self.run_test_class(test_cls_name, test_case_names)
+                except signals.TestAbortAll as e:
+                    self.log.warning(("Abort all subsequent test classes. Reason: "
+                                      "%s"), e)
+                    raise
+        finally:
+            self.unregister_controllers()
 
     def stop(self):
         """Releases resources from test run. Should always be called after
         TestRunner.run finishes.
+
+        This function concludes a test run and writes out a test report.
         """
         if self.running:
             msg = "\nSummary for test run %s: %s\n" % (self.id,
                 self.results.summary_str())
             self._write_results_json_str()
             self.log.info(msg.strip())
-            self.clean_up()
             logger.kill_test_logger(self.log)
             self.running = False
 
-    def clean_up(self):
-        for name, destroy in self.controller_destructors.items():
-            try:
-                self.log.debug("Destroying %s.", name)
-                destroy(self.controller_registry[name])
-            except:
-                self.log.exception("Exception occurred destroying %s.", name)
-
     def _write_results_json_str(self):
         """Writes out a json file with the test result info for easy parsing.
 
diff --git a/acts/framework/tests/HelloWorldTest.py b/acts/framework/tests/IntegrationTest.py
similarity index 87%
rename from acts/framework/tests/HelloWorldTest.py
rename to acts/framework/tests/IntegrationTest.py
index 27133ab..519dd46 100755
--- a/acts/framework/tests/HelloWorldTest.py
+++ b/acts/framework/tests/IntegrationTest.py
@@ -17,7 +17,12 @@
 from acts import asserts
 from acts import base_test
 
-class HelloWorldTest(base_test.BaseTestClass):
+import mock_controller
+
+class IntegrationTest(base_test.BaseTestClass):
+
+    def setup_class(self):
+        self.register_controller(mock_controller)
 
     def test_hello_world(self):
         asserts.assert_equal(self.user_params["icecream"], 42)
diff --git a/acts/framework/tests/acts_hello_world_test.py b/acts/framework/tests/acts_integration_test.py
similarity index 82%
rename from acts/framework/tests/acts_hello_world_test.py
rename to acts/framework/tests/acts_integration_test.py
index 81b1d11..fa344a1 100644
--- a/acts/framework/tests/acts_hello_world_test.py
+++ b/acts/framework/tests/acts_integration_test.py
@@ -18,12 +18,12 @@
 import subprocess
 import unittest
 
-class ActsHelloWorldTest(unittest.TestCase):
-    """Execute a simple hello world ACTS test to make sure the basic workflow
-    of ACTS is intact.
+class ActsIntegrationTest(unittest.TestCase):
+    """Execute a simple ACTS test to make sure the basic workflow of ACTS is
+    intact.
     """
     def test_acts(self):
-        cmd = "act.py -c acts_sanity_test_config.json -tc HelloWorldTest"
+        cmd = "act.py -c acts_sanity_test_config.json -tc IntegrationTest"
         subprocess.check_call([cmd], shell=True)
         with open("/tmp/logs/Sanity/latest/test_run_summary.json", 'r') as f:
             results = json.load(f)
diff --git a/acts/framework/tests/acts_sanity_test_config.json b/acts/framework/tests/acts_sanity_test_config.json
index fc06226..d2a1d74 100644
--- a/acts/framework/tests/acts_sanity_test_config.json
+++ b/acts/framework/tests/acts_sanity_test_config.json
@@ -4,7 +4,8 @@
         {
             "_description": "ACTS sanity test bed, no device needed.",
             "name": "Sanity",
-            "icecream": 42
+            "icecream": 42,
+            "MagicDevice": ["Magic!"]
         }
     ],
     "logpath": "/tmp/logs",
diff --git a/acts/framework/tests/acts_test_runner_test.py b/acts/framework/tests/acts_test_runner_test.py
index a99030b..15263a9 100755
--- a/acts/framework/tests/acts_test_runner_test.py
+++ b/acts/framework/tests/acts_test_runner_test.py
@@ -34,11 +34,13 @@
         self.tmp_dir = tempfile.mkdtemp()
         self.base_mock_test_config = {
             "testbed":{
-                "name": "SampleTestBed"
+                "name": "SampleTestBed",
             },
             "logpath": self.tmp_dir,
             "cli_args": None,
-            "testpaths": ["../tests/sample"]
+            "testpaths": ["./"],
+            "icecream": 42,
+            "extra_param": "haha"
         }
         self.mock_run_list = [('SampleTest', None)]
 
@@ -114,6 +116,25 @@
         self.assertEqual(magic_devices[0].magic, "magic1")
         self.assertEqual(magic_devices[1].magic, "magic2")
 
+    def test_run_twice(self):
+        mock_test_config = dict(self.base_mock_test_config)
+        tb_key = keys.Config.key_testbed.value
+        mock_ctrlr_config_name = mock_controller.ACTS_CONTROLLER_CONFIG_NAME
+        mock_test_config[tb_key][mock_ctrlr_config_name] = ["magic1", "magic2"]
+        tr = test_runner.TestRunner(self.base_mock_test_config,
+                                    [('IntegrationTest', None)])
+        tr.run()
+        self.assertFalse(tr.controller_registry)
+        self.assertFalse(tr.controller_destructors)
+        tr.run()
+        tr.stop()
+        self.assertFalse(tr.controller_registry)
+        self.assertFalse(tr.controller_destructors)
+        results = tr.results.summary_dict()
+        self.assertEqual(results["Requested"], 2)
+        self.assertEqual(results["Executed"], 2)
+        self.assertEqual(results["Passed"], 2)
+
     def test_verify_controller_module(self):
         test_runner.TestRunner.verify_controller_module(mock_controller)
 
diff --git a/acts/framework/tests/test_all b/acts/framework/tests/test_all
index 04802a2..afc962c 100755
--- a/acts/framework/tests/test_all
+++ b/acts/framework/tests/test_all
@@ -3,13 +3,13 @@
 import sys
 import unittest
 
-import acts_hello_world_test
+import acts_integration_test
 import acts_unittest_suite
 
 if __name__ == "__main__":
     # This files executes both the unit tests and the integration test.
     suite = acts_unittest_suite.compile_suite()
-    suite.addTest(acts_hello_world_test.ActsHelloWorldTest("test_acts"))
+    suite.addTest(acts_integration_test.ActsIntegrationTest("test_acts"))
     runner = unittest.TextTestRunner()
     results = runner.run(suite)
     sys.exit(not results.wasSuccessful())