merge in nyc-release history after reset to nyc-dev
diff --git a/acts/framework/acts/controllers/diag_logger.py b/acts/framework/acts/controllers/diag_logger.py
new file mode 100644
index 0000000..96f1f03
--- /dev/null
+++ b/acts/framework/acts/controllers/diag_logger.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2016 - The Android Open Source Project
+#
+#   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 importlib
+import logging
+import os
+
+ACTS_CONTROLLER_CONFIG_NAME = "DiagLogger"
+ACTS_CONTROLLER_REFERENCE_NAME = "diag_logger"
+
+
+class DiagLoggerError(Exception):
+    """This is the base exception class for errors generated by
+    DiagLogger modules.
+    """
+    pass
+
+
+def create(configs, logger):
+    """Initializes the Diagnotic Logger instances based on the
+    provided JSON configuration(s). The expected keys are:
+
+    Package: A package name containing the diagnostic logger
+        module. It should be in python path of the environment.
+    Type: A first-level type for the Logger, which should correspond
+        the name of the module containing the Logger implementation.
+    SubType: The exact implementation of the sniffer, which should
+        correspond to the name of the class to be used.
+    HostLogPath: This is the default directory used to dump any logs
+        that are captured or any other files that are stored as part
+        of the logging process. It's use is implementation specific,
+        but it should be provided by all loggers for completeness.
+    Configs: A dictionary specifying baseline configurations of the
+        particular Logger. These configurations may be overridden at
+        the start of a session.
+    """
+    objs = []
+    for c in configs:
+        diag_package_name = c["Package"]  # package containing module
+        diag_logger_type = c["Type"]  # module name
+        diag_logger_name = c["SubType"]  # class name
+        host_log_path = c["HostLogPath"]
+        base_configs = c["Configs"]
+        module_name = "{}.{}".format(diag_package_name, diag_logger_type)
+        module = importlib.import_module(module_name)
+        logger = getattr(module, diag_logger_name)
+
+        objs.append(logger(host_log_path,
+                           logger,
+                           config_container=base_configs))
+    return objs
+
+
+def destroy(objs):
+    """Stops all ongoing logger sessions, deletes any temporary files, and
+    prepares logger objects for destruction.
+    """
+    for diag_logger in objs:
+        try:
+            diag_logger.reset()
+        except DiagLoggerError:
+            # TODO: log if things go badly here
+            pass
+
+
+class DiagLoggerBase():
+    """Base Class for Proprietary Diagnostic Log Collection
+
+    The DiagLoggerBase is a simple interface for running on-device logging via
+    a standard workflow that can be integrated without the caller actually
+    needing to know the details of what logs are captured or how.
+    The workflow is as follows:
+
+    1) Create a DiagLoggerBase Object
+    2) Call start() to begin an active logging session.
+    3) Call stop() to end an active logging session.
+    4) Call pull() to ensure all collected logs are stored at
+       'host_log_path'
+    5) Call reset() to stop all logging and clear any unretrieved logs.
+    """
+
+    def __init__(self, host_log_path, logger=None, config_container=None):
+        """Create a Diagnostic Logging Proxy Object
+
+        Args:
+            host_log_path: File path where retrieved logs should be stored
+            config_container: A transparent container used to pass config info
+        """
+        self.host_log_path = os.path.realpath(os.path.expanduser(
+            host_log_path))
+        self.config_container = config_container
+        if not os.path.isdir(self.host_log_path):
+            os.mkdir(self.host_log_path)
+        self.logger = logger
+        if not self.logger:
+            self.logger = logging.getLogger(self.__class__.__name__)
+
+    def start(self, config_container=None):
+        """Start collecting Diagnostic Logs
+
+        Args:
+            config_container: A transparent container used to pass config info
+
+        Returns:
+            A logging session ID that can be later used to stop the session
+            For Diag interfaces supporting only one session this is unneeded
+        """
+        raise NotImplementedError("Base class should not be invoked directly!")
+
+    def stop(self, session_id=None):
+        """Stop collecting Diagnostic Logs
+
+        Args:
+            session_id: an optional session id provided for multi session
+                        logging support
+
+        Returns:
+        """
+        raise NotImplementedError("Base class should not be invoked directly!")
+
+    def pull(self, session_id=None, out_path=None):
+        """Save all cached diagnostic logs collected to the host
+
+        Args:
+            session_id: an optional session id provided for multi session
+                        logging support
+
+            out_path: an optional override to host_log_path for a specific set
+                      of logs
+
+        Returns:
+            An integer representing a port number on the host available for adb
+            forward.
+        """
+        raise NotImplementedError("Base class should not be invoked directly!")
+
+    def reset(self):
+        """Stop any ongoing logging sessions and clear any cached logs that have
+        not been retrieved with pull(). This must delete all session records and
+        return the logging object to a state equal to when constructed.
+        """
+        raise NotImplementedError("Base class should not be invoked directly!")
+
+    def get_log_path(self):
+        """Return the log path for this object"""
+        return self.host_log_path
diff --git a/acts/framework/acts/test_runner.py b/acts/framework/acts/test_runner.py
index bcf1706..bd4c678 100644
--- a/acts/framework/acts/test_runner.py
+++ b/acts/framework/acts/test_runner.py
@@ -14,6 +14,8 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+__author__ = "angli@google.com"
+
 from future import standard_library
 standard_library.install_aliases()
 
