Basic idea for the new cw-2.py
's timeout
function to have an extra argument that prevents running the function right away.
pass
def timeout(sec, autorun=True):
def wrapper(func):
from multiprocessing import Process
process = Process(target=func)
process.start()
process.join(sec)
if process.is_alive():
test.fail('Exceeded time limit of {:.3f} seconds'.format(sec))
process.terminate()
process.join()
def wrapper2(func):
def wrapped(*args, **kwargs):
from multiprocessing import Process
process = Process(target=lambda: func(*args, **kwargs))
process.start()
process.join(sec)
if process.is_alive():
test.fail('Exceeded time limit of {:.3f} seconds'.format(sec))
process.terminate()
process.join()
return wrapped
return wrapper if autorun else wrapper2
@timeout(0.1)
def f():
for r in range(10**9): pass
test.pass_()
@timeout(0.1, autorun=False)
def f2(n):
for r in range(n): pass
test.pass_()
f2(10**6)
f2(10**9)
@test.it('test')
@timeout(0.1, autorun=False)
def fx():
for r in range(10**9): pass
test.pass_()
def f3(testname, t, n):
@test.it(testname)
@timeout(t, autorun=False)
def sample_it():
for r in range(n): pass
test.pass_()
f3('test1', 0.01, 10**3)
f3('test2', 0.1, 10**9)
This tries to address the problem observed in this Kata which tries to wrap the user function in the tests.
def replicate(times, num):
if times <= 0: return []
return [num] + replicate(times-1, num)
from solution import *
@test.describe('Replicate')
def fixture():
def _ref(times, num):
return [num] * times
@test.it('Basic Tests')
def basic():
data = [(3, 5), (-1, 2), (5, 10), (0, 4)]
for t, n in data:
test.assert_equals(replicate(t, n), _ref(t, n))
def counts(f):
def wrapped(*args, **kwargs):
wrapped.calls += 1
return f(*args, **kwargs)
wrapped.calls = 0
return wrapped
#@test.it('Recursion Tests')
def recurse():
import sys
r = sys.modules['solution'].replicate = counts(sys.modules['solution'].replicate)
data = [(3, 5), (5, 10), (-1, 2)]
for t, n in data:
r.calls = 0
test.assert_equals(r(t, n), _ref(t, n))
if t <= 0: t = 1
test.expect(t <= r.calls <= t + 1)
print(r.calls)
#or...
@test.it('Recursion Tests')
def recurse2():
import solution
r = solution.replicate = counts(solution.replicate)
data = [(3, 5), (5, 10), (-1, 2)]
for t, n in data:
r.calls = 0
test.assert_equals(r(t, n), _ref(t, n))
if t <= 0: t = 1
test.expect(t <= r.calls <= t + 1)
print(r.calls)
More testing on expect_error
. By the nature of try..except
statement, it is possible to specify multiple exception types to expect.
@test.describe('expect_error is backwards compatible') def d1(): for f in (f0, f1, f2, f3): test.expect_error('Should raise something', f) excn = ('Exception', 'ArithmeticError', 'ZeroDivisionError', 'LookupError', 'KeyError', 'OSError') exc = (Exception, ArithmeticError, ZeroDivisionError, LookupError, KeyError, OSError) @test.describe('expect_error with exception class') def d2(): @test.it('f0 raises nothing') def i0(): test.expect_error('f0 did not raise any exception', f0) for i in range(6): test.expect_error('f0 did not raise {}'.format(excn[i]), f0, exc[i]) @test.it('f1 raises Exception') def i1(): test.expect_error('f1 did not raise Exception', f1) for i in range(6): test.expect_error('f1 did not raise {}'.format(excn[i]), f1, exc[i]) @test.it('f2 raises Exception >> ArithmeticError >> ZeroDivisionError') def i2(): test.expect_error('f2 did not raise Exception', f2) for i in range(6): test.expect_error('f2 did not raise {}'.format(excn[i]), f2, exc[i]) @test.it('f3 raises Exception >> LookupError >> KeyError') def i3(): test.expect_error('f3 did not raise Exception', f3) for i in range(6): test.expect_error('f3 did not raise {}'.format(excn[i]), f3, exc[i]) @test.it('Testing with multiple exception types') def i4(): test.expect_error('f2 did not raise one of ArithmeticError or KeyError', f2, (ArithmeticError, KeyError)) test.expect_error('f3 did not raise one of LookupError or OSError', f3, (LookupError, OSError)) test.expect_error('f0 did not raise one of LookupError or OSError', f0, (LookupError, OSError))
- @test.describe('expect_error is backwards compatible')
- def d1():
- for f in (f0, f1, f2, f3):
- test.expect_error('Should raise something', f)
- excn = ('Exception', 'ArithmeticError', 'ZeroDivisionError', 'LookupError', 'KeyError', 'OSError')
- exc = (Exception, ArithmeticError, ZeroDivisionError, LookupError, KeyError, OSError)
- @test.describe('expect_error with exception class')
- def d2():
- @test.it('f0 raises nothing')
- def i0():
- test.expect_error('f0 did not raise any exception', f0)
- for i in range(6):
- test.expect_error('f0 did not raise {}'.format(excn[i]), f0, exc[i])
- @test.it('f1 raises Exception')
- def i1():
- test.expect_error('f1 did not raise Exception', f1)
- for i in range(6):
- test.expect_error('f1 did not raise {}'.format(excn[i]), f1, exc[i])
- @test.it('f2 raises Exception >> ArithmeticError >> ZeroDivisionError')
- def i2():
- test.expect_error('f2 did not raise Exception', f2)
- for i in range(6):
- test.expect_error('f2 did not raise {}'.format(excn[i]), f2, exc[i])
- @test.it('f3 raises Exception >> LookupError >> KeyError')
- def i3():
- test.expect_error('f3 did not raise Exception', f3)
- for i in range(6):
- test.expect_error('f3 did not raise {}'.format(excn[i]), f3, exc[i])
- @test.it('Testing with multiple exception types')
- def i4():
- test.expect_error('f2 did not raise one of ArithmeticError or KeyError',
- f2, (ArithmeticError, KeyError))
- test.expect_error('f3 did not raise one of LookupError or OSError',
- f3, (LookupError, OSError))
- test.expect_error('f0 did not raise one of LookupError or OSError',
- f0, (LookupError, OSError))
This Kumite is a showcase for further possible improvements of Python testing framework cw-2.py
, which has been going through major changes. You can see already implemented features here (on Preview site).
Proposal
Improved expect_error()
, idea by Blind4Basics
As-is:
- Test if the function throws an error. It doesn't matter which exception it exactly throws.
To-be:
- Test if the function throws an error, which is an instance of the given
Exception
class.
# BaseException >> Exception
def f1(): raise Exception()
# BaseException >> Exception >> ArithmeticError >> ZeroDivisionError
def f2(): return 1 // 0
# BaseException >> Exception >> LookupError >> KeyError
def f3(): return {}[1]
def f0(): pass
@test.describe('expect_error, previous version')
def d1():
for f in (f0, f1, f2, f3):
test.expect_error('Should raise something', f)
def expect_error(message, function, class_=Exception):
passed = False
try: function()
except class_: passed = True
except: pass
test.expect(passed, message)
excn = ('Exception', 'ArithmeticError', 'ZeroDivisionError', 'LookupError', 'KeyError', 'OSError')
exc = (Exception, ArithmeticError, ZeroDivisionError, LookupError, KeyError, OSError)
@test.describe('expect_error, new version')
def d2():
@test.it('f0 raises nothing')
def i0():
expect_error('f0 did not raise any exception', f0)
for i in range(6):
expect_error('f0 did not raise {}'.format(excn[i]), f0, exc[i])
@test.it('f1 raises Exception')
def i1():
expect_error('f1 did not raise Exception', f1)
for i in range(6):
expect_error('f1 did not raise {}'.format(excn[i]), f1, exc[i])
@test.it('f2 raises Exception >> ArithmeticError >> ZeroDivisionError')
def i2():
expect_error('f2 did not raise Exception', f2)
for i in range(6):
expect_error('f2 did not raise {}'.format(excn[i]), f2, exc[i])
@test.it('f3 raises Exception >> LookupError >> KeyError')
def i3():
expect_error('f3 did not raise Exception', f3)
for i in range(6):
expect_error('f3 did not raise {}'.format(excn[i]), f3, exc[i])
This kumite illustrates some of the new features of the Python test framework (a.k.a. cw-2.py
) through a simple kata format.
Example Task
Compute the sum of two integers.
Constraints
-10**9 <= a, b <= 10**9
Example Solutions
-
add
: Correct solution -
add1
: Incorrect solution -
add2
: Solution that timeouts -
add3
: Solution that cheats -
add4
: Solution that throws error (see how the error is formatted; files and lines are shown properly now)
You can uncomment each line at the bottom of Code
section to see how the tests combat the invalid solutions in different ways.
Extra Things in the code section
- You can use and print Unicode characters without error!
Example Test Fixture
@Test.describe(name)
@Test.it(name)
@Test.timeout(sec)
-
Test.assert_equals
andTest.assert_not_equals
are non-blocking by default -
Test.assert_not_equals
can now prevent cheating with magic methods
For more complete list of features and guide on how to write a test fixture, consult this page on GitHub Wiki.
# Correct solution def add(a, b): return a + b # Incorrect solution def add1(a, b): return a | b # Solution that timeouts def add2(a, b): while a: a, b = a-1, b+1 return b # Solution that cheats def add3(a, b): class X(object): def __eq__(self, other): return True def __ne__(self, other): return True def __gt__(self, other): return True def __lt__(self, other): return True def __ge__(self, other): return True def __le__(self, other): return True return X() # Solution that throws error def add4(a, b): if a > 10: raise NotImplementedError() return a + b #add = add1 #add = add2 #add = add3 #add = add4 # Other things to demonstrate print('가나다')
- # Correct solution
- def add(a, b): return a + b
- # Incorrect solution
- def add1(a, b): return a | b
- # Solution that timeouts
- def add2(a, b):
- while a: a, b = a-1, b+1
- return b
- # Solution that cheats
- def add3(a, b):
- class X(object):
- def __eq__(self, other): return True
- def __ne__(self, other): return True
- def __gt__(self, other): return True
- def __lt__(self, other): return True
- def __ge__(self, other): return True
- def __le__(self, other): return True
- return X()
- # Solution that throws error
- def add4(a, b):
- if a > 10: raise NotImplementedError()
- return a + b
- #add = add1
- #add = add2
#add = add3- #add = add3
- #add = add4
- # Other things to demonstrate
- print('가나다')
Please go to same kumite on the preview site to run these tests correctly :)
Example Task
Compute the sum of two integers.
Constraints
-10**9 <= a, b <= 10**9
Example Solutions
-
add
: Correct solution -
add1
: Incorrect solution -
add2
: Solution that timeouts -
add3
: Solution that cheats
You can uncomment each line at the bottom of Code
section to see how the tests combat the invalid solutions in different ways.
Example Test Fixture
@Test.describe(name)
@Test.it(name)
@Test.timeout(sec)
-
Test.assert_equals
andTest.assert_not_equals
are non-blocking by default -
Test.assert_not_equals
can now prevent cheating with magic methods
For more complete list of features and guide on how to write a test fixture, consult this page on GitHub Wiki.
# Correct solution
def add(a, b): return a + b
# Incorrect solution
def add1(a, b): return a | b
# Solution that timeouts
def add2(a, b):
while a: a, b = a-1, b+1
return b
# Solution that cheats
def add3(a, b):
class X(object):
def __eq__(self, other): return True
def __ne__(self, other): return True
def __gt__(self, other): return True
def __lt__(self, other): return True
def __ge__(self, other): return True
def __le__(self, other): return True
return X()
#add = add1
#add = add2
#add = add3
@Test.describe('Add two integers')
def _describe():
def test_one(a, b, c=None):
if c is None: c = a + b
@Test.timeout(0.01)
def do():
Test.assert_equals(add(a, b), c)
Test.assert_not_equals(add(a, b), c+1)
@Test.it('Basic tests')
def _it1():
test_cases = [(0, 0, 0), (1, 2, 3), (-1, -2, -3), (10**9, 10**9, 2 * 10**9), (-10**9, 10**9, 0)]
for a, b, c in test_cases:
test_one(a, b, c)
@Test.it('Random tests')
def _it2():
from random import randint
def r(): return randint(-10**9, 10**9)
for _ in range(100):
test_one(r(), r())
Outline
This is a proposal for CodeWars test framework for Python to improve it in many ways (and be more consistent with other languages' frameworks).
Unlike my previous Kumite, this one is designed to completely replace cw-2.py
in the runner repo.
Changes / Improvements
Individual Testing / Logging Functions
- Make testing functions non-blocking
- Change the expression inside
assert_not_equals
so that it can prevent operator injection hacks - Provide a way to log and test Unicode strings without Unicode-related error
- Provide a utility for timeout
- Provide
pass
,fail
andassert_approx_equals
Describe / It
- Build the decorator version of
describe
andit
, so the text fixture may look like this (and the decorator itself runs the code, so it doesn't need a separate runner function):
@describe('describe-text')
def describe1():
@it('it-text', before=f1, after=f2)
def it1():
# some test function
@it('it-text')
def it2():
# some test function
- Properly close
describe
andit
blocks in the test output - Print the running times of
describe
andit
blocks - Provide
before
andafter
fordescribe
andit
Changelog
v1.1
- Replace the timeout utility with decorator version.
v1.2
- Make the whole code (including fixture examples) compatible with Python 2 using
six
.
from __future__ import print_function import re, six range = six.moves.range class AssertException(Exception): pass '''Fix the dreaded Unicode Error Trap''' def uni_print(*args, **kwargs): from sys import stdout sep = kwargs.get('sep', ' ') end = kwargs.get('end', '\n') file = kwargs.get('file', stdout) def _replace(c): if ord(c) >= 128: return u'&#{};'.format(ord(c)) return c def _escape(s): escaped = ''.join(_replace(c) for c in six.text_type(s)) escaped = re.sub(r'\\u([\da-f]{4})', lambda m: '&#{};'.format(int(m.group(1), 16)), escaped) escaped = re.sub(r'\\U([\da-f]{8})', lambda m: '&#{};'.format(int(m.group(1), 16)), escaped) return escaped six.print_(*map(_escape, args), sep=_escape(sep), end=_escape(end), file=file) def format_message(message): def _replace(c): if ord(c) >= 65536: return r'\U' + hex(ord(c))[2:].zfill(8) if ord(c) >= 128: return r'\u' + hex(ord(c))[2:].zfill(4) return c def _escape(s): return ''.join(_replace(c) for c in s) return _escape(message.replace("\n", "<:LF:>")) def display(type, message, label="", mode=""): print("\n<{0}:{1}:{2}>{3}".format(type.upper(), mode.upper(), label, format_message(message))) def expect(passed=None, message=None, allow_raise=False): if passed: display('PASSED', 'Test Passed') else: message = message or "Value is not what was expected" display('FAILED', message) if allow_raise: raise AssertException(message) def assert_equals(actual, expected, message=None, allow_raise=False): equals_msg = "{0} should equal {1}".format(repr(actual), repr(expected)) if message is None: message = equals_msg else: message += ": " + equals_msg expect(actual == expected, message, allow_raise) def assert_not_equals(actual, expected, message=None, allow_raise=False): equals_msg = "{0} should not equal {1}".format(repr(actual), repr(expected)) if message is None: message = equals_msg else: message += ": " + equals_msg expect(not (actual == expected), message, allow_raise) def expect_error(message, function): passed = False try: function() except: passed = True expect(passed, message) def pass_(): expect(True) def fail(message): expect(False, message) def assert_approx_equals(actual, expected, margin=1e-9, message=None, allow_raise=False): equals_msg = "{0} should be close to {1} with absolute or relative margin of {2}".format( repr(actual), repr(expected), repr(margin)) if message is None: message = equals_msg else: message += ": " + equals_msg div = max(abs(actual), abs(expected), 1) expect(abs((actual - expected) / div) < margin, message, allow_raise) ''' Usage: @describe('describe text') def describe1(): @it('it text') def it1(): # some test cases... ''' def _timed_block_factory(opening_text): from timeit import default_timer as timer from traceback import format_exception from sys import exc_info def _timed_block_decorator(s, before=None, after=None): display(opening_text, s) def wrapper(func): if callable(before): before() time = timer() try: func() except: fail('Unexpected exception raised') tb_str = ''.join(format_exception(*exc_info())) display('ERROR', tb_str) display('COMPLETEDIN', '{:.2f}'.format((timer() - time) * 1000)) if callable(after): after() return wrapper return _timed_block_decorator describe = _timed_block_factory('DESCRIBE') it = _timed_block_factory('IT') ''' Timeout utility Usage: @timeout(sec) def some_tests(): any code block... Note: Timeout value can be a float. ''' def timeout(sec): def wrapper(func): from multiprocessing import Process process = Process(target=func) process.start() process.join(sec) if process.is_alive(): fail('Exceeded time limit of {:.3f} seconds'.format(sec)) process.terminate() process.join() return wrapper '''Old-style Fixture''' describe('Old-style Describe') it('Old-style It') assert_equals(0, 0) assert_equals(0, 1) print('<COMPLETEDIN::>') it('Old-style It 2') assert_equals('a', 'a') assert_equals('a', 'b') print('<COMPLETEDIN::>') print('<COMPLETEDIN::>') '''Sample Fixture #1''' @describe('Sample Fixture #1') def sample_describe_1(): @it('Sample Testcase #1-1') def sample_it_1(): assert_equals(0, 0) assert_equals(0, 1) assert_not_equals(0, 2) pass_() fail('This should fail') @it('Sample Testcase #1-2') def sample_it_2(): expect_error('ZeroDivisionError', lambda: 0 / 0) assert_equals(0, 0 / 0) assert_equals(1, 1, 'This is not run due to exception') @it('Sample Testcase #1-3') def sample_it_3(): assert_equals('abc', 'abc') # source code doesn't support utf-8 chars, but you can at least log and test unicode assert_equals(u'\uac00 \ub098 \ub2e4', u'\uac00 \ub098 \ub2e4') uni_print(1, 'a', u'\uac00 \ub098 \ub2e4', [2, 'b', u'\uac00']) assert_equals(u'\uac00 \ub098 \ub2e4', 'a b c') '''Sample Fixture #2: Featuring Before and After''' @describe('Sample Fixture #2') def sample_describe_2(): a = {0} def before(): a.add(len(a)) @it('Sample Testcase #2-1', before=before, after=before) def sample_it_1(): assert_equals(a, {0, 1}) @it('Sample Testcase #2-2') def sample_it_2(): assert_equals(a, {0, 1, 2}) '''Sample Fixture #3: Featuring Timeout''' @describe('Sample Fixture #3') def sample_describe_3(): @it('Sample Testcase #3-1') def sample_it_1(): @timeout(0.01) def count(): for _ in range(100): pass pass_() @it('Sample Testcase #3-2') def sample_it_2(): @timeout(0.01) def count(): for _ in range(10**10): pass pass_() '''Sample Fixture #4: Featuring assert_approx_equals''' @describe('Sample Fixture #4') def sample_describe_4(): @it('Sample Testcase #4-1') def sample_it_1(): assert_approx_equals(1, 1 + 1e-10, 1e-9) assert_approx_equals(1, 1 + 1e-7, 1e-9) assert_approx_equals(-1, -1 - 1e-10, 1e-9) assert_approx_equals(-1, -1 - 1e-7, 1e-9) @it('Sample Testcase #4-2') def sample_it_2(): assert_approx_equals(0, 1e-10, 1e-9) assert_approx_equals(0, 1e-7, 1e-9) assert_approx_equals(0, -1e-10, 1e-9) assert_approx_equals(0, -1e-7, 1e-9)
#from __future__ import print_function- from __future__ import print_function
- import re, six
- range = six.moves.range
- class AssertException(Exception):
- pass
- '''Fix the dreaded Unicode Error Trap'''
_print = printdef print(*args, sep=' ', end=''):from io import StringIOdef _escape(s): return s.encode('ascii', 'xmlcharrefreplace').decode('ascii')sio = StringIO()_print(*args, sep=sep, end=end, file=sio)_print(_escape(sio.getvalue()))sio.close()- def uni_print(*args, **kwargs):
- from sys import stdout
- sep = kwargs.get('sep', ' ')
- end = kwargs.get('end', '
- ')
- file = kwargs.get('file', stdout)
- def _replace(c):
- if ord(c) >= 128: return u'&#{};'.format(ord(c))
- return c
- def _escape(s):
- escaped = ''.join(_replace(c) for c in six.text_type(s))
- escaped = re.sub(r'\\u([\da-f]{4})', lambda m: '&#{};'.format(int(m.group(1), 16)), escaped)
- escaped = re.sub(r'\\U([\da-f]{8})', lambda m: '&#{};'.format(int(m.group(1), 16)), escaped)
- return escaped
- six.print_(*map(_escape, args), sep=_escape(sep), end=_escape(end), file=file)
- def format_message(message):
return message.replace("", "<:LF:>")- def _replace(c):
- if ord(c) >= 65536: return r'\U' + hex(ord(c))[2:].zfill(8)
- if ord(c) >= 128: return r'\u' + hex(ord(c))[2:].zfill(4)
- return c
- def _escape(s): return ''.join(_replace(c) for c in s)
- return _escape(message.replace("
- ", "<:LF:>"))
- def display(type, message, label="", mode=""):
- print("\n<{0}:{1}:{2}>{3}".format(type.upper(), mode.upper(), label, format_message(message)))
- def expect(passed=None, message=None, allow_raise=False):
if passed: print("\n<PASSED::>Test Passed")- if passed:
- display('PASSED', 'Test Passed')
- else:
- message = message or "Value is not what was expected"
print("\n<FAILED::>{0}".format(message))if allow_raise: raise AssertException(message)- display('FAILED', message)
- if allow_raise:
- raise AssertException(message)
'''Fix the blocking asserts to non-blocking'''- def assert_equals(actual, expected, message=None, allow_raise=False):
- equals_msg = "{0} should equal {1}".format(repr(actual), repr(expected))
if message is None: message = equals_msgelse: message += ": " + equals_msg- if message is None:
- message = equals_msg
- else:
- message += ": " + equals_msg
- expect(actual == expected, message, allow_raise)
'''Fix the blocking asserts to non-blockingAlso change the expected formula from `actual != expected` to `not (actual == expected)`so that using this assertion can prevent the `==` / `!=` injection hack'''- def assert_not_equals(actual, expected, message=None, allow_raise=False):
- equals_msg = "{0} should not equal {1}".format(repr(actual), repr(expected))
if message is None: message = equals_msgelse: message += ": " + equals_msg- if message is None:
- message = equals_msg
- else:
- message += ": " + equals_msg
- expect(not (actual == expected), message, allow_raise)
- def expect_error(message, function):
- passed = False
try: function()except: passed = True- try:
- function()
- except:
- passed = True
- expect(passed, message)
'''Additional test functions: pass, fail, and assert_approx_equals'''- def pass_(): expect(True)
- def fail(message): expect(False, message)
- def assert_approx_equals(actual, expected, margin=1e-9, message=None, allow_raise=False):
- equals_msg = "{0} should be close to {1} with absolute or relative margin of {2}".format(
- repr(actual), repr(expected), repr(margin))
- if message is None: message = equals_msg
- else: message += ": " + equals_msg
- div = max(abs(actual), abs(expected), 1)
- expect(abs((actual - expected) / div) < margin, message, allow_raise)
def display(type, message, label="", mode=""):print("\n<{0}:{1}:{2}>{3}".format(type.upper(), mode.upper(), label, format_message(message)))- '''
Modern-Style Describe & It- Usage:
- @describe('describe text')
- def describe1():
- @it('it text')
- def it1():
- # some test cases...
- '''
- def _timed_block_factory(opening_text):
- from timeit import default_timer as timer
- from traceback import format_exception
- from sys import exc_info
- def _timed_block_decorator(s, before=None, after=None):
print('<{}::>{}'.format(opening_text, s))- display(opening_text, s)
- def wrapper(func):
- if callable(before): before()
- time = timer()
- try: func()
- except:
- fail('Unexpected exception raised')
tb_str = ''.join(format_exception(*exc_info())).replace('\n', '<:LF:>')print('<ERROR::>' + tb_str)print('<COMPLETEDIN::>{}'.format(round((timer() - time) * 1000, 2)))- tb_str = ''.join(format_exception(*exc_info()))
- display('ERROR', tb_str)
- display('COMPLETEDIN', '{:.2f}'.format((timer() - time) * 1000))
- if callable(after): after()
- return wrapper
- return _timed_block_decorator
- describe = _timed_block_factory('DESCRIBE')
- it = _timed_block_factory('IT')
- '''
- Timeout utility
- Usage:
with run_with_timeout(func, tuple_of_args, timeout_in_seconds) as run:Test.assert_equals(run.get(), expected_value)- @timeout(sec)
- def some_tests():
- any code block...
- Note: Timeout value can be a float.
- '''
class run_with_timeout(object):def __init__(self, func, inputs, sec):from multiprocessing import Process, Queuedef timeout_wrapper(func, inputs, q):q.put(func(*inputs))self.sec = secself.q = Queue()self.p = Process(target=timeout_wrapper, args=(func, inputs, self.q))self.result = Nonedef __enter__(self):self.p.start()return selfdef get(self):if self.result is None: self.result = self.q.get(timeout=self.sec)return self.resultdef __exit__(self, typ, val, traceback):self.q.close()self.p.terminate()self.p.join()if traceback: fail('Exceeded time limit of {:.3f} seconds'.format(self.sec))return True- def timeout(sec):
- def wrapper(func):
- from multiprocessing import Process
- process = Process(target=func)
- process.start()
- process.join(sec)
- if process.is_alive():
- fail('Exceeded time limit of {:.3f} seconds'.format(sec))
- process.terminate()
- process.join()
- return wrapper
- '''Old-style Fixture'''
- describe('Old-style Describe')
- it('Old-style It')
- assert_equals(0, 0)
- assert_equals(0, 1)
- print('<COMPLETEDIN::>')
- it('Old-style It 2')
- assert_equals('a', 'a')
- assert_equals('a', 'b')
- print('<COMPLETEDIN::>')
- print('<COMPLETEDIN::>')
- '''Sample Fixture #1'''
- @describe('Sample Fixture #1')
- def sample_describe_1():
- @it('Sample Testcase #1-1')
- def sample_it_1():
- assert_equals(0, 0)
- assert_equals(0, 1)
- assert_not_equals(0, 2)
- pass_()
- fail('This should fail')
- @it('Sample Testcase #1-2')
- def sample_it_2():
- expect_error('ZeroDivisionError', lambda: 0 / 0)
- assert_equals(0, 0 / 0)
- assert_equals(1, 1, 'This is not run due to exception')
- @it('Sample Testcase #1-3')
- def sample_it_3():
- assert_equals('abc', 'abc')
- # source code doesn't support utf-8 chars, but you can at least log and test unicode
assert_equals('\uac00 \ub098 \ub2e4', '\uac00 \ub098 \ub2e4')print('\uac00 \ub098 \ub2e4')assert_equals('\uac00 \ub098 \ub2e4', 'a b c')- assert_equals(u'\uac00 \ub098 \ub2e4', u'\uac00 \ub098 \ub2e4')
- uni_print(1, 'a', u'\uac00 \ub098 \ub2e4', [2, 'b', u'\uac00'])
- assert_equals(u'\uac00 \ub098 \ub2e4', 'a b c')
- '''Sample Fixture #2: Featuring Before and After'''
- @describe('Sample Fixture #2')
- def sample_describe_2():
- a = {0}
- def before():
- a.add(len(a))
- @it('Sample Testcase #2-1', before=before, after=before)
- def sample_it_1():
- assert_equals(a, {0, 1})
- @it('Sample Testcase #2-2')
- def sample_it_2():
- assert_equals(a, {0, 1, 2})
- '''Sample Fixture #3: Featuring Timeout'''
- @describe('Sample Fixture #3')
- def sample_describe_3():
def wait_count(n):for _ in range(n): passreturn n- @it('Sample Testcase #3-1')
- def sample_it_1():
with run_with_timeout(wait_count, (100,), 0.01) as run:assert_equals(run.get(), 100)- @timeout(0.01)
- def count():
- for _ in range(100): pass
- pass_()
- @it('Sample Testcase #3-2')
- def sample_it_2():
with run_with_timeout(wait_count, (10 ** 10,), 0.01) as run:assert_equals(run.get(), 10 ** 10)- @timeout(0.01)
- def count():
- for _ in range(10**10): pass
- pass_()
- '''Sample Fixture #4: Featuring assert_approx_equals'''
- @describe('Sample Fixture #4')
- def sample_describe_4():
- @it('Sample Testcase #4-1')
- def sample_it_1():
- assert_approx_equals(1, 1 + 1e-10, 1e-9)
- assert_approx_equals(1, 1 + 1e-7, 1e-9)
- assert_approx_equals(-1, -1 - 1e-10, 1e-9)
- assert_approx_equals(-1, -1 - 1e-7, 1e-9)
- @it('Sample Testcase #4-2')
- def sample_it_2():
- assert_approx_equals(0, 1e-10, 1e-9)
- assert_approx_equals(0, 1e-7, 1e-9)
- assert_approx_equals(0, -1e-10, 1e-9)
- assert_approx_equals(0, -1e-7, 1e-9)
Outline
This is a proposal for CodeWars test framework for Python to improve it in many ways (and be more consistent with other languages' frameworks).
Unlike my previous Kumite, this one is designed to completely replace cw-2.py
in the runner repo.
Changes / Improvements
Individual Testing / Logging Functions
- Make testing functions non-blocking
- Change the expression inside
assert_not_equals
so that it can prevent operator injection hacks - Provide a way to log and test Unicode strings without Unicode-related error
- Provide a utility for timeout
- Provide
pass
,fail
andassert_approx_equals
Describe / It
- Build the decorator version of
describe
andit
, so the text fixture may look like this (and the decorator itself runs the code, so it doesn't need a separate runner function):
@describe('describe-text')
def describe1():
@it('it-text', before=f1, after=f2)
def it1():
# some test function
@it('it-text')
def it2():
# some test function
- Properly close
describe
andit
blocks in the test output - Print the running times of
describe
andit
blocks - Provide
before
andafter
fordescribe
andit
#from __future__ import print_function
class AssertException(Exception):
pass
'''Fix the dreaded Unicode Error Trap'''
_print = print
def print(*args, sep=' ', end='\n'):
from io import StringIO
def _escape(s): return s.encode('ascii', 'xmlcharrefreplace').decode('ascii')
sio = StringIO()
_print(*args, sep=sep, end=end, file=sio)
_print(_escape(sio.getvalue()))
sio.close()
def format_message(message):
return message.replace("\n", "<:LF:>")
def expect(passed=None, message=None, allow_raise=False):
if passed: print("\n<PASSED::>Test Passed")
else:
message = message or "Value is not what was expected"
print("\n<FAILED::>{0}".format(message))
if allow_raise: raise AssertException(message)
'''Fix the blocking asserts to non-blocking'''
def assert_equals(actual, expected, message=None, allow_raise=False):
equals_msg = "{0} should equal {1}".format(repr(actual), repr(expected))
if message is None: message = equals_msg
else: message += ": " + equals_msg
expect(actual == expected, message, allow_raise)
'''
Fix the blocking asserts to non-blocking
Also change the expected formula from `actual != expected` to `not (actual == expected)`
so that using this assertion can prevent the `==` / `!=` injection hack
'''
def assert_not_equals(actual, expected, message=None, allow_raise=False):
equals_msg = "{0} should not equal {1}".format(repr(actual), repr(expected))
if message is None: message = equals_msg
else: message += ": " + equals_msg
expect(not (actual == expected), message, allow_raise)
def expect_error(message, function):
passed = False
try: function()
except: passed = True
expect(passed, message)
'''Additional test functions: pass, fail, and assert_approx_equals'''
def pass_(): expect(True)
def fail(message): expect(False, message)
def assert_approx_equals(actual, expected, margin=1e-9, message=None, allow_raise=False):
equals_msg = "{0} should be close to {1} with absolute or relative margin of {2}".format(
repr(actual), repr(expected), repr(margin))
if message is None: message = equals_msg
else: message += ": " + equals_msg
div = max(abs(actual), abs(expected), 1)
expect(abs((actual - expected) / div) < margin, message, allow_raise)
def display(type, message, label="", mode=""):
print("\n<{0}:{1}:{2}>{3}".format(type.upper(), mode.upper(), label, format_message(message)))
'''
Modern-Style Describe & It
Usage:
@describe('describe text')
def describe1():
@it('it text')
def it1():
# some test cases...
'''
def _timed_block_factory(opening_text):
from timeit import default_timer as timer
from traceback import format_exception
from sys import exc_info
def _timed_block_decorator(s, before=None, after=None):
print('<{}::>{}'.format(opening_text, s))
def wrapper(func):
if callable(before): before()
time = timer()
try: func()
except:
fail('Unexpected exception raised')
tb_str = ''.join(format_exception(*exc_info())).replace('\n', '<:LF:>')
print('<ERROR::>' + tb_str)
print('<COMPLETEDIN::>{}'.format(round((timer() - time) * 1000, 2)))
if callable(after): after()
return wrapper
return _timed_block_decorator
describe = _timed_block_factory('DESCRIBE')
it = _timed_block_factory('IT')
'''
Timeout utility
Usage:
with run_with_timeout(func, tuple_of_args, timeout_in_seconds) as run:
Test.assert_equals(run.get(), expected_value)
Note: Timeout value can be a float.
'''
class run_with_timeout(object):
def __init__(self, func, inputs, sec):
from multiprocessing import Process, Queue
def timeout_wrapper(func, inputs, q):
q.put(func(*inputs))
self.sec = sec
self.q = Queue()
self.p = Process(target=timeout_wrapper, args=(func, inputs, self.q))
self.result = None
def __enter__(self):
self.p.start()
return self
def get(self):
if self.result is None: self.result = self.q.get(timeout=self.sec)
return self.result
def __exit__(self, typ, val, traceback):
self.q.close()
self.p.terminate()
self.p.join()
if traceback: fail('Exceeded time limit of {:.3f} seconds'.format(self.sec))
return True
'''Old-style Fixture'''
describe('Old-style Describe')
it('Old-style It')
assert_equals(0, 0)
assert_equals(0, 1)
print('<COMPLETEDIN::>')
it('Old-style It 2')
assert_equals('a', 'a')
assert_equals('a', 'b')
print('<COMPLETEDIN::>')
print('<COMPLETEDIN::>')
'''Sample Fixture #1'''
@describe('Sample Fixture #1')
def sample_describe_1():
@it('Sample Testcase #1-1')
def sample_it_1():
assert_equals(0, 0)
assert_equals(0, 1)
assert_not_equals(0, 2)
pass_()
fail('This should fail')
@it('Sample Testcase #1-2')
def sample_it_2():
expect_error('ZeroDivisionError', lambda: 0 / 0)
assert_equals(0, 0 / 0)
assert_equals(1, 1, 'This is not run due to exception')
@it('Sample Testcase #1-3')
def sample_it_3():
assert_equals('abc', 'abc')
# source code doesn't support utf-8 chars, but you can at least log and test unicode
assert_equals('\uac00 \ub098 \ub2e4', '\uac00 \ub098 \ub2e4')
print('\uac00 \ub098 \ub2e4')
assert_equals('\uac00 \ub098 \ub2e4', 'a b c')
'''Sample Fixture #2: Featuring Before and After'''
@describe('Sample Fixture #2')
def sample_describe_2():
a = {0}
def before():
a.add(len(a))
@it('Sample Testcase #2-1', before=before, after=before)
def sample_it_1():
assert_equals(a, {0, 1})
@it('Sample Testcase #2-2')
def sample_it_2():
assert_equals(a, {0, 1, 2})
'''Sample Fixture #3: Featuring Timeout'''
@describe('Sample Fixture #3')
def sample_describe_3():
def wait_count(n):
for _ in range(n): pass
return n
@it('Sample Testcase #3-1')
def sample_it_1():
with run_with_timeout(wait_count, (100,), 0.01) as run:
assert_equals(run.get(), 100)
@it('Sample Testcase #3-2')
def sample_it_2():
with run_with_timeout(wait_count, (10 ** 10,), 0.01) as run:
assert_equals(run.get(), 10 ** 10)
'''Sample Fixture #4: Featuring assert_approx_equals'''
@describe('Sample Fixture #4')
def sample_describe_4():
@it('Sample Testcase #4-1')
def sample_it_1():
assert_approx_equals(1, 1 + 1e-10, 1e-9)
assert_approx_equals(1, 1 + 1e-7, 1e-9)
assert_approx_equals(-1, -1 - 1e-10, 1e-9)
assert_approx_equals(-1, -1 - 1e-7, 1e-9)
@it('Sample Testcase #4-2')
def sample_it_2():
assert_approx_equals(0, 1e-10, 1e-9)
assert_approx_equals(0, 1e-7, 1e-9)
assert_approx_equals(0, -1e-10, 1e-9)
assert_approx_equals(0, -1e-7, 1e-9)
This shows a piece of test code in Python that is improved over the default cw-2
(and consistent with other languages' test frameworks) in the following aspects:
Initial release (v1.0)
- Utilize the
with
-block to support proper indentation ofdescribe
andit
blocks in the code - Properly close
describe
andit
blocks in the test output - Print the running times of
describe
andit
blocks - Make testing functions non-blocking
v1.1
- Provide a way to log Unicode strings for users (does not work for test framework)
v1.2
- Provide
before
andafter
fordescribe
andit
- Provide a utility for timeout
- Version 1: Function version
- Version 2: Context manager version (looks cleaner)
Run the tests and see how the test output looks like.
''' Fix for the dreaded Unicode Trap (works in py3+) This does not fix the test outputs, so some revision in cw-2 is needed ''' from io import StringIO import sys _print = print def _escape(s): return s.encode('ascii', 'xmlcharrefreplace').decode('ascii') def print(*args, sep=' ', end='\n', file=sys.stdout): sio = StringIO() _print(*args, sep=sep, end=end, file=sio) _print(_escape(sio.getvalue()), file=file) sio.close() print('\uac00 \ub098 \ub2e4 <\'">\\') ''' Defining `describe` and `it` properly Allows `with`-structure, properly outputs <COMPLETEDIN::> with timing information, and also supports optional `before` and `after`. ''' from timeit import default_timer as timer from traceback import format_exception def timed_block_wrapper(func): class _wrapper(object): def __init__(self, name, before=None, after=None): self.name = name self.func = func self.timer = None self.before = before self.after = after def __enter__(self): self.func(self.name) if callable(self.before): self.before() self.timer = timer() def __exit__(self, typ, val, traceback): if traceback: test.expect(False, 'Unexpected exception raised') tb_str = ''.join(format_exception(typ, val, traceback)).replace('\n', '<:LF:>') print('<ERROR::>{}'.format(tb_str)) print('<COMPLETEDIN::>{}'.format(round((timer() - self.timer) * 1000, 2))) if callable(self.after): self.after() return True return _wrapper Test.describe = timed_block_wrapper(Test.describe) Test.it = timed_block_wrapper(Test.it) ''' Non-blocking test failures There are only four testing functions provided by CW (expect, assert_equals, assert_not_equals, expect_error), two of which are blocking functions (i.e. stops execution on failure). If you write customized test functions, you don't need this fix as long as you use Test.expect and provide helpful messages. ''' AssertException = __import__('cw-2').AssertException def test_wrapper(func): def wrapped(*args, **kwargs): try: func(*args, **kwargs) except AssertException: pass return wrapped Test.assert_equals = test_wrapper(Test.assert_equals) Test.assert_not_equals = test_wrapper(Test.assert_not_equals) ''' Timeout utility ''' from multiprocessing import Process, Queue from queue import Empty def run_with_timeout(func, inputs, sec): def timeout_wrapper(func, inputs, q): q.put(func(*inputs)) q = Queue() p = Process(target=timeout_wrapper, args=(func, inputs, q)) p.start() ret = None try: ret = q.get(timeout=sec) q.close() p.join() return ret except Empty: q.close() p.terminate() p.join() Test.expect(False, 'Exceeded time limit of {:.3f} seconds'.format(sec)) ''' Sample test fixture ''' with Test.describe('Unicode'): with Test.it('Simple'): Test.assert_equals('\uac00 \ub098 \ub2e4', 'a b c') with Test.describe('Fibonacci'): with Test.it('Simple'): Test.assert_equals(fibonacci(1), 2) Test.assert_not_equals(fibonacci(1), 2) Test.assert_not_equals(fibonacci(1), 1) Test.assert_equals(fibonacci(5), 8) Test.assert_equals(fibonacci(10), 89) with Test.it('Higher'): Test.assert_equals(fibonacci(15), 987) Test.assert_equals(fibonacci(20), 10946) Test.assert_equals(fibonacci(25), 121393) Test.assert_equals(fibonacci(30), 1346269) with Test.it('Extreme'): Test.assert_equals(fibonacci(32), 3524578) Test.expect_error('Some message', lambda: fibonacci(36)) Test.expect_error('Some other message', lambda: fibonacci(1)) Test.assert_equals(fibonacci(36), 0) with Test.describe('Before/After'): a = 0 def before(): global a a += 1 with Test.it('Simple', before=before): Test.assert_equals(a, 1) with Test.it('Another', before=before): Test.assert_equals(a, 2) with Test.describe('Timeout'): with Test.it('Simple'): actual = run_with_timeout(fibonacci, (20,), 0.01) Test.assert_equals(actual, 10946) actual = run_with_timeout(fibonacci, (25,), 0.01) Test.assert_equals(actual, 121393) class run_with_timeout2(object): def __init__(self, func, inputs, sec): def timeout_wrapper(func, inputs, q): q.put(func(*inputs)) self.sec = sec self.q = Queue() self.p = Process(target=timeout_wrapper, args=(func, inputs, self.q)) self.result = None def __enter__(self): self.p.start() return self def get(self): if self.result is None: self.result = self.q.get(timeout=self.sec) return self.result def __exit__(self, typ, val, traceback): self.q.close() self.p.terminate() self.p.join() if traceback: Test.expect(False, 'Exceeded time limit of {:.3f} seconds'.format(self.sec)) return True with Test.describe('Timeout2'): with Test.it('Simple'): with run_with_timeout2(fibonacci, (20,), 0.01) as run: Test.assert_equals(run.get(), 10946) with run_with_timeout2(fibonacci, (25,), 0.01) as run: Test.assert_equals(run.get(), 121393)
- '''
- Fix for the dreaded Unicode Trap (works in py3+)
- This does not fix the test outputs, so some revision in cw-2 is needed
- '''
- from io import StringIO
- import sys
- _print = print
- def _escape(s): return s.encode('ascii', 'xmlcharrefreplace').decode('ascii')
- def print(*args, sep=' ', end='\n', file=sys.stdout):
- sio = StringIO()
- _print(*args, sep=sep, end=end, file=sio)
- _print(_escape(sio.getvalue()), file=file)
- sio.close()
- print('\uac00 \ub098 \ub2e4 <\'">\\')
- '''
- Defining `describe` and `it` properly
- Allows `with`-structure, properly outputs <COMPLETEDIN::> with timing information,
- and also supports optional `before` and `after`.
- '''
- from timeit import default_timer as timer
- from traceback import format_exception
- def timed_block_wrapper(func):
- class _wrapper(object):
def __init__(self, name):- def __init__(self, name, before=None, after=None):
- self.name = name
- self.func = func
- self.timer = None
- self.before = before
- self.after = after
- def __enter__(self):
- self.func(self.name)
- if callable(self.before): self.before()
- self.timer = timer()
- def __exit__(self, typ, val, traceback):
- if traceback:
- test.expect(False, 'Unexpected exception raised')
- tb_str = ''.join(format_exception(typ, val, traceback)).replace('\n', '<:LF:>')
- print('<ERROR::>{}'.format(tb_str))
- print('<COMPLETEDIN::>{}'.format(round((timer() - self.timer) * 1000, 2)))
- if callable(self.after): self.after()
- return True
- return _wrapper
- Test.describe = timed_block_wrapper(Test.describe)
- Test.it = timed_block_wrapper(Test.it)
- '''
- Non-blocking test failures
- There are only four testing functions provided by CW (expect, assert_equals, assert_not_equals, expect_error),
- two of which are blocking functions (i.e. stops execution on failure).
- If you write customized test functions, you don't need this fix as long as you use Test.expect and provide helpful messages.
- '''
- AssertException = __import__('cw-2').AssertException
- def test_wrapper(func):
- def wrapped(*args, **kwargs):
- try: func(*args, **kwargs)
- except AssertException: pass
- return wrapped
- Test.assert_equals = test_wrapper(Test.assert_equals)
- Test.assert_not_equals = test_wrapper(Test.assert_not_equals)
- '''
- Timeout utility
- '''
- from multiprocessing import Process, Queue
- from queue import Empty
- def run_with_timeout(func, inputs, sec):
- def timeout_wrapper(func, inputs, q):
- q.put(func(*inputs))
- q = Queue()
- p = Process(target=timeout_wrapper, args=(func, inputs, q))
- p.start()
- ret = None
- try:
- ret = q.get(timeout=sec)
- q.close()
- p.join()
- return ret
- except Empty:
- q.close()
- p.terminate()
- p.join()
- Test.expect(False, 'Exceeded time limit of {:.3f} seconds'.format(sec))
- '''
- Sample test fixture
- '''
- with Test.describe('Unicode'):
- with Test.it('Simple'):
- Test.assert_equals('\uac00 \ub098 \ub2e4', 'a b c')
- with Test.describe('Fibonacci'):
- with Test.it('Simple'):
- Test.assert_equals(fibonacci(1), 2)
- Test.assert_not_equals(fibonacci(1), 2)
- Test.assert_not_equals(fibonacci(1), 1)
- Test.assert_equals(fibonacci(5), 8)
- Test.assert_equals(fibonacci(10), 89)
- with Test.it('Higher'):
- Test.assert_equals(fibonacci(15), 987)
- Test.assert_equals(fibonacci(20), 10946)
- Test.assert_equals(fibonacci(25), 121393)
- Test.assert_equals(fibonacci(30), 1346269)
- with Test.it('Extreme'):
- Test.assert_equals(fibonacci(32), 3524578)
- Test.expect_error('Some message', lambda: fibonacci(36))
- Test.expect_error('Some other message', lambda: fibonacci(1))
Test.assert_equals(fibonacci(36), 0)- Test.assert_equals(fibonacci(36), 0)
- with Test.describe('Before/After'):
- a = 0
- def before():
- global a
- a += 1
- with Test.it('Simple', before=before):
- Test.assert_equals(a, 1)
- with Test.it('Another', before=before):
- Test.assert_equals(a, 2)
- with Test.describe('Timeout'):
- with Test.it('Simple'):
- actual = run_with_timeout(fibonacci, (20,), 0.01)
- Test.assert_equals(actual, 10946)
- actual = run_with_timeout(fibonacci, (25,), 0.01)
- Test.assert_equals(actual, 121393)
- class run_with_timeout2(object):
- def __init__(self, func, inputs, sec):
- def timeout_wrapper(func, inputs, q):
- q.put(func(*inputs))
- self.sec = sec
- self.q = Queue()
- self.p = Process(target=timeout_wrapper, args=(func, inputs, self.q))
- self.result = None
- def __enter__(self):
- self.p.start()
- return self
- def get(self):
- if self.result is None: self.result = self.q.get(timeout=self.sec)
- return self.result
- def __exit__(self, typ, val, traceback):
- self.q.close()
- self.p.terminate()
- self.p.join()
- if traceback: Test.expect(False, 'Exceeded time limit of {:.3f} seconds'.format(self.sec))
- return True
- with Test.describe('Timeout2'):
- with Test.it('Simple'):
- with run_with_timeout2(fibonacci, (20,), 0.01) as run:
- Test.assert_equals(run.get(), 10946)
- with run_with_timeout2(fibonacci, (25,), 0.01) as run:
- Test.assert_equals(run.get(), 121393)
This shows a piece of test code in Python that is improved over the default cw-2
(and consistent with other languages' test frameworks) in the following aspects:
Initial release (v1.0)
- Utilize the
with
-block to support proper indentation ofdescribe
andit
blocks in the code - Properly close
describe
andit
blocks in the test output - Print the running times of
describe
andit
blocks - Make testing functions non-blocking
v1.1
- Provide a way to log Unicode strings for users (does not work for test framework)
Run the tests and see how the test output looks like.
def fibonacci(n):
if n <= 1: return 1
if n > 35: raise RuntimeError('Too large input')
return fibonacci(n-1) + fibonacci(n-2)
'''
Fix for the dreaded Unicode Trap (works in py3+)
This does not fix the test outputs, so some revision in cw-2 is needed
'''
from io import StringIO
import sys
_print = print
def _escape(s): return s.encode('ascii', 'xmlcharrefreplace').decode('ascii')
def print(*args, sep=' ', end='\n', file=sys.stdout):
sio = StringIO()
_print(*args, sep=sep, end=end, file=sio)
_print(_escape(sio.getvalue()), file=file)
sio.close()
print('\uac00 \ub098 \ub2e4 <\'">\\')
'''
Defining `describe` and `it` properly
'''
from timeit import default_timer as timer
from traceback import format_exception
def timed_block_wrapper(func):
class _wrapper(object):
def __init__(self, name):
self.name = name
self.func = func
self.timer = None
def __enter__(self):
self.func(self.name)
self.timer = timer()
def __exit__(self, typ, val, traceback):
if traceback:
test.expect(False, 'Unexpected exception raised')
tb_str = ''.join(format_exception(typ, val, traceback)).replace('\n', '<:LF:>')
print('<ERROR::>{}'.format(tb_str))
print('<COMPLETEDIN::>{}'.format(round((timer() - self.timer) * 1000, 2)))
return True
return _wrapper
Test.describe = timed_block_wrapper(Test.describe)
Test.it = timed_block_wrapper(Test.it)
'''
Non-blocking test failures
There are only four testing functions provided by CW (expect, assert_equals, assert_not_equals, expect_error),
two of which are blocking functions (i.e. stops execution on failure).
If you write customized test functions, you don't need this fix as long as you use Test.expect and provide helpful messages.
'''
AssertException = __import__('cw-2').AssertException
def test_wrapper(func):
def wrapped(*args, **kwargs):
try: func(*args, **kwargs)
except AssertException: pass
return wrapped
Test.assert_equals = test_wrapper(Test.assert_equals)
Test.assert_not_equals = test_wrapper(Test.assert_not_equals)
'''
Sample test fixture
'''
with Test.describe('Unicode'):
with Test.it('Simple'):
Test.assert_equals('\uac00 \ub098 \ub2e4', 'a b c')
with Test.describe('Fibonacci'):
with Test.it('Simple'):
Test.assert_equals(fibonacci(1), 2)
Test.assert_not_equals(fibonacci(1), 2)
Test.assert_not_equals(fibonacci(1), 1)
Test.assert_equals(fibonacci(5), 8)
Test.assert_equals(fibonacci(10), 89)
with Test.it('Higher'):
Test.assert_equals(fibonacci(15), 987)
Test.assert_equals(fibonacci(20), 10946)
Test.assert_equals(fibonacci(30), 1346269)
with Test.it('Extreme'):
Test.assert_equals(fibonacci(32), 3524578)
Test.expect_error('Some message', lambda: fibonacci(36))
Test.expect_error('Some other message', lambda: fibonacci(1))
Test.assert_equals(fibonacci(36), 0)