• 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        pass
150
151    def end(self, event):
152        """End the logging process.
153
154        Args:
155            event: the event that is triggering this start
156        """
157        pass
158
159    def _init_for_event(self, event):
160        """Populate unset attributes with default values."""
161        if not self.context:
162            self.context = self._get_default_context(event)
163        if not self.publisher:
164            self.publisher = self._get_default_publisher(event)
165
166    def _get_default_context(self, event):
167        """Get the default context for the given event."""
168        return get_context_for_event(event)
169
170    def _get_default_publisher(self, _):
171        """Get the default publisher for the given event."""
172        return ProtoMetricPublisher(self.context)
173
174
175class LoggerProxy(object):
176    """A proxy object to manage and forward calls to an underlying logger.
177
178    The proxy is intended to respond to certain framework events and
179    create/discard the underlying logger as appropriate. It should be treated
180    as an abstract class, with subclasses specifying what actions to be taken
181    based on certain events.
182
183    There is no global registry of proxies, so implementations should be
184    inherently self-managing. In particular, they should unregister any
185    subscriptions they have once they are finished.
186
187    Attributes:
188        _logger_cls: the class object for the underlying logger
189        _logger_args: the position args for the logger constructor
190        _logger_kwargs: the keyword args for the logger constructor. Note that
191                        the triggering even is always passed as a keyword arg.
192        __initialized: Whether the class attributes have been initialized. Used
193                      by __getattr__ and __setattr__ to prevent infinite
194                      recursion.
195    """
196
197    def __init__(self, logger_cls, logger_args, logger_kwargs):
198        """Constructs a proxy for the given logger class.
199
200        The logger class will later be constructed using the triggering event,
201        along with the args and kwargs passed here.
202
203        This will also register any methods decorated with event subscriptions
204        that may have been defined in a subclass. It is the subclass's
205        responsibility to unregister them once the logger is finished.
206
207        Args:
208            logger_cls: The class object for the underlying logger.
209            logger_args: The position args for the logger constructor.
210            logger_kwargs: The keyword args for the logger constructor.
211        """
212        self._logger_cls = logger_cls
213        self._logger_args = logger_args
214        self._logger_kwargs = logger_kwargs
215        self._logger = None
216        bundle = subscription_bundle.create_from_instance(self)
217        bundle.register()
218        self.__initialized = True
219
220    def _setup_proxy(self, event):
221        """Creates and starts the underlying logger based on the event.
222
223        Args:
224            event: The event that triggered this logger.
225        """
226        self._logger = self._logger_cls(event=event, *self._logger_args,
227                                        **self._logger_kwargs)
228        self._logger.start(event)
229
230    def _teardown_proxy(self, event):
231        """Ends and removes the underlying logger.
232
233        If the underlying logger does not exist, no action is taken. We avoid
234        raising an error in this case with the implicit assumption that
235        _setup_proxy would have raised one already if logger creation failed.
236
237        Args:
238            event: The triggering event.
239        """
240
241        # Here, we surround the logger's end() function with a catch-all try
242        # statement. This prevents logging failures from crashing the test class
243        # before all test cases have completed. Note that this has not been
244        # added to _setup_proxy. Failure in teardown is more likely due to
245        # failure to receive metric data (e.g., was unable to be gathered), or
246        # failure to log to the correct proto (e.g., incorrect format).
247
248        # noinspection PyBroadException
249        try:
250            if self._logger:
251                self._logger.end(event)
252        except Exception:
253            logging.error('Unable to properly close logger %s.' %
254                          self._logger.__class__.__name__)
255            logging.debug("\n%s" % traceback.format_exc())
256        finally:
257            self._logger = None
258
259    def __getattr__(self, attr):
260        """Forwards attribute access to the underlying logger.
261
262        Args:
263            attr: The name of the attribute to retrieve.
264
265        Returns:
266            The attribute with name attr from the underlying logger.
267
268        Throws:
269            ValueError: If the underlying logger is not set.
270        """
271        logger = getattr(self, '_logger', None)
272        if not logger:
273            raise ValueError('Underlying logger is not initialized.')
274        return getattr(logger, attr)
275
276    def __setattr__(self, attr, value):
277        """Forwards attribute access to the underlying logger.
278
279        Args:
280            attr: The name of the attribute to set.
281            value: The value of the attribute to set.
282
283        Throws:
284            ValueError: If the underlying logger is not set.
285        """
286        if not self.__dict__.get('_LoggerProxy__initialized', False):
287            return super().__setattr__(attr, value)
288        if attr == '_logger':
289            return super().__setattr__(attr, value)
290        logger = getattr(self, '_logger', None)
291        if not logger:
292            raise ValueError('Underlying logger is not initialized.')
293        return setattr(logger, attr, value)
294
295
296class TestCaseLoggerProxy(LoggerProxy):
297    """A LoggerProxy implementation to subscribe to test case events.
298
299    The underlying logger will be created and destroyed on test case begin and
300    end events respectively. The proxy will unregister itself from the event
301    bus at the end of the test class.
302    """
303
304    def __init__(self, logger_cls, logger_args, logger_kwargs):
305        super().__init__(logger_cls, logger_args, logger_kwargs)
306
307    @subscribe(TestCaseBeginEvent)
308    def __on_test_case_begin(self, event):
309        """Sets up the proxy for a test case."""
310        self._setup_proxy(event)
311
312    @subscribe(TestCaseEndEvent)
313    def __on_test_case_end(self, event):
314        """Tears down the proxy for a test case."""
315        self._teardown_proxy(event)
316
317    @subscribe(TestClassEndEvent)
318    def __on_test_class_end(self, event):
319        """Cleans up the subscriptions at the end of a class."""
320        event_bus.unregister(self.__on_test_case_begin)
321        event_bus.unregister(self.__on_test_case_end)
322        event_bus.unregister(self.__on_test_class_end)
323
324
325class TestClassLoggerProxy(LoggerProxy):
326    """A LoggerProxy implementation to subscribe to test class events.
327
328    The underlying logger will be created and destroyed on test class begin and
329    end events respectively. The proxy will also unregister itself from the
330    event bus at the end of the test class.
331    """
332
333    def __init__(self, logger_cls, logger_args, logger_kwargs):
334        super().__init__(logger_cls, logger_args, logger_kwargs)
335
336    @subscribe(TestClassBeginEvent)
337    def __on_test_class_begin(self, event):
338        """Sets up the proxy for a test class."""
339        self._setup_proxy(event)
340
341    @subscribe(TestClassEndEvent)
342    def __on_test_class_end(self, event):
343        """Tears down the proxy for a test class and removes subscriptions."""
344        self._teardown_proxy(event)
345        event_bus.unregister(self.__on_test_class_begin)
346        event_bus.unregister(self.__on_test_class_end)
347