@@ -159,7 +161,7 @@
                 raise signals.ControllerError(("Controller interface %s in %s "
                     "cannot be null.") % (attr, module.__name__))
 
-    def register_controller(self, module):
+    def register_controller(self, module, required=True):
         """Registers a controller module for a test run.
 
         This declares a controller dependency of this test class. If the target
@@ -169,13 +171,20 @@
 
         Params:
             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, returns
+                      None upon failures.
 
         Returns:
-            A list of controller objects instantiated from controller_module.
+            A list of controller objects instantiated from controller_module, or
+            None.
 
         Raises:
-            ControllerError is raised if no corresponding config can be found,
-            or if the controller module has already been registered.
+            When required is True, ControllerError is raised if no corresponding
+            config can be found.
+            Regardless of the value of "required", ControllerError is raised if
+            the controller module has already been registered or any other error
+            occurred in the registration process.
         """
         TestRunner.verify_controller_module(module)
         try:
@@ -195,8 +204,14 @@
         create = module.create
         module_config_name = module.ACTS_CONTROLLER_CONFIG_NAME
         if module_config_name not in self.testbed_configs:
-            raise signals.ControllerError(("No corresponding config found for"
-                                           " %s") % module_config_name)
+            if required:
+                raise signals.ControllerError(
+                    "No corresponding config found for %s" %
+                    module_config_name)
+            self.log.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.
diff --git a/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py b/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
index c6b060c..3187ca7 100644
--- a/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
+++ b/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
@@ -20,6 +20,9 @@
 import os
 import time
 import traceback
+
+import acts.controllers.diag_logger
+
 from acts.base_test import BaseTestClass
 from acts.signals import TestSignal
 from acts import utils
@@ -51,6 +54,7 @@
 class TelephonyBaseTest(BaseTestClass):
     def __init__(self, controllers):
         BaseTestClass.__init__(self, controllers)
+        self.logger_sessions = []
 
     # Use for logging in the test cases to facilitate
     # faster log lookup and reduce ambiguity in logging.
@@ -105,6 +109,9 @@
         return _safe_wrap_test_case
 
     def setup_class(self):
+        setattr(self, "diag_logger",
+                self.register_controller(acts.controllers.diag_logger,
+                                         required=False))
         for ad in self.android_devices:
             setup_droid_properties(self.log, ad,
                                    self.user_params["sim_conf_file"])
@@ -163,17 +170,39 @@
     def setup_test(self):
         for ad in self.android_devices:
             refresh_droid_config(self.log, ad)
+
+        if getattr(self, "diag_logger", None):
+            for logger in self.diag_logger:
+                self.log.info("Starting a diagnostic session {}".format(
+                    logger))
+                self.logger_sessions.append((logger, logger.start()))
+
         return ensure_phones_default_state(self.log, self.android_devices)
 
     def teardown_test(self):
