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