• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2018 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import logging
18import traceback
19
20from acts.context import get_context_for_event
21from acts.event import event_bus
22from acts.event import subscription_bundle
23from acts.event.decorators import subscribe
24from acts.event.event import TestCaseBeginEvent
25from acts.event.event import TestCaseEndEvent
26from acts.event.event import TestClassBeginEvent
27from acts.event.event import TestClassEndEvent
28from acts.metrics.core import ProtoMetricPublisher
29
30
31class MetricLogger(object):
32    """The base class for a logger object that records metric data.
33
34    This is the central component to the ACTS metrics framework. Users should
35    extend this class with the functionality needed to log their specific
36    metric.
37
38    The public API for this class contains only a start() and end() method,
39    intended to bookend the logging process for a particular metric. The timing
40    of when those methods are called depends on how the logger is subscribed.
41    The canonical use for this class is to use the class methods to
42    automatically subscribe the logger to certain test events.
43
44    Example:
45        def MyTestClass(BaseTestClass):
46            def __init__(self):
47                self.my_metric_logger = MyMetricLogger.for_test_case()
48
49    This would subscribe the logger to test case begin and end events. For each
50    test case in MyTestClass, a new MyMetricLogger instance will be created,
51    and start() and end() will be called at the before and after the test case,
52    respectively.
53
54    The self.my_metric_logger object will be a proxy object that points to
55    whatever MyMetricLogger is being used in the current context. This means
56    that test code can access this logger without worrying about managing
57    separate instances for each test case.
58
59    Example:
60         def MyMetricLogger(MetricLogger):
61             def store_data(self, data):
62                 # store data
63
64             def end(self, event):
65                 # write out stored data
66
67         def MyTestClass(BaseTestClass):
68             def __init__(self):
69                 self.my_metric_logger = MyMetricLogger.for_test_case()
70
71             def test_case_a(self):
72                 # do some test stuff
73                 self.my_metric_logger.store_data(data)
74                 # more test stuff
75
76             def test_case_b(self):
77                 # do some test stuff
78                 self.my_metric_logger.store_data(data)
79                 # more test stuff
80
81    In the above example, test_case_a and test_case_b both record data to
82    self.my_metric_logger. However, because the MyMetricLogger was subscribed
83    to test cases, the proxy object would point to a new instance for each
84    test case.
85
86
87    Attributes:
88
89        context: A MetricContext object describing metadata about how the
90                 logger is being run. For example, on a test case metric
91                 logger, the context should contain the test class and test
92                 case name.
93        publisher: A MetricPublisher object that provides an API for publishing
94                   metric data, typically to a file.
95    """
96
97    @classmethod
98    def for_test_case(cls, *args, **kwargs):
99        """Registers the logger class for each test case.
100
101        Creates a proxy logger that will instantiate this method's logger class
102        for each test case. Any arguments passed to this method will be
103        forwarded to the underlying MetricLogger construction by the proxy.
104
105        Returns:
106            The proxy logger.
107        """
108        return TestCaseLoggerProxy(cls, args, kwargs)
109
110    @classmethod
111    def for_test_class(cls, *args, **kwargs):
112        """Registers the logger class for each test class.
113
114        Creates a proxy logger that will instantiate this method's logger class
115        for each test class. Any arguments passed to this method will be
116        forwarded to the underlying MetricLogger construction by the proxy.
117
118        Returns:
119            The proxy logger.
120        """
121        return TestClassLoggerProxy(cls, args, kwargs)
122
123    def __init__(self, context=None, publisher=None, event=None):
124        """Initializes a MetricLogger.
125
126        If context or publisher are passed, they are set as attributes to the
127        logger. Otherwise, they will be initialized later by an event.
128
129        If event is passed, it is used immediately to populate the context and
130        publisher (unless they are explicitly passed as well).
131
132        Args:
133             context: the MetricContext in which this logger has been created
134             publisher: the MetricPublisher to use
135             event: an event triggering the creation of this logger, used to
136                    populate context and publisher
137        """
138        self.context = context
139        self.publisher = publisher
140        if event:
141            self._init_for_event(event)
142
143    def start(self, event):
144        """Start the logging process.
145
146        Args:
147            event: the event that is triggering this start
148        """
149
150    def end(self, event):
151        """End the logging process.
152
153        Args:
154            event: the event that is triggering this start
155        """
156
157    def _init_for_event(self, event):
158        """Populate unset attributes with default values."""
159        if not self.context:
160            self.context = self._get_default_context(event)
161        if not self.publisher:
162            self.publisher = self._get_default_publisher(event)
163
164    def _get_default_context(self, event):
165        """Get the default context for the given event."""
166        return get_context_for_event(event)
167
168    def _get_default_publisher(self, _):
169        """Get the default publisher for the given event."""
170        return ProtoMetricPublisher(self.context)
171
172
173class LoggerProxy(object):
174    """A proxy object to manage and forward calls to an underlying logger.
175
176    The proxy is intended to respond to certain framework events and
177    create/discard the underlying logger as appropriate. It should be treated
178    as an abstract class, with subclasses specifying what actions to be taken
179    based on certain events.
180
181    There is no global registry of proxies, so implementations should be
182    inherently self-managing. In particular, they should unregister any
183    subscriptions they have once they are finished.
184
185    Attributes:
186        _logger_cls: the class object for the underlying logger
187        _logger_args: the position args for the logger constructor
188        _logger_kwargs: the keyword args for the logger constructor. Note that
189                        the triggering even is always passed as a keyword arg.
190        __initialized: Whether the class attributes have been initialized. Used
191                      by __getattr__ and __setattr__ to prevent infinite
192                      recursion.
193    """
194
195    def __init__(self, logger_cls, logger_args, logger_kwargs):
196        """Constructs a proxy for the given logger class.
197
198        The logger class will later be constructed using the triggering event,
199        along with the args and kwargs passed here.
200
201        This will also register any methods decorated with event subscriptions
202        that may have been defined in a subclass. It is the subclass's
203        responsibility to unregister them once the logger is finished.
204
205        Args:
206            logger_cls: The class object for the underlying logger.
207            logger_args: The position args for the logger constructor.
208            logger_kwargs: The keyword args for the logger constructor.
209        """
210        self._logger_cls = logger_cls
211        self._logger_args = logger_args
212        self._logger_kwargs = logger_kwargs
213        self._logger = None
214        bundle = subscription_bundle.create_from_instance(self)
215        bundle.register()
216        self.__initialized = True
217
218    def _setup_proxy(self, event):
219        """Creates and starts the underlying logger based on the event.
220
221        Args:
222            event: The event that triggered this logger.
223        """
224        self._logger = self._logger_cls(event=event,
225                                        *self._logger_args,
226                                        **self._logger_kwargs)
227        self._logger.start(event)
228
229    def _teardown_proxy(self, event):
230        """Ends and removes the underlying logger.
231
232        If the underlying logger does not exist, no action is taken. We avoid
233        raising an error in this case with the implicit assumption that
234        _setup_proxy would have raised one already if logger creation failed.
235
236        Args:
237            event: The triggering event.
238        """
239
240        # Here, we surround the logger's end() function with a catch-all try
241        # statement. This prevents logging failures from crashing the test class
242        # before all test cases have completed. Note that this has not been
243        # added to _setup_proxy. Failure in teardown is more likely due to
244        # failure to receive metric data (e.g., was unable to be gathered), or
245        # failure to log to the correct proto (e.g., incorrect format).
246
247        # noinspection PyBroadException
248        try:
249            if self._logger:
250                self._logger.end(event)
251        except Exception:
252            logging.error('Unable to properly close logger %s.' %
253                          self._logger.__class__.__name__)
254            logging.debug("\n%s" % traceback.format_exc())
255        finally:
256            self._logger = None
257
258    def __getattr__(self, attr):
259        """Forwards attribute access to the underlying logger.
260
261        Args:
262            attr: The name of the attribute to retrieve.
263
264        Returns:
265            The attribute with name attr from the underlying logger.
266
267        Throws:
268            ValueError: If the underlying logger is not set.
269        """
270        logger = getattr(self, '_logger', None)
271        if not logger:
272            raise ValueError('Underlying logger is not initialized.')
273        return getattr(logger, attr)
274
275    def __setattr__(self, attr, value):
276        """Forwards attribute access to the underlying logger.
277
278        Args:
279            attr: The name of the attribute to set.
280            value: The value of the attribute to set.
281
282        Throws:
283            ValueError: If the underlying logger is not set.
284        """
285        if not self.__dict__.get('_LoggerProxy__initialized', False):
286            return super().__setattr__(attr, value)
287        if attr == '_logger':
288            return super().__setattr__(attr, value)
289        logger = getattr(self, '_logger', None)
290        if not logger:
291            raise ValueError('Underlying logger is not initialized.')
292        return setattr(logger, attr, value)
293
294
295class TestCaseLoggerProxy(LoggerProxy):
296    """A LoggerProxy implementation to subscribe to test case events.
297
298    The underlying logger will be created and destroyed on test case begin and
299    end events respectively. The proxy will unregister itself from the event
300    bus at the end of the test class.
301    """
302
303    def __init__(self, logger_cls, logger_args, logger_kwargs):
304        super().__init__(logger_cls, logger_args, logger_kwargs)
305
306    @subscribe(TestCaseBeginEvent)
307    def __on_test_case_begin(self, event):
308        """Sets up the proxy for a test case."""
309        self._setup_proxy(event)
310
311    @subscribe(TestCaseEndEvent)
312    def __on_test_case_end(self, event):
313        """Tears down the proxy for a test case."""
314        self._teardown_proxy(event)
315
316    @subscribe(TestClassEndEvent)
317    def __on_test_class_end(self, event):
318        """Cleans up the subscriptions at the end of a class."""
319        event_bus.unregister(self.__on_test_case_begin)
320        event_bus.unregister(self.__on_test_case_end)
321        event_bus.unregister(self.__on_test_class_end)
322
323
324class TestClassLoggerProxy(LoggerProxy):
325    """A LoggerProxy implementation to subscribe to test class events.
326
327    The underlying logger will be created and destroyed on test class begin and
328    end events respectively. The proxy will also unregister itself from the
329    event bus at the end of the test class.
330    """
331
332    def __init__(self, logger_cls, logger_args, logger_kwargs):
333        super().__init__(logger_cls, logger_args, logger_kwargs)
334
335    @subscribe(TestClassBeginEvent)
336    def __on_test_class_begin(self, event):
337        """Sets up the proxy for a test class."""
338        self._setup_proxy(event)
339
340    @subscribe(TestClassEndEvent)
341    def __on_test_class_end(self, event):
342        """Tears down the proxy for a test class and removes subscriptions."""
343        self._teardown_proxy(event)
344        event_bus.unregister(self.__on_test_class_begin)
345        event_bus.unregister(self.__on_test_class_end)
346