#!/usr/bin/env python3 # # Copyright 2018 - 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 logging import traceback from acts.context import get_context_for_event from acts.event import event_bus from acts.event import subscription_bundle from acts.event.decorators import subscribe from acts.event.event import TestCaseBeginEvent from acts.event.event import TestCaseEndEvent from acts.event.event import TestClassBeginEvent from acts.event.event import TestClassEndEvent from acts.metrics.core import ProtoMetricPublisher class MetricLogger(object): """The base class for a logger object that records metric data. This is the central component to the ACTS metrics framework. Users should extend this class with the functionality needed to log their specific metric. The public API for this class contains only a start() and end() method, intended to bookend the logging process for a particular metric. The timing of when those methods are called depends on how the logger is subscribed. The canonical use for this class is to use the class methods to automatically subscribe the logger to certain test events. Example: def MyTestClass(BaseTestClass): def __init__(self): self.my_metric_logger = MyMetricLogger.for_test_case() This would subscribe the logger to test case begin and end events. For each test case in MyTestClass, a new MyMetricLogger instance will be created, and start() and end() will be called at the before and after the test case, respectively. The self.my_metric_logger object will be a proxy object that points to whatever MyMetricLogger is being used in the current context. This means that test code can access this logger without worrying about managing separate instances for each test case. Example: def MyMetricLogger(MetricLogger): def store_data(self, data): # store data def end(self, event): # write out stored data def MyTestClass(BaseTestClass): def __init__(self): self.my_metric_logger = MyMetricLogger.for_test_case() def test_case_a(self): # do some test stuff self.my_metric_logger.store_data(data) # more test stuff def test_case_b(self): # do some test stuff self.my_metric_logger.store_data(data) # more test stuff In the above example, test_case_a and test_case_b both record data to self.my_metric_logger. However, because the MyMetricLogger was subscribed to test cases, the proxy object would point to a new instance for each test case. Attributes: context: A MetricContext object describing metadata about how the logger is being run. For example, on a test case metric logger, the context should contain the test class and test case name. publisher: A MetricPublisher object that provides an API for publishing metric data, typically to a file. """ @classmethod def for_test_case(cls, *args, **kwargs): """Registers the logger class for each test case. Creates a proxy logger that will instantiate this method's logger class for each test case. Any arguments passed to this method will be forwarded to the underlying MetricLogger construction by the proxy. Returns: The proxy logger. """ return TestCaseLoggerProxy(cls, args, kwargs) @classmethod def for_test_class(cls, *args, **kwargs): """Registers the logger class for each test class. Creates a proxy logger that will instantiate this method's logger class for each test class. Any arguments passed to this method will be forwarded to the underlying MetricLogger construction by the proxy. Returns: The proxy logger. """ return TestClassLoggerProxy(cls, args, kwargs) def __init__(self, context=None, publisher=None, event=None): """Initializes a MetricLogger. If context or publisher are passed, they are set as attributes to the logger. Otherwise, they will be initialized later by an event. If event is passed, it is used immediately to populate the context and publisher (unless they are explicitly passed as well). Args: context: the MetricContext in which this logger has been created publisher: the MetricPublisher to use event: an event triggering the creation of this logger, used to populate context and publisher """ self.context = context self.publisher = publisher if event: self._init_for_event(event) def start(self, event): """Start the logging process. Args: event: the event that is triggering this start """ pass def end(self, event): """End the logging process. Args: event: the event that is triggering this start """ pass def _init_for_event(self, event): """Populate unset attributes with default values.""" if not self.context: self.context = self._get_default_context(event) if not self.publisher: self.publisher = self._get_default_publisher(event) def _get_default_context(self, event): """Get the default context for the given event.""" return get_context_for_event(event) def _get_default_publisher(self, _): """Get the default publisher for the given event.""" return ProtoMetricPublisher(self.context) class LoggerProxy(object): """A proxy object to manage and forward calls to an underlying logger. The proxy is intended to respond to certain framework events and create/discard the underlying logger as appropriate. It should be treated as an abstract class, with subclasses specifying what actions to be taken based on certain events. There is no global registry of proxies, so implementations should be inherently self-managing. In particular, they should unregister any subscriptions they have once they are finished. Attributes: _logger_cls: the class object for the underlying logger _logger_args: the position args for the logger constructor _logger_kwargs: the keyword args for the logger constructor. Note that the triggering even is always passed as a keyword arg. __initialized: Whether the class attributes have been initialized. Used by __getattr__ and __setattr__ to prevent infinite recursion. """ def __init__(self, logger_cls, logger_args, logger_kwargs): """Constructs a proxy for the given logger class. The logger class will later be constructed using the triggering event, along with the args and kwargs passed here. This will also register any methods decorated with event subscriptions that may have been defined in a subclass. It is the subclass's responsibility to unregister them once the logger is finished. Args: logger_cls: The class object for the underlying logger. logger_args: The position args for the logger constructor. logger_kwargs: The keyword args for the logger constructor. """ self._logger_cls = logger_cls self._logger_args = logger_args self._logger_kwargs = logger_kwargs self._logger = None bundle = subscription_bundle.create_from_instance(self) bundle.register() self.__initialized = True def _setup_proxy(self, event): """Creates and starts the underlying logger based on the event. Args: event: The event that triggered this logger. """ self._logger = self._logger_cls(event=event, *self._logger_args, **self._logger_kwargs) self._logger.start(event) def _teardown_proxy(self, event): """Ends and removes the underlying logger. If the underlying logger does not exist, no action is taken. We avoid raising an error in this case with the implicit assumption that _setup_proxy would have raised one already if logger creation failed. Args: event: The triggering event. """ # Here, we surround the logger's end() function with a catch-all try # statement. This prevents logging failures from crashing the test class # before all test cases have completed. Note that this has not been # added to _setup_proxy. Failure in teardown is more likely due to # failure to receive metric data (e.g., was unable to be gathered), or # failure to log to the correct proto (e.g., incorrect format). # noinspection PyBroadException try: if self._logger: self._logger.end(event) except Exception: logging.error('Unable to properly close logger %s.' % self._logger.__class__.__name__) logging.debug("\n%s" % traceback.format_exc()) finally: self._logger = None def __getattr__(self, attr): """Forwards attribute access to the underlying logger. Args: attr: The name of the attribute to retrieve. Returns: The attribute with name attr from the underlying logger. Throws: ValueError: If the underlying logger is not set. """ logger = getattr(self, '_logger', None) if not logger: raise ValueError('Underlying logger is not initialized.') return getattr(logger, attr) def __setattr__(self, attr, value): """Forwards attribute access to the underlying logger. Args: attr: The name of the attribute to set. value: The value of the attribute to set. Throws: ValueError: If the underlying logger is not set. """ if not self.__dict__.get('_LoggerProxy__initialized', False): return super().__setattr__(attr, value) if attr == '_logger': return super().__setattr__(attr, value) logger = getattr(self, '_logger', None) if not logger: raise ValueError('Underlying logger is not initialized.') return setattr(logger, attr, value) class TestCaseLoggerProxy(LoggerProxy): """A LoggerProxy implementation to subscribe to test case events. The underlying logger will be created and destroyed on test case begin and end events respectively. The proxy will unregister itself from the event bus at the end of the test class. """ def __init__(self, logger_cls, logger_args, logger_kwargs): super().__init__(logger_cls, logger_args, logger_kwargs) @subscribe(TestCaseBeginEvent) def __on_test_case_begin(self, event): """Sets up the proxy for a test case.""" self._setup_proxy(event) @subscribe(TestCaseEndEvent) def __on_test_case_end(self, event): """Tears down the proxy for a test case.""" self._teardown_proxy(event) @subscribe(TestClassEndEvent) def __on_test_class_end(self, event): """Cleans up the subscriptions at the end of a class.""" event_bus.unregister(self.__on_test_case_begin) event_bus.unregister(self.__on_test_case_end) event_bus.unregister(self.__on_test_class_end) class TestClassLoggerProxy(LoggerProxy): """A LoggerProxy implementation to subscribe to test class events. The underlying logger will be created and destroyed on test class begin and end events respectively. The proxy will also unregister itself from the event bus at the end of the test class. """ def __init__(self, logger_cls, logger_args, logger_kwargs): super().__init__(logger_cls, logger_args, logger_kwargs) @subscribe(TestClassBeginEvent) def __on_test_class_begin(self, event): """Sets up the proxy for a test class.""" self._setup_proxy(event) @subscribe(TestClassEndEvent) def __on_test_class_end(self, event): """Tears down the proxy for a test class and removes subscriptions.""" self._teardown_proxy(event) event_bus.unregister(self.__on_test_class_begin) event_bus.unregister(self.__on_test_class_end)