From 6afd0a3074c3e91611e299113c570b8681727ef4 Mon Sep 17 00:00:00 2001 From: Nathan Harold Date: Mon, 29 Feb 2016 13:11:46 -0800 Subject: Create a Generic DiagLogger Interface for Collecting Radio Logs This controller defines an interface that can be used by on-device logging mechanisms that capture logs for offline analysis. The semantics support basic start/stop, pulling of logs, and resetting the logging for subsequent tests. Because the mechanisms for this are typically proprietary, vendors must provide their own instantiations of the DiagLogger in order to collect logs. Bug: 27299974 Change-Id: Ie2ff6ce7e3fcdbdb78f3bea9f6cd87629d6adc73 --- acts/framework/acts/controllers/diag_logger.py | 159 +++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 acts/framework/acts/controllers/diag_logger.py diff --git a/acts/framework/acts/controllers/diag_logger.py b/acts/framework/acts/controllers/diag_logger.py new file mode 100644 index 0000000000..96f1f0331f --- /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 -- cgit v1.2.3 From f631c4a41309c22da961857ce0fd482e04127f24 Mon Sep 17 00:00:00 2001 From: Nathan Harold Date: Mon, 16 May 2016 16:32:54 -0700 Subject: Add DiagLogger Controller Support in TelephonyBaseTest Bug: 27299974 Change-Id: I332f3eac1eaba50bb1de7ecf0ce8f6f6e2a90ee2 --- .../acts/test_utils/tel/TelephonyBaseTest.py | 34 ++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py b/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py index c6b060cf77..3187ca75ee 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 @@ from acts.utils import force_airplane_mode 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 @@ class TelephonyBaseTest(BaseTestClass): 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 @@ class TelephonyBaseTest(BaseTestClass): 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 @@ class TelephonyBaseTest(BaseTestClass): 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: -- cgit v1.2.3