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