1# Copyright (c) 2017 Mark D. Hill and David A. Wood 2# All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without 5# modification, are permitted provided that the following conditions are 6# met: redistributions of source code must retain the above copyright 7# notice, this list of conditions and the following disclaimer; 8# redistributions in binary form must reproduce the above copyright 9# notice, this list of conditions and the following disclaimer in the 10# documentation and/or other materials provided with the distribution; 11# neither the name of the copyright holders nor the names of its 12# contributors may be used to endorse or promote products derived from 13# this software without specific prior written permission. 14# 15# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26# 27# Authors: Sean Wilson 28 29''' 30This module supplies the global `test_log` object which all testing 31results and messages are reported through. 32''' 33import wrappers 34 35 36class LogLevel(): 37 Fatal = 0 38 Error = 1 39 Warn = 2 40 Info = 3 41 Debug = 4 42 Trace = 5 43 44 45class RecordTypeCounterMetaclass(type): 46 ''' 47 Record type metaclass. 48 49 Adds a static integer value in addition to typeinfo so identifiers 50 are common across processes, networks and module reloads. 51 ''' 52 counter = 0 53 def __init__(cls, name, bases, dct): 54 cls.type_id = RecordTypeCounterMetaclass.counter 55 RecordTypeCounterMetaclass.counter += 1 56 57 58class Record(object): 59 ''' 60 A generic object that is passed to the :class:`Log` and its handlers. 61 62 ..note: Although not statically enforced, all items in the record should be 63 be pickleable. This enables logging accross multiple processes. 64 ''' 65 __metaclass__ = RecordTypeCounterMetaclass 66 67 def __init__(self, **data): 68 self.data = data 69 70 def __getitem__(self, item): 71 if item not in self.data: 72 raise KeyError('%s not in record %s' %\ 73 (item, self.__class__.__name__)) 74 return self.data[item] 75 76 def __str__(self): 77 return str(self.data) 78 79 80class StatusRecord(Record): 81 def __init__(self, obj, status): 82 Record.__init__(self, metadata=obj.metadata, status=status) 83class ResultRecord(Record): 84 def __init__(self, obj, result): 85 Record.__init__(self, metadata=obj.metadata, result=result) 86#TODO Refactor this shit... Not ideal. Should just specify attributes. 87class TestStatus(StatusRecord): 88 pass 89class SuiteStatus(StatusRecord): 90 pass 91class LibraryStatus(StatusRecord): 92 pass 93class TestResult(ResultRecord): 94 pass 95class SuiteResult(ResultRecord): 96 pass 97class LibraryResult(ResultRecord): 98 pass 99# Test Output Types 100class TestStderr(Record): 101 pass 102class TestStdout(Record): 103 pass 104# Message (Raw String) Types 105class TestMessage(Record): 106 pass 107class LibraryMessage(Record): 108 pass 109 110 111class Log(object): 112 def __init__(self): 113 self.handlers = [] 114 self._opened = False # TODO Guards to methods 115 self._closed = False # TODO Guards to methods 116 117 def finish_init(self): 118 self._opened = True 119 120 def close(self): 121 self._closed = True 122 for handler in self.handlers: 123 handler.close() 124 125 def log(self, record): 126 if not self._opened: 127 self.finish_init() 128 if self._closed: 129 raise Exception('The log has been closed' 130 ' and is no longer available.') 131 132 map(lambda handler:handler.prehandle(), self.handlers) 133 for handler in self.handlers: 134 handler.handle(record) 135 handler.posthandle() 136 137 def add_handler(self, handler): 138 if self._opened: 139 raise Exception('Unable to add a handler once the log is open.') 140 self.handlers.append(handler) 141 142 def close_handler(self, handler): 143 handler.close() 144 self.handlers.remove(handler) 145 146 147class Handler(object): 148 ''' 149 Empty implementation of the interface available to handlers which 150 is expected by the :class:`Log`. 151 ''' 152 def __init__(self): 153 pass 154 155 def handle(self, record): 156 pass 157 158 def close(self): 159 pass 160 161 def prehandle(self): 162 pass 163 164 def posthandle(self): 165 pass 166 167 168class LogWrapper(object): 169 _result_typemap = { 170 wrappers.LoadedLibrary.__name__: LibraryResult, 171 wrappers.LoadedSuite.__name__: SuiteResult, 172 wrappers.LoadedTest.__name__: TestResult, 173 } 174 _status_typemap = { 175 wrappers.LoadedLibrary.__name__: LibraryStatus, 176 wrappers.LoadedSuite.__name__: SuiteStatus, 177 wrappers.LoadedTest.__name__: TestStatus, 178 } 179 def __init__(self, log): 180 self.log_obj = log 181 182 def log(self, *args, **kwargs): 183 self.log_obj.log(*args, **kwargs) 184 185 # Library Logging Methods 186 # TODO Replace these methods in a test/create a wrapper? 187 # That way they still can log like this it's just hidden that they 188 # capture the current test. 189 def message(self, message, level=LogLevel.Info, bold=False, **metadata): 190 self.log_obj.log(LibraryMessage(message=message, level=level, 191 bold=bold, **metadata)) 192 193 def error(self, message): 194 self.message(message, LogLevel.Error) 195 196 def warn(self, message): 197 self.message(message, LogLevel.Warn) 198 199 def info(self, message): 200 self.message(message, LogLevel.Info) 201 202 def debug(self, message): 203 self.message(message, LogLevel.Debug) 204 205 def trace(self, message): 206 self.message(message, LogLevel.Trace) 207 208 # Ongoing Test Logging Methods 209 def status_update(self, obj, status): 210 self.log_obj.log( 211 self._status_typemap[obj.__class__.__name__](obj, status)) 212 213 def result_update(self, obj, result): 214 self.log_obj.log( 215 self._result_typemap[obj.__class__.__name__](obj, result)) 216 217 def test_message(self, test, message, level): 218 self.log_obj.log(TestMessage(message=message, level=level, 219 test_uid=test.uid, suite_uid=test.parent_suite.uid)) 220 221 # NOTE If performance starts to drag on logging stdout/err 222 # replace metadata with just test and suite uid tags. 223 def test_stdout(self, test, suite, buf): 224 self.log_obj.log(TestStdout(buffer=buf, metadata=test.metadata)) 225 226 def test_stderr(self, test, suite, buf): 227 self.log_obj.log(TestStderr(buffer=buf, metadata=test.metadata)) 228 229 def close(self): 230 self.log_obj.close() 231 232class TestLogWrapper(object): 233 def __init__(self, log, test, suite): 234 self.log_obj = log 235 self.test = test 236 237 def test_message(self, message, level): 238 self.log_obj.test_message(test=self.test, 239 message=message, level=level) 240 241 def error(self, message): 242 self.test_message(message, LogLevel.Error) 243 244 def warn(self, message): 245 self.test_message(message, LogLevel.Warn) 246 247 def info(self, message): 248 self.test_message(message, LogLevel.Info) 249 250 def debug(self, message): 251 self.test_message(message, LogLevel.Debug) 252 253 def trace(self, message): 254 self.test_message(message, LogLevel.Trace) 255 256test_log = LogWrapper(Log()) 257