|
@@ -0,0 +1,356 @@
|
|
1
|
+#!/usr/bin/env python
|
|
2
|
+# -*- coding: utf-8 -*-
|
|
3
|
+#
|
|
4
|
+"""
|
|
5
|
+report (Report Module)
|
|
6
|
+======================
|
|
7
|
+
|
|
8
|
+**Author:**
|
|
9
|
+
|
|
10
|
+* Dirk Alders <sudo-dirk@mount-mockery.de>
|
|
11
|
+
|
|
12
|
+**Description:**
|
|
13
|
+
|
|
14
|
+ The Module is designed to help with python logging and to support some handlers for logging to memory.
|
|
15
|
+
|
|
16
|
+**Submodules:**
|
|
17
|
+
|
|
18
|
+* :class:`report.collectingHandler`
|
|
19
|
+* :class:`report.collectingRingHandler`
|
|
20
|
+* :class:`report.collectingTestcaseHandler`
|
|
21
|
+* :func:`report.consoleLoggingConfigure`
|
|
22
|
+* :class:`report.testSession`
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+**Unittest:**
|
|
26
|
+
|
|
27
|
+ See also the :download:`unittest <../../report/_testresults_/unittest.pdf>` documentation.
|
|
28
|
+"""
|
|
29
|
+__DEPENDENCIES__ = []
|
|
30
|
+
|
|
31
|
+import collections
|
|
32
|
+import logging
|
|
33
|
+from logging.config import dictConfig
|
|
34
|
+import os
|
|
35
|
+import sys
|
|
36
|
+
|
|
37
|
+logger_name = 'REPORT'
|
|
38
|
+logger = logging.getLogger(logger_name)
|
|
39
|
+
|
|
40
|
+__DESCRIPTION__ = """The Module {\\tt %s} is designed to help with python logging and to support some handlers for logging to memory.
|
|
41
|
+For more Information read the sphinx documentation.""" % __name__.replace('_', '\_')
|
|
42
|
+"""The Module Description"""
|
|
43
|
+__INTERPRETER__ = (2, 3, )
|
|
44
|
+"""The Tested Interpreter-Versions"""
|
|
45
|
+
|
|
46
|
+SHORT_FMT = "%(asctime)s: %(name)s - %(levelname)s - %(message)s"
|
|
47
|
+""" A short formatter including the most important information"""
|
|
48
|
+LONG_FMT = """~~~~(%(levelname)-10s)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
49
|
+File "%(pathname)s", line %(lineno)d, in %(funcName)s
|
|
50
|
+%(asctime)s: %(name)s- %(message)s
|
|
51
|
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"""
|
|
52
|
+""" A long formatter which results in links to the source code inside Eclipse"""
|
|
53
|
+MAX_FMT = """
|
|
54
|
+%(name)s
|
|
55
|
+%(levelno)s
|
|
56
|
+%(levelname)s
|
|
57
|
+%(pathname)s
|
|
58
|
+%(filename)s
|
|
59
|
+%(module)s
|
|
60
|
+%(lineno)d
|
|
61
|
+%(funcName)s
|
|
62
|
+%(created)f
|
|
63
|
+%(asctime)s
|
|
64
|
+%(msecs)d
|
|
65
|
+%(relativeCreated)d
|
|
66
|
+%(thread)d
|
|
67
|
+%(threadName)s
|
|
68
|
+%(process)d
|
|
69
|
+%(message)s"""
|
|
70
|
+DEFAULT_FMT = LONG_FMT
|
|
71
|
+"""The default formatstring"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+class collectingHandler(logging.Handler):
|
|
75
|
+ MY_LOGS = []
|
|
76
|
+
|
|
77
|
+ def __init__(self):
|
|
78
|
+ logging.Handler.__init__(self)
|
|
79
|
+ self.setFormatter(logging.Formatter(MAX_FMT))
|
|
80
|
+ self.setLevel(logging.DEBUG)
|
|
81
|
+
|
|
82
|
+ def emit(self, record):
|
|
83
|
+ self.format(record)
|
|
84
|
+ self.MY_LOGS.append(record.__dict__)
|
|
85
|
+
|
|
86
|
+ def make_independent(self):
|
|
87
|
+ self.MY_LOGS = []
|
|
88
|
+
|
|
89
|
+ def get_logs(self):
|
|
90
|
+ rv = []
|
|
91
|
+ while len(self.MY_LOGS) > 0:
|
|
92
|
+ rv.append(self.MY_LOGS.pop(0))
|
|
93
|
+ return rv
|
|
94
|
+
|
|
95
|
+ def get_str(self, logs=None, fmt=SHORT_FMT):
|
|
96
|
+ logs = logs or self.MY_LOGS
|
|
97
|
+ return '\n'.join([fmt % log for log in logs])
|
|
98
|
+
|
|
99
|
+ def __len__(self):
|
|
100
|
+ return len(self.MY_LOGS)
|
|
101
|
+
|
|
102
|
+ def __str__(self):
|
|
103
|
+ return self.get_str(self.MY_LOGS)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+class collectingRingHandler(collectingHandler):
|
|
107
|
+ MY_LOGS = collections.deque([], 10)
|
|
108
|
+
|
|
109
|
+ def __init__(self, max_logs=None):
|
|
110
|
+ collectingHandler.__init__(self)
|
|
111
|
+ if max_logs is not None and max_logs != self.MY_LOGS.maxlen:
|
|
112
|
+ self.MY_LOGS.__init__(list(self.MY_LOGS), max_logs)
|
|
113
|
+
|
|
114
|
+ def make_independent(self):
|
|
115
|
+ self.MY_LOGS = collections.deque([], self.MY_LOGS.maxlen)
|
|
116
|
+
|
|
117
|
+ def get_logs(self):
|
|
118
|
+ return list(self.MY_LOGS)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+TCEL_SINGLE = 0
|
|
122
|
+"""Testcase level (smoke), this is just a rough test for the main functionality"""
|
|
123
|
+TCEL_SMOKE = 10
|
|
124
|
+"""Testcase level (smoke), this is just a rough test for the main functionality"""
|
|
125
|
+TCEL_SHORT = 50
|
|
126
|
+"""Testcase level (short), this is a short test for an extended functionality"""
|
|
127
|
+TCEL_FULL = 90
|
|
128
|
+"""Testcase level (full), this is a complete test for the full functionality"""
|
|
129
|
+TCEL_NAMES = {
|
|
130
|
+ TCEL_SINGLE: 'Single Test',
|
|
131
|
+ TCEL_SMOKE: 'Smoke Test (Minumum subset)',
|
|
132
|
+ TCEL_SHORT: 'Short Test (Subset)',
|
|
133
|
+ TCEL_FULL: 'Full Test (all defined tests)'
|
|
134
|
+}
|
|
135
|
+"""Dictionary for resolving the test case levels (TCL) to a (human readable) name"""
|
|
136
|
+
|
|
137
|
+TCEL_REVERSE_NAMED = {
|
|
138
|
+ 'short': TCEL_SHORT,
|
|
139
|
+ 'smoke': TCEL_SMOKE,
|
|
140
|
+ 'single': TCEL_SINGLE,
|
|
141
|
+ 'full': TCEL_FULL,
|
|
142
|
+}
|
|
143
|
+"""Dictionary for resolving named test case levels (TCL) to test case level number"""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+class collectingTestcaseHandler(collectingHandler):
|
|
147
|
+ MY_LOGS = []
|
|
148
|
+
|
|
149
|
+ def emit(self, record):
|
|
150
|
+ self.format(record)
|
|
151
|
+ self.MY_LOGS.append(record.__dict__)
|
|
152
|
+ self.MY_LOGS[-1]['moduleLogger'] = collectingHandler().get_logs()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+def appLoggingConfigure(basepath, target, log_name_lvl=[], fmt=SHORT_FMT, ring_logs=None):
|
|
156
|
+ target_handlers = ['main', 'logwarn']
|
|
157
|
+ # define handler
|
|
158
|
+ #
|
|
159
|
+ if target == 'stdout':
|
|
160
|
+ handler = dict(main={
|
|
161
|
+ 'level': 'DEBUG',
|
|
162
|
+ 'formatter': 'format',
|
|
163
|
+ 'class': 'logging.StreamHandler',
|
|
164
|
+ 'stream': 'ext://sys.stdout',
|
|
165
|
+ })
|
|
166
|
+ elif target == 'logfile':
|
|
167
|
+ handler = dict(main={
|
|
168
|
+ 'level': 'DEBUG',
|
|
169
|
+ 'formatter': 'format',
|
|
170
|
+ 'class': 'logging.handlers.RotatingFileHandler',
|
|
171
|
+ 'filename': os.path.join(basepath, 'messages.log'),
|
|
172
|
+ 'mode': 'a',
|
|
173
|
+ 'maxBytes': 10485760,
|
|
174
|
+ 'backupCount': 7
|
|
175
|
+ })
|
|
176
|
+ else:
|
|
177
|
+ handler = dict(my_handler={
|
|
178
|
+ 'level': 'DEBUG',
|
|
179
|
+ 'formatter': 'my_format',
|
|
180
|
+ 'class': 'logging.NullHandler',
|
|
181
|
+ })
|
|
182
|
+ if ring_logs is not None:
|
|
183
|
+ target_handlers.append('ring')
|
|
184
|
+ handler['ring'] = {
|
|
185
|
+ 'class': 'report.collectingRingHandler',
|
|
186
|
+ 'max_logs': ring_logs,
|
|
187
|
+ }
|
|
188
|
+ handler['logwarn'] = {
|
|
189
|
+ 'level': 'WARNING',
|
|
190
|
+ 'formatter': 'long',
|
|
191
|
+ 'class': 'logging.handlers.RotatingFileHandler',
|
|
192
|
+ 'filename': os.path.join(basepath, 'messages.warn'),
|
|
193
|
+ 'mode': 'a',
|
|
194
|
+ 'maxBytes': 10485760,
|
|
195
|
+ 'backupCount': 2
|
|
196
|
+ }
|
|
197
|
+ # define loggers
|
|
198
|
+ #
|
|
199
|
+ loggers = {}
|
|
200
|
+ for name, lvl in log_name_lvl:
|
|
201
|
+ loggers[name] = {
|
|
202
|
+ 'handlers': target_handlers,
|
|
203
|
+ 'level': lvl,
|
|
204
|
+ 'propagate': False
|
|
205
|
+ }
|
|
206
|
+ # configure logging
|
|
207
|
+ #
|
|
208
|
+ dictConfig(dict(
|
|
209
|
+ version=1,
|
|
210
|
+ formatters={
|
|
211
|
+ 'long': {
|
|
212
|
+ 'format': LONG_FMT
|
|
213
|
+ },
|
|
214
|
+ 'format': {
|
|
215
|
+ 'format': fmt,
|
|
216
|
+ },
|
|
217
|
+ },
|
|
218
|
+ handlers=handler,
|
|
219
|
+ loggers=loggers,
|
|
220
|
+ ))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+class testSession(dict):
|
|
224
|
+ KEY_NAME = 'name'
|
|
225
|
+ KEY_FAILED_TESTS = 'number_of_failed_tests'
|
|
226
|
+ KEY_POSSIBLY_FAILED_TESTS = 'number_of_possibly_failed_tests'
|
|
227
|
+ KEY_SUCCESS_TESTS = 'number_of_successfull_tests'
|
|
228
|
+ KEY_ALL_TESTS = 'number_of_tests'
|
|
229
|
+ KEY_EXEC_LVL = 'testcase_execution_level'
|
|
230
|
+ KEY_EXEC_NAMES = 'testcase_names'
|
|
231
|
+ KEY_LVL_NAMES = 'level_names'
|
|
232
|
+ KEY_TESTCASELIST = 'testcases'
|
|
233
|
+ KEY_UID_LIST = 'uid_list_sorted'
|
|
234
|
+ #
|
|
235
|
+ DEFAULT_BASE_DATA = {
|
|
236
|
+ KEY_NAME: 'Default Testsession name',
|
|
237
|
+ KEY_FAILED_TESTS: 0,
|
|
238
|
+ KEY_POSSIBLY_FAILED_TESTS: 0,
|
|
239
|
+ KEY_FAILED_TESTS: 0,
|
|
240
|
+ KEY_SUCCESS_TESTS: 0,
|
|
241
|
+ KEY_ALL_TESTS: 0,
|
|
242
|
+ KEY_EXEC_LVL: TCEL_FULL,
|
|
243
|
+ KEY_EXEC_NAMES: TCEL_NAMES,
|
|
244
|
+ }
|
|
245
|
+
|
|
246
|
+ def __init__(self, module_names=[], **kwargs):
|
|
247
|
+ dict.__init__(self, time_consumption=0.)
|
|
248
|
+ self.__testcase__ = None
|
|
249
|
+ self.__set_base_data__(**kwargs)
|
|
250
|
+ self.__configure_logging__(module_names)
|
|
251
|
+
|
|
252
|
+ def __set_base_data__(self, **kwargs):
|
|
253
|
+ for key in set([key for key in self.DEFAULT_BASE_DATA.keys()] + [key for key in kwargs.keys()]):
|
|
254
|
+ self[key] = kwargs.get(key, self.DEFAULT_BASE_DATA.get(key))
|
|
255
|
+ self[self.KEY_TESTCASELIST] = {}
|
|
256
|
+ self[self.KEY_UID_LIST] = []
|
|
257
|
+
|
|
258
|
+ def __configure_logging__(self, module_names):
|
|
259
|
+ #
|
|
260
|
+ # Configure logging for testSession
|
|
261
|
+ #
|
|
262
|
+ logging_config = dict(
|
|
263
|
+ version=1,
|
|
264
|
+ formatters={
|
|
265
|
+ 'short': {
|
|
266
|
+ 'format': SHORT_FMT,
|
|
267
|
+ },
|
|
268
|
+ 'long': {
|
|
269
|
+ 'format': LONG_FMT,
|
|
270
|
+ },
|
|
271
|
+ },
|
|
272
|
+ handlers={
|
|
273
|
+ 'console': {
|
|
274
|
+ 'level': 'DEBUG',
|
|
275
|
+ 'class': 'logging.NullHandler',
|
|
276
|
+ 'formatter': 'short',
|
|
277
|
+ },
|
|
278
|
+ 'module_logs': {
|
|
279
|
+ 'level': 'DEBUG',
|
|
280
|
+ 'class': 'report.collectingHandler',
|
|
281
|
+ 'formatter': 'short',
|
|
282
|
+ },
|
|
283
|
+ 'testcase_logs': {
|
|
284
|
+ 'level': 'DEBUG',
|
|
285
|
+ 'class': 'report.collectingTestcaseHandler',
|
|
286
|
+ 'formatter': 'short',
|
|
287
|
+ },
|
|
288
|
+ },
|
|
289
|
+ loggers=self.__module_loggers__(module_names),
|
|
290
|
+ )
|
|
291
|
+ dictConfig(logging_config)
|
|
292
|
+
|
|
293
|
+ def __module_loggers__(self, module_names):
|
|
294
|
+ rv = {}
|
|
295
|
+ rv['__tLogger__'] = dict(handlers=['console', 'testcase_logs'], level='DEBUG', propagate=False)
|
|
296
|
+ for name in module_names + ['__mLogger__']:
|
|
297
|
+ rv[name] = dict(handlers=['console', 'module_logs'], level='DEBUG', propagate=False)
|
|
298
|
+ return rv
|
|
299
|
+
|
|
300
|
+ def testCase(self, name, testcase_execution_level, test_method, *args, **kwargs):
|
|
301
|
+ if testcase_execution_level <= self[self.KEY_EXEC_LVL]:
|
|
302
|
+ tLogger = logging.getLogger('__tLogger__')
|
|
303
|
+ tHandler = collectingTestcaseHandler()
|
|
304
|
+ if len(tHandler.MY_LOGS) > 0:
|
|
305
|
+ raise AttributeError("Testcaselogger shall be empty after closing testcase!")
|
|
306
|
+ tLogger._log(logging.DEBUG, name, None)
|
|
307
|
+ if len(tHandler.MY_LOGS) != 1:
|
|
308
|
+ raise AttributeError("Testcaselogger shall have only one entry for the main testcase (temporary)!")
|
|
309
|
+ self.__testcase__ = tHandler.get_logs()[0]
|
|
310
|
+ test_method(logging.getLogger('__tLogger__'), *args, **kwargs)
|
|
311
|
+ self.__close_active_testcase__()
|
|
312
|
+
|
|
313
|
+ def __close_active_testcase__(self):
|
|
314
|
+ if self.__testcase__ is not None:
|
|
315
|
+ name = self.__testcase__.get('message')
|
|
316
|
+ #
|
|
317
|
+ # Add testcase
|
|
318
|
+ #
|
|
319
|
+ tch = collectingTestcaseHandler()
|
|
320
|
+ self.__testcase__['testcaseLogger'] = tch.get_logs()
|
|
321
|
+ if name in self[self.KEY_TESTCASELIST]:
|
|
322
|
+ raise AttributeError("Testcase named %s already exists" % name)
|
|
323
|
+ self[self.KEY_TESTCASELIST][name] = self.__testcase__
|
|
324
|
+ self[self.KEY_UID_LIST].append(name)
|
|
325
|
+ #
|
|
326
|
+ # Adapt testcase data
|
|
327
|
+ #
|
|
328
|
+ self[self.KEY_TESTCASELIST][name]['levelno'] = 0
|
|
329
|
+ self[self.KEY_TESTCASELIST][name]['time_consumption'] = 0.
|
|
330
|
+ for teststep in self[self.KEY_TESTCASELIST][name]['testcaseLogger']:
|
|
331
|
+ # store maximum level to testcase
|
|
332
|
+ if teststep.get('levelno') > self[self.KEY_TESTCASELIST][name]['levelno']:
|
|
333
|
+ self[self.KEY_TESTCASELIST][name]['levelno'] = teststep.get('levelno')
|
|
334
|
+ self[self.KEY_TESTCASELIST][name]['levelname'] = teststep.get('levelname')
|
|
335
|
+ # store time_consumption for teststep
|
|
336
|
+ try:
|
|
337
|
+ teststep['time_consumption'] = teststep['created'] - teststep['moduleLogger'][-1]['created']
|
|
338
|
+ except IndexError:
|
|
339
|
+ teststep['time_consumption'] = 0.
|
|
340
|
+ # Increment testcase time_comsumption
|
|
341
|
+ # Increment testcase counters
|
|
342
|
+ #
|
|
343
|
+ self[self.KEY_ALL_TESTS] += 1
|
|
344
|
+ if self[self.KEY_TESTCASELIST][name]['levelno'] <= logging.INFO:
|
|
345
|
+ self[self.KEY_SUCCESS_TESTS] += 1
|
|
346
|
+ elif self[self.KEY_TESTCASELIST][name]['levelno'] >= logging.ERROR:
|
|
347
|
+ self[self.KEY_FAILED_TESTS] += 1
|
|
348
|
+ else:
|
|
349
|
+ self[self.KEY_POSSIBLY_FAILED_TESTS] += 1
|
|
350
|
+ # Set testcase time and time_consumption
|
|
351
|
+ self[self.KEY_TESTCASELIST][name]['time_start'] = self.__testcase__['asctime']
|
|
352
|
+ self[self.KEY_TESTCASELIST][name]['time_finished'] = teststep['asctime']
|
|
353
|
+ self[self.KEY_TESTCASELIST][name]['time_consumption'] = teststep['created'] - self.__testcase__['created']
|
|
354
|
+ # Set testcase time consumption
|
|
355
|
+ self['time_consumption'] += self[self.KEY_TESTCASELIST][name]['time_consumption']
|
|
356
|
+ self.__testcase__ = None
|