+        for (logger, session) in self.logger_sessions:
+            self.log.info("Resetting a diagnostic session {},{}".format(
+                logger, session))
+            logger.reset()
+        self.logger_sessions = []
         return True
 
     def on_exception(self, test_name, begin_time):
+        self._pull_diag_logs(test_name, begin_time)
         return self._take_bug_report(test_name, begin_time)
 
     def on_fail(self, test_name, begin_time):
+        self._pull_diag_logs(test_name, begin_time)
         return self._take_bug_report(test_name, begin_time)
 
+    def _pull_diag_logs(self, test_name, begin_time):
+        for (logger, session) in self.logger_sessions:
+            self.log.info("Pulling diagnostic session {}".format(logger))
+            logger.stop(session)
+            diag_path = os.path.join(self.log_path, begin_time)
+            utils.create_dir(diag_path)
+            logger.pull(session, diag_path)
+
     def _take_bug_report(self, test_name, begin_time):
         if "no_bug_report_on_fail" in self.user_params:
             return
@@ -184,8 +213,9 @@
             try:
                 ad.adb.wait_for_device()
                 ad.take_bug_report(test_name, begin_time)
-                tombstone_path = os.path.join(ad.log_path, "BugReports",
-                        "{},{}".format(begin_time, ad.serial).replace(' ','_'))
+                tombstone_path = os.path.join(
+                    ad.log_path, "BugReports",
+                    "{},{}".format(begin_time, ad.serial).replace(' ', '_'))
                 utils.create_dir(tombstone_path)
                 ad.adb.pull('/data/tombstones/', tombstone_path)
             except:
diff --git a/acts/framework/tests/acts_test_runner_test.py b/acts/framework/tests/acts_test_runner_test.py
index f9c7b8e..40ff6fe 100755
--- a/acts/framework/tests/acts_test_runner_test.py
+++ b/acts/framework/tests/acts_test_runner_test.py
@@ -15,6 +15,8 @@
 #   limitations under the License.
 
 
+
+import mock
 import shutil
 import tempfile
 import unittest
