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