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