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