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()