blob: 57cc9d8f1041e3175ad1de1213ffdde5892e2da7 [file] [log] [blame]
#!/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)