# Copyright (c) 2017 Mark D. Hill and David A. Wood # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer; # redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution; # neither the name of the copyright holders nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # Authors: Sean Wilson ''' This module supplies the global `test_log` object which all testing results and messages are reported through. ''' import testlib.wrappers as wrappers class LogLevel(): Fatal = 0 Error = 1 Warn = 2 Info = 3 Debug = 4 Trace = 5 class RecordTypeCounterMetaclass(type): ''' Record type metaclass. Adds a static integer value in addition to typeinfo so identifiers are common across processes, networks and module reloads. ''' counter = 0 def __init__(cls, name, bases, dct): cls.type_id = RecordTypeCounterMetaclass.counter RecordTypeCounterMetaclass.counter += 1 class Record(object, metaclass=RecordTypeCounterMetaclass): ''' A generic object that is passed to the :class:`Log` and its handlers. ..note: Although not statically enforced, all items in the record should be be pickleable. This enables logging accross multiple processes. ''' def __init__(self, **data): self.data = data def __getitem__(self, item): if item not in self.data: raise KeyError('%s not in record %s' %\ (item, self.__class__.__name__)) return self.data[item] def __str__(self): return str(self.data) class StatusRecord(Record): def __init__(self, obj, status): Record.__init__(self, metadata=obj.metadata, status=status) class ResultRecord(Record): def __init__(self, obj, result): Record.__init__(self, metadata=obj.metadata, result=result) #TODO Refactor this shit... Not ideal. Should just specify attributes. class TestStatus(StatusRecord): pass class SuiteStatus(StatusRecord): pass class LibraryStatus(StatusRecord): pass class TestResult(ResultRecord): pass class SuiteResult(ResultRecord): pass class LibraryResult(ResultRecord): pass # Test Output Types class TestStderr(Record): pass class TestStdout(Record): pass # Message (Raw String) Types class TestMessage(Record): pass class LibraryMessage(Record): pass class Log(object): _result_typemap = { wrappers.LoadedLibrary.__name__: LibraryResult, wrappers.LoadedSuite.__name__: SuiteResult, wrappers.LoadedTest.__name__: TestResult, } _status_typemap = { wrappers.LoadedLibrary.__name__: LibraryStatus, wrappers.LoadedSuite.__name__: SuiteStatus, wrappers.LoadedTest.__name__: TestStatus, } def __init__(self, test=None): self.test = test self.handlers = [] self._opened = False # TODO Guards to methods self._closed = False # TODO Guards to methods def finish_init(self): self._opened = True def close(self): self._closed = True for handler in self.handlers: handler.close() def log(self, record): if not self._opened: self.finish_init() if self._closed: raise Exception('The log has been closed' ' and is no longer available.') for handler in self.handlers: handler.handle(record) def message(self, message, level=LogLevel.Info, bold=False, **metadata): if self.test: record = TestMessage(message=message, level=level, test_uid=self.test.uid, suite_uid=self.test.parent_suite.uid) else: record = LibraryMessage(message=message, level=level, bold=bold, **metadata) self.log(record) def error(self, message): self.message(message, LogLevel.Error) def warn(self, message): self.message(message, LogLevel.Warn) def info(self, message): self.message(message, LogLevel.Info) def debug(self, message): self.message(message, LogLevel.Debug) def trace(self, message): self.message(message, LogLevel.Trace) def status_update(self, obj, status): self.log( self._status_typemap[obj.__class__.__name__](obj, status)) def result_update(self, obj, result): self.log( self._result_typemap[obj.__class__.__name__](obj, result)) def add_handler(self, handler): if self._opened: raise Exception('Unable to add a handler once the log is open.') self.handlers.append(handler) def close_handler(self, handler): handler.close() self.handlers.remove(handler) test_log = Log()