-
Notifications
You must be signed in to change notification settings - Fork 3
/
unittest.py
538 lines (451 loc) · 13.5 KB
/
unittest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
import sys
import traceback
class SkipTest(Exception):
"""
Class to handle skipping test functionality
"""
pass
class TestResult:
"""
Class to handle test result functionality
"""
def __init__(self):
self.errorsNum = 0
self.failuresNum = 0
self.skippedNum = 0
self.testsRun = 0
def wasSuccessful(self): # noqa
"""
Method to handle indication of a successful test functionality
Returns:
bool
"""
return self.errorsNum == 0 and self.failuresNum == 0
class AssertRaisesContext:
"""
Class to handle an assertion raising context
"""
def __init__(self, exc: Exception):
"""
Params:
exc: Exception
"""
self.expected = exc
self.raised = None
self.traceback = None
self.exception_value = None
def __enter__(self):
"""
Magic method to handle enter implementation objects used with the with statement
Returns:
object: AssertRaisesContext
"""
return self
def __exit__(self, exc_type: type, exc_value: Exception, traceback):
"""
Magic method to handle exit implementation objects used with the with statement
Params:
exc_type: type the raised exception type (class)
exc_value: Exception the raised exception instance
traceback: the traceback for the raised exception
Returns:
bool
"""
self.traceback = traceback
self.raised = exc_type
self.exception_value = exc_value
if exc_type is None:
assert False, '%r not raised' % self.expected
if issubclass(exc_type, self.expected):
return True
# unhandled exceptions will get re-raise: Returning false indicates not handled
return False
class TestCase:
"""
Class to handle unittest test case functionality
"""
@classmethod
def setUpClass(cls):
"""
Setup resources and conditions need for the whole suite (TestCase)
The main test runner executes this one time only before creating an instance of the class
"""
pass
@classmethod
def tearDownClass(cls):
"""
Release resources and restore conditions after the test suite has finished
"""
pass
def setUp(self):
"""
Setup resources and starting conditions needed for every test in the suite
The main test runner executes this before calling any test method in the suite
"""
pass
def tearDown(self):
"""
Release resources, and do any needed cleanup after every test in the suite
The main test runner executes this after calling any test method in the suite
"""
pass
def run(self, result: TestResult):
for name in dir(self):
if name.startswith('test'):
print(f'{name} ({self.__qualname__}) ...', end='') # report progress
test_method = getattr(self, name)
self.setUp() # Pre-test setup (every test)
try:
result.testsRun += 1
test_method()
print(' ok')
except SkipTest as e:
print(' skipped:', e.args[0])
result.skippedNum += 1
result.testsRun -= 1 # not run if skipped
except AssertionError as e:
print(' FAIL:', e.args[0] if e.args else 'no assert message')
result.failuresNum += 1
except (SystemExit, KeyboardInterrupt):
raise
except Exception as e: # noqa
print(' ERROR', type(e).__name__)
print(''.join(traceback.format_exception(e)))
result.errorsNum += 1
finally:
self.tearDown() # Post-test teardown (every test)
@staticmethod
def assertAlmostEqual(x, y, places=None, msg='', delta=None):
"""
Method to handle assert almost equal logic
Params:
x: any
y: any
places: NoneType, optional
msg: str, optional
delta: NoneType, optional
"""
if x == y:
return
if delta is not None and places is not None:
raise TypeError('specify delta or places not both')
if delta is not None:
if abs(x - y) <= delta:
return
if not msg:
msg = '%r != %r within %r delta' % (x, y, delta)
else:
if places is None:
places = 7
if round(abs(y - x), places) == 0:
return
if not msg:
msg = '%r != %r within %r places' % (x, y, places)
assert False, msg
def fail(self, msg=''): # noqa
"""
Method to handle fail logic
Params:
msg: str, optional
"""
assert False, msg
def assertEqual(self, x, y, msg=''): # noqa
"""
Method to handle assert equal logic
Params:
x: any
y: any
msg: str, optional
"""
if not msg: # noqa
msg = '%r vs (expected) %r' % (x, y)
assert x == y, msg
def assertNotEqual(self, x, y, msg=''): # noqa
"""
Method to handle assert not equal logic
Params:
x: any
y: any
msg: str, optional
"""
if not msg:
msg = '%r not expected to be equal %r' % (x, y)
assert x != y, msg
def assertNotAlmostEqual(self, x, y, places=None, msg='', delta=None): # noqa
"""
Method to handle assert not almost equal logic
Params:
x: any
y: any
places: None, optional
msg: str, optional
delta: None, optional
"""
if delta is not None and places is not None:
raise TypeError("specify delta or places not both")
if delta is not None:
if not (x == y) and abs(x - y) > delta:
return
if not msg:
msg = '%r == %r within %r delta' % (x, y, delta)
else:
if places is None:
places = 7
if not (x == y) and round(abs(y - x), places) != 0:
return
if not msg:
msg = '%r == %r within %r places' % (x, y, places)
assert False, msg
def assertIs(self, x, y, msg=''): # noqa
"""
Method to handle assert is logic
Params:
x: any
y: any
msg: str, optional
"""
if not msg:
msg = '%r is not %r' % (x, y)
assert x is y, msg
def assertIsNot(self, x, y, msg=''): # noqa
"""
Method to handle assert is not logic
Params:
x: any
y: any
msg: str, optional
"""
if not msg:
msg = '%r is %r' % (x, y)
assert x is not y, msg
def assertIsNone(self, x, msg=''): # noqa
"""
Method to handle assert is none logic
Params:
x: any
msg: str, optional
"""
if not msg:
msg = '%r is not None' % x
assert x is None, msg
def assertIsNotNone(self, x, msg=''): # noqa
"""
Method to handle assert is not none logic
Params:
x: any
msg: str, optional
"""
if not msg:
msg = '%r is None' % x
assert x is not None, msg
def assertTrue(self, x, msg=''): # noqa
"""
Method to handle assert true logic
Params:
x: any
msg: str, optional
"""
if not msg:
msg = 'Expected %r to be True' % x
assert x, msg
def assertFalse(self, x, msg=''): # noqa
"""
Method to handle assert false logic
Params:
x: any
msg: str, optional
"""
if not msg:
msg = 'Expected %r to be False' % x
assert not x, msg
def assertIn(self, x, y, msg=''): # noqa
"""
Method to handle assert in logic
Params:
x: any
y: any
msg: str, optional
"""
if not msg:
msg = 'Expected %r to be in %r' % (x, y)
assert x in y, msg
def assertNotIn(self, x, y, msg=''): # noqa
"""
Method to handle assert not in logic
Params:
x: any
y: any
msg: str, optional
"""
if not msg:
msg = 'Expected %r to not be in %r' % (x, y)
assert x not in y, msg
def assertIsInstance(self, x, y, msg=''): # noqa
"""
Method to handle assert is instance logic
Params:
x: any
y: any
msg: str, optional
"""
assert isinstance(x, y), msg
@staticmethod
def assertRaises(exc, func=None, *args, **kwargs):
"""
Method to handle assert is instance logic
Params:
exc: str
func: NoneType, optional
*args: any, optional
**kwargs: any, optional
Returns:
object or None
"""
if func is None:
return AssertRaisesContext(exc)
try:
func(*args, **kwargs)
assert False, "%r not raised" % exc
except Exception as e:
if isinstance(e, exc):
return None
raise
def skip(msg): # noqa
"""
Function to handle skip logic
Params:
msg: str
Returns:
object
"""
def _decor(func): # noqa
"""
Inner function to handle private _decor logic
Params:
func: function
Closure:
msg: str
Returns:
object
"""
def _inner(self): # noqa
"""
Inner function to handle replacing original fun with _inner
Params:
self: class instance; subclass of TestCase
Closure:
msg: str
Returns:
object
"""
raise SkipTest(msg)
return _inner
return _decor
def skipIf(cond, msg): # noqa
"""
Function to handle skip if logic
Params:
cond: str
msg: str
Returns:
object
"""
if not cond:
return lambda x: x
return skip(msg)
def skipUnless(cond, msg): # noqa
"""
Function to handle skip unless logic
Params:
cond: str
msg: str
Returns:
object
"""
if cond:
return lambda x: x
return skip(msg)
class TestSuite:
"""
Class to handle unittest test suite functionality
"""
def __init__(self):
self.tests = []
def addTest(self, cls): # noqa
"""
Method to handle adding a test functionality
Params:
cls: str
"""
self.tests.append(cls)
class TestRunner:
"""
Class to handle test runner functionality
"""
def run(self, suite): # noqa
"""
Method to handle test run functionality
Params:
suite: TestSuite
Returns:
TestResult
"""
res = TestResult()
for c in suite.tests:
run_class(c, res)
print('Ran %d tests\n' % res.testsRun)
if res.failuresNum > 0 or res.errorsNum > 0:
print('FAILED (failures=%d, errors=%d)' % (res.failuresNum, res.errorsNum))
else:
msg = 'OK'
if res.skippedNum > 0:
msg += ' (%d skipped)' % res.skippedNum
print(msg)
return res
def run_class(test_class: TestCase, test_result: TestResult):
"""
Execute test methods within a test class, handling setup and teardown.
Params:
test_class: TestCase subclass indicating the class to run.
test_result: TestResult instance to update with test outcomes.
"""
context = 'setUpClass'
try:
test_class.setUpClass()
context = 'instantiate class'
testing_instance = test_class()
context = 'run tests'
testing_instance.run(test_result)
except Exception as exc:
print(f'Error in {context} for {test_class.__name__}:')
traceback_str = traceback.format_exception(exc)
print(''.join(traceback_str))
if context != 'run tests':
context = 'early tearDownClass due to error'
# Always Proceed with tearDownClass, with varying context
if context == 'run tests':
context = 'tearDownClass'
try:
test_class.tearDownClass()
except Exception as exc:
print(f'Error in {context} for {test_class.__name__}: {exc}')
def main(module='__main__'):
def test_cases(m): # noqa
"""
Function to handle test case running functionality
Params:
m: object
"""
for tn in dir(m):
c = getattr(m, tn) # noqa
if isinstance(c, type) and issubclass(c, TestCase) and c is not TestCase:
yield c
m = __import__(module, None, None, ['*']) # handle tests in folder
suite = TestSuite()
for c in test_cases(m):
suite.addTest(c)
runner = TestRunner()
result = runner.run(suite)
# Terminate with non-zero return code in case of failures
sys.exit(result.failuresNum > 0)
# cSpell:ignore noqa