@@ -33,7 +35,7 @@
     def setUp(self):
         self.tmp_dir = tempfile.mkdtemp()
         self.base_mock_test_config = {
-            "testbed":{
+            "testbed": {
                 "name": "SampleTestBed",
             },
             "logpath": self.tmp_dir,
@@ -54,6 +56,12 @@
                                      "No corresponding config found for"):
             tr.register_controller(mock_controller)
 
+    def test_register_optional_controller_no_config(self):
+        tr = test_runner.TestRunner(self.base_mock_test_config,
+                                    self.mock_run_list)
+        self.assertIsNone(tr.register_controller(mock_controller,
+                                                 required=False))
+
     def test_register_controller_third_party_dup_register(self):
         """Verifies correctness of registration, internal tally of controllers
         objects, and the right error happen when a controller module is
@@ -75,6 +83,21 @@
         with self.assertRaisesRegexp(signals.ControllerError, expected_msg):
             tr.register_controller(mock_controller)
 
+    def test_register_optional_controller_third_party_dup_register(self):
+        """Verifies correctness of registration, internal tally of controllers
+        objects, and the right error happen when an optional controller module
+        is registered twice.
+        """
+        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(mock_test_config, self.mock_run_list)
+        tr.register_controller(mock_controller, required=False)
+        expected_msg = "Controller module .* has already been registered."
+        with self.assertRaisesRegexp(signals.ControllerError, expected_msg):
+            tr.register_controller(mock_controller, required=False)
+
     def test_register_controller_builtin_dup_register(self):
         """Same as test_register_controller_third_party_dup_register, except
         this is for a builtin controller module.
@@ -83,12 +106,12 @@
         tb_key = keys.Config.key_testbed.value
         mock_ctrlr_config_name = mock_controller.ACTS_CONTROLLER_CONFIG_NAME
         mock_ref_name = "haha"
-        setattr(mock_controller,
-                "ACTS_CONTROLLER_REFERENCE_NAME",
+        setattr(mock_controller, "ACTS_CONTROLLER_REFERENCE_NAME",
                 mock_ref_name)
         try:
             mock_ctrlr_ref_name = mock_controller.ACTS_CONTROLLER_REFERENCE_NAME
-            mock_test_config[tb_key][mock_ctrlr_config_name] = ["magic1", "magic2"]
+            mock_test_config[tb_key][mock_ctrlr_config_name] = ["magic1",
+                                                                "magic2"]
             tr = test_runner.TestRunner(mock_test_config, self.mock_run_list)
             tr.register_controller(mock_controller)
             self.assertTrue(mock_ref_name in tr.test_run_info)
@@ -98,7 +121,8 @@
             self.assertEqual(mock_ctrlrs[1].magic, "magic2")
             self.assertTrue(tr.controller_destructors[mock_ctrlr_ref_name])
             expected_msg = "Controller module .* has already been registered."
-            with self.assertRaisesRegexp(signals.ControllerError, expected_msg):
+            with self.assertRaisesRegexp(signals.ControllerError,
+                                         expected_msg):
                 tr.register_controller(mock_controller)
         finally:
             delattr(mock_controller, "ACTS_CONTROLLER_REFERENCE_NAME")
@@ -122,10 +146,12 @@
         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
-        my_config = [{"serial": "xxxx", "magic": "Magic1"},
-                     {"serial": "xxxx", "magic": "Magic2"}]
+        my_config = [{"serial": "xxxx",
+                      "magic": "Magic1"}, {"serial": "xxxx",
+                                           "magic": "Magic2"}]
         mock_test_config[tb_key][mock_ctrlr_config_name] = my_config
-        tr = test_runner.TestRunner(mock_test_config, [('IntegrationTest', None)])
+        tr = test_runner.TestRunner(mock_test_config, [('IntegrationTest',
+                                                        None)])
         tr.run()
         self.assertFalse(tr.controller_registry)
         self.assertFalse(tr.controller_destructors)
@@ -139,6 +165,41 @@
         self.assertEqual(results["Executed"], 2)
         self.assertEqual(results["Passed"], 2)
 
+    @mock.patch('acts.controllers.adb.AdbProxy',
+                return_value=acts_android_device_test.MockAdbProxy(1))
+    @mock.patch('acts.controllers.android_device.list_adb_devices',
+                return_value=["1"])
+    @mock.patch('acts.controllers.android_device.get_all_instances',
+                return_value=acts_android_device_test.get_mock_ads(1))
+    def test_run_two_test_classes(self, mock_adb, mock_list_adb, mock_get_all):
+        """Verifies that runing more than one test class in one test run works
+        proerly.
+
+        This requires using a built-in controller module. Using AndroidDevice
+        module since it has all the mocks needed already.
+        """
+        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
+        my_config = [{"serial": "xxxx", "magic": "Magic1"},
+                     {"serial": "xxxx", "magic": "Magic2"}]
+        mock_test_config[tb_key][mock_ctrlr_config_name] = my_config
+        mock_test_config[tb_key]["AndroidDevice"] = [
+            {"serial": "1",
+             "skip_sl4a": True}
+        ]
+        tr = test_runner.TestRunner(mock_test_config,
+                                    [('IntegrationTest', None),
+                                     ('IntegrationTest', None)])
+        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)
 
@@ -148,7 +209,8 @@
             mock_controller.ACTS_CONTROLLER_CONFIG_NAME = None
             msg = "Controller interface .* in .* cannot be null."
             with self.assertRaisesRegexp(signals.ControllerError, msg):
-                test_runner.TestRunner.verify_controller_module(mock_controller)
+                test_runner.TestRunner.verify_controller_module(
+                    mock_controller)
         finally:
             mock_controller.ACTS_CONTROLLER_CONFIG_NAME = tmp
 
@@ -158,10 +220,11 @@
             delattr(mock_controller, "ACTS_CONTROLLER_CONFIG_NAME")
             msg = "Module .* missing required controller module attribute"
             with self.assertRaisesRegexp(signals.ControllerError, msg):
-                test_runner.TestRunner.verify_controller_module(mock_controller)
+                test_runner.TestRunner.verify_controller_module(
+                    mock_controller)
         finally:
             setattr(mock_controller, "ACTS_CONTROLLER_CONFIG_NAME", tmp)
 
 
 if __name__ == "__main__":
-   unittest.main()
\ No newline at end of file
+    unittest.main()