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. 16import logging 17import os 18import sys 19from logging import FileHandler 20from logging import Handler 21from logging import StreamHandler 22from logging.handlers import RotatingFileHandler 23 24from acts import context 25from acts.context import ContextLevel 26from acts.event import event_bus 27from acts.event.decorators import subscribe_static 28 29 30# yapf: disable 31class LogStyles: 32 NONE = 0x00 33 LOG_DEBUG = 0x01 34 LOG_INFO = 0x02 35 LOG_WARNING = 0x04 36 LOG_ERROR = 0x08 37 LOG_CRITICAL = 0x10 38 39 DEFAULT_LEVELS = LOG_DEBUG + LOG_INFO + LOG_ERROR 40 ALL_LEVELS = LOG_DEBUG + LOG_INFO + LOG_WARNING + LOG_ERROR + LOG_CRITICAL 41 42 MONOLITH_LOG = 0x0100 43 TESTCLASS_LOG = 0x0200 44 TESTCASE_LOG = 0x0400 45 TO_STDOUT = 0x0800 46 TO_ACTS_LOG = 0x1000 47 ROTATE_LOGS = 0x2000 48 49 ALL_FILE_LOGS = MONOLITH_LOG + TESTCLASS_LOG + TESTCASE_LOG 50 51 LEVEL_NAMES = { 52 LOG_DEBUG: 'debug', 53 LOG_INFO: 'info', 54 LOG_WARNING: 'warning', 55 LOG_ERROR: 'error', 56 LOG_CRITICAL: 'critical', 57 } 58 59 LOG_LEVELS = [ 60 LOG_DEBUG, 61 LOG_INFO, 62 LOG_WARNING, 63 LOG_ERROR, 64 LOG_CRITICAL, 65 ] 66 67 LOG_LOCATIONS = [ 68 TO_STDOUT, 69 TO_ACTS_LOG, 70 MONOLITH_LOG, 71 TESTCLASS_LOG, 72 TESTCASE_LOG 73 ] 74 75 LEVEL_TO_NO = { 76 LOG_DEBUG: logging.DEBUG, 77 LOG_INFO: logging.INFO, 78 LOG_WARNING: logging.WARNING, 79 LOG_ERROR: logging.ERROR, 80 LOG_CRITICAL: logging.CRITICAL, 81 } 82 83 LOCATION_TO_CONTEXT_LEVEL = { 84 MONOLITH_LOG: ContextLevel.ROOT, 85 TESTCLASS_LOG: ContextLevel.TESTCLASS, 86 TESTCASE_LOG: ContextLevel.TESTCASE 87 } 88# yapf: enable 89 90 91_log_streams = dict() 92_null_handler = logging.NullHandler() 93 94 95@subscribe_static(context.NewContextEvent) 96def _update_handlers(event): 97 for log_stream in _log_streams.values(): 98 log_stream.update_handlers(event) 99 100 101event_bus.register_subscription(_update_handlers.subscription) 102 103 104def create_logger(name, log_name=None, base_path='', subcontext='', 105 log_styles=LogStyles.NONE, stream_format=None, 106 file_format=None): 107 """Creates a Python Logger object with the given attributes. 108 109 Creation through this method will automatically manage the logger in the 110 background for test-related events, such as TestCaseBegin and TestCaseEnd 111 Events. 112 113 Args: 114 name: The name of the LogStream. Used as the file name prefix. 115 log_name: The name of the underlying logger. Use LogStream name as 116 default. 117 base_path: The base path used by the logger. 118 subcontext: Location of logs relative to the test context path. 119 log_styles: An integer or array of integers that are the sum of 120 corresponding flag values in LogStyles. Examples include: 121 122 >>> LogStyles.LOG_INFO + LogStyles.TESTCASE_LOG 123 124 >>> LogStyles.ALL_LEVELS + LogStyles.MONOLITH_LOG 125 126 >>> [LogStyles.DEFAULT_LEVELS + LogStyles.MONOLITH_LOG] 127 >>> LogStyles.LOG_ERROR + LogStyles.TO_ACTS_LOG] 128 stream_format: Format used for log output to stream 129 file_format: Format used for log output to files 130 """ 131 if name in _log_streams: 132 _log_streams[name].cleanup() 133 log_stream = _LogStream(name, log_name, base_path, subcontext, log_styles, 134 stream_format, file_format) 135 _set_logger(log_stream) 136 return log_stream.logger 137 138 139def _set_logger(log_stream): 140 _log_streams[log_stream.name] = log_stream 141 return log_stream 142 143 144class AlsoToLogHandler(Handler): 145 """Logs a message at a given level also to another logger. 146 147 Used for logging messages at a high enough level to the main log, or another 148 logger. 149 """ 150 151 def __init__(self, to_logger=None, *args, **kwargs): 152 super().__init__(*args, **kwargs) 153 self._log = logging.getLogger(to_logger) 154 155 def emit(self, record): 156 self._log.log(record.levelno, record.getMessage()) 157 158 159class MovableFileHandler(FileHandler): 160 """FileHandler implementation that allows the output file to be changed 161 during operation. 162 """ 163 def set_file(self, file_name): 164 """Set the target output file to file_name. 165 166 Args: 167 file_name: path to the new output file 168 """ 169 self.baseFilename = os.path.abspath(file_name) 170 if self.stream is not None: 171 new_stream = self._open() 172 # An atomic operation redirects the output and closes the old file 173 os.dup2(new_stream.fileno(), self.stream.fileno()) 174 self.stream = new_stream 175 176 177class MovableRotatingFileHandler(RotatingFileHandler): 178 """RotatingFileHandler implementation that allows the output file to be 179 changed during operation. Rotated files will automatically adopt the newest 180 output path. 181 """ 182 set_file = MovableFileHandler.set_file 183 184 185class InvalidStyleSetError(Exception): 186 """Raised when the given LogStyles are an invalid set.""" 187 188 189class _LogStream(object): 190 """A class that sets up a logging.Logger object. 191 192 The LogStream class creates a logging.Logger object. LogStream is also 193 responsible for managing the logger when events take place, such as 194 TestCaseEndedEvents and TestCaseBeginEvents. 195 196 Attributes: 197 name: The name of the LogStream. 198 logger: The logger created by this LogStream. 199 base_path: The base path used by the logger. Use logging.log_path 200 as default. 201 subcontext: Location of logs relative to the test context path. 202 stream_format: Format used for log output to stream 203 file_format: Format used for log output to files 204 205 _test_case_handler_descriptors: The list of HandlerDescriptors that are 206 used to create LogHandlers for each new test case. 207 _test_case_log_handlers: The list of current LogHandlers for the current 208 test case. 209 """ 210 211 def __init__(self, name, log_name=None, base_path='', subcontext='', 212 log_styles=LogStyles.NONE, stream_format=None, 213 file_format=None): 214 """Creates a LogStream. 215 216 Args: 217 name: The name of the LogStream. Used as the file name prefix. 218 log_name: The name of the underlying logger. Use LogStream name 219 as default. 220 base_path: The base path used by the logger. Use logging.log_path 221 as default. 222 subcontext: Location of logs relative to the test context path. 223 log_styles: An integer or array of integers that are the sum of 224 corresponding flag values in LogStyles. Examples include: 225 226 >>> LogStyles.LOG_INFO + LogStyles.TESTCASE_LOG 227 228 >>> LogStyles.ALL_LEVELS + LogStyles.MONOLITH_LOG 229 230 >>> [LogStyles.DEFAULT_LEVELS + LogStyles.MONOLITH_LOG] 231 >>> LogStyles.LOG_ERROR + LogStyles.TO_ACTS_LOG] 232 stream_format: Format used for log output to stream 233 file_format: Format used for log output to files 234 """ 235 self.name = name 236 if log_name is not None: 237 self.logger = logging.getLogger(log_name) 238 else: 239 self.logger = logging.getLogger(name) 240 # Add a NullHandler to suppress unwanted console output 241 self.logger.addHandler(_null_handler) 242 self.logger.propagate = False 243 self.base_path = base_path or logging.log_path 244 self.subcontext = subcontext 245 context.TestContext.add_base_output_path(self.logger.name, self.base_path) 246 context.TestContext.add_subcontext(self.logger.name, self.subcontext) 247 self.stream_format = stream_format 248 self.file_format = file_format 249 self._testclass_handlers = [] 250 self._testcase_handlers = [] 251 if not isinstance(log_styles, list): 252 log_styles = [log_styles] 253 self.__validate_styles(log_styles) 254 for log_style in log_styles: 255 self.__handle_style(log_style) 256 257 @staticmethod 258 def __validate_styles(_log_styles_list): 259 """Determines if the given list of styles is valid. 260 261 Terminology: 262 Log-level: any of [DEBUG, INFO, WARNING, ERROR, CRITICAL]. 263 Log Location: any of [MONOLITH_LOG, TESTCLASS_LOG, 264 TESTCASE_LOG, TO_STDOUT, TO_ACTS_LOG]. 265 266 Styles are invalid when any of the below criteria are met: 267 A log-level is not set within an element of the list. 268 A log location is not set within an element of the list. 269 A log-level, log location pair appears twice within the list. 270 A log-level has both TESTCLASS and TESTCASE locations set 271 within the list. 272 ROTATE_LOGS is set without MONOLITH_LOG, 273 TESTCLASS_LOG, or TESTCASE_LOG. 274 275 Raises: 276 InvalidStyleSetError if the given style cannot be achieved. 277 """ 278 279 def invalid_style_error(message): 280 raise InvalidStyleSetError('{LogStyle Set: %s} %s' % 281 (_log_styles_list, message)) 282 283 # Store the log locations that have already been set per level. 284 levels_dict = {} 285 for log_style in _log_styles_list: 286 for level in LogStyles.LOG_LEVELS: 287 if log_style & level: 288 levels_dict[level] = levels_dict.get(level, LogStyles.NONE) 289 # Check that a log-level, log location pair has not yet 290 # been set. 291 for log_location in LogStyles.LOG_LOCATIONS: 292 if log_style & log_location: 293 if log_location & levels_dict[level]: 294 invalid_style_error( 295 'The log location %s for log level %s has ' 296 'been set multiple times' % 297 (log_location, level)) 298 else: 299 levels_dict[level] |= log_location 300 # Check that for a given log-level, not more than one 301 # of MONOLITH_LOG, TESTCLASS_LOG, TESTCASE_LOG is set. 302 locations = levels_dict[level] & LogStyles.ALL_FILE_LOGS 303 valid_locations = [ 304 LogStyles.TESTCASE_LOG, LogStyles.TESTCLASS_LOG, 305 LogStyles.MONOLITH_LOG, LogStyles.NONE] 306 if locations not in valid_locations: 307 invalid_style_error( 308 'More than one of MONOLITH_LOG, TESTCLASS_LOG, ' 309 'TESTCASE_LOG is set for log level %s.' % level) 310 if log_style & LogStyles.ALL_LEVELS == 0: 311 invalid_style_error('LogStyle %s needs to set a log ' 312 'level.' % log_style) 313 if log_style & ~LogStyles.ALL_LEVELS == 0: 314 invalid_style_error('LogStyle %s needs to set a log ' 315 'location.' % log_style) 316 if log_style & LogStyles.ROTATE_LOGS and not log_style & ( 317 LogStyles.MONOLITH_LOG | LogStyles.TESTCLASS_LOG | 318 LogStyles.TESTCASE_LOG): 319 invalid_style_error('LogStyle %s has ROTATE_LOGS set, but does ' 320 'not specify a log type.' % log_style) 321 322 @staticmethod 323 def __create_rotating_file_handler(filename): 324 """Generates a callable to create an appropriate RotatingFileHandler.""" 325 # Magic number explanation: 10485760 == 10MB 326 return MovableRotatingFileHandler(filename, maxBytes=10485760, 327 backupCount=5) 328 329 @staticmethod 330 def __get_file_handler_creator(log_style): 331 """Gets the callable to create the correct FileLogHandler.""" 332 create_file_handler = MovableFileHandler 333 if log_style & LogStyles.ROTATE_LOGS: 334 create_file_handler = _LogStream.__create_rotating_file_handler 335 return create_file_handler 336 337 @staticmethod 338 def __get_lowest_log_level(log_style): 339 """Returns the lowest log level's LogStyle for the given log_style.""" 340 for log_level in LogStyles.LOG_LEVELS: 341 if log_level & log_style: 342 return log_level 343 return LogStyles.NONE 344 345 def __get_current_output_dir(self, depth=ContextLevel.TESTCASE): 346 """Gets the current output directory from the context system. Make the 347 directory if it doesn't exist. 348 349 Args: 350 depth: The desired level of the output directory. For example, 351 the TESTCLASS level would yield the directory associated with 352 the current test class context, even if the test is currently 353 within a test case. 354 """ 355 curr_context = context.get_current_context(depth) 356 return curr_context.get_full_output_path(self.logger.name) 357 358 def __create_handler(self, creator, level, location): 359 """Creates the FileHandler. 360 361 Args: 362 creator: The callable that creates the FileHandler 363 level: The logging level (INFO, DEBUG, etc.) for this handler. 364 location: The log location (MONOLITH, TESTCLASS, TESTCASE) for this 365 handler. 366 367 Returns: A FileHandler 368 """ 369 directory = self.__get_current_output_dir( 370 LogStyles.LOCATION_TO_CONTEXT_LEVEL[location]) 371 base_name = '%s_%s.txt' % (self.name, LogStyles.LEVEL_NAMES[level]) 372 handler = creator(os.path.join(directory, base_name)) 373 handler.setLevel(LogStyles.LEVEL_TO_NO[level]) 374 if self.file_format: 375 handler.setFormatter(self.file_format) 376 return handler 377 378 def __handle_style(self, log_style): 379 """Creates the handlers described in the given log_style.""" 380 handler_creator = self.__get_file_handler_creator(log_style) 381 382 # Handle streaming logs to STDOUT or the ACTS Logger 383 if log_style & (LogStyles.TO_ACTS_LOG | LogStyles.TO_STDOUT): 384 lowest_log_level = self.__get_lowest_log_level(log_style) 385 386 if log_style & LogStyles.TO_ACTS_LOG: 387 handler = AlsoToLogHandler() 388 else: # LogStyles.TO_STDOUT: 389 handler = StreamHandler(sys.stdout) 390 if self.stream_format: 391 handler.setFormatter(self.stream_format) 392 393 handler.setLevel(LogStyles.LEVEL_TO_NO[lowest_log_level]) 394 self.logger.addHandler(handler) 395 396 # Handle streaming logs to log-level files 397 for log_level in LogStyles.LOG_LEVELS: 398 log_location = log_style & LogStyles.ALL_FILE_LOGS 399 if not (log_style & log_level and log_location): 400 continue 401 402 handler = self.__create_handler( 403 handler_creator, log_level, log_location) 404 self.logger.addHandler(handler) 405 406 if log_style & LogStyles.TESTCLASS_LOG: 407 self._testclass_handlers.append(handler) 408 if log_style & LogStyles.TESTCASE_LOG: 409 self._testcase_handlers.append(handler) 410 411 def __remove_handler(self, handler): 412 """Removes a handler from the logger, unless it's a NullHandler.""" 413 if handler is not _null_handler: 414 handler.close() 415 self.logger.removeHandler(handler) 416 417 def update_handlers(self, event): 418 """Update the output file paths for log handlers upon a change in 419 the test context. 420 421 Args: 422 event: An instance of NewContextEvent. 423 """ 424 handlers = [] 425 if isinstance(event, context.NewTestClassContextEvent): 426 handlers = self._testclass_handlers + self._testcase_handlers 427 if isinstance(event, context.NewTestCaseContextEvent): 428 handlers = self._testcase_handlers 429 430 if not handlers: 431 return 432 new_dir = self.__get_current_output_dir() 433 for handler in handlers: 434 filename = os.path.basename(handler.baseFilename) 435 handler.set_file(os.path.join(new_dir, filename)) 436 437 def cleanup(self): 438 """Removes all LogHandlers from the logger.""" 439 for handler in self.logger.handlers: 440 self.__remove_handler(handler) 441