nose_parametrized.py
238 lines
| 8.1 KiB
| text/x-python
|
PythonLexer
r2465 | import re | |||
import new | ||||
import inspect | ||||
import logging | ||||
import logging.handlers | ||||
from functools import wraps | ||||
from nose.tools import nottest | ||||
from unittest import TestCase | ||||
def _terrible_magic_get_defining_classes(): | ||||
""" Returns the set of parent classes of the class currently being defined. | ||||
Will likely only work if called from the ``parameterized`` decorator. | ||||
This function is entirely @brandon_rhodes's fault, as he suggested | ||||
the implementation: http://stackoverflow.com/a/8793684/71522 | ||||
""" | ||||
stack = inspect.stack() | ||||
if len(stack) <= 4: | ||||
return [] | ||||
frame = stack[3] | ||||
code_context = frame[4][0].strip() | ||||
if not code_context.startswith("class "): | ||||
return [] | ||||
_, parents = code_context.split("(", 1) | ||||
parents, _ = parents.rsplit(")", 1) | ||||
return eval("[" + parents + "]", frame[0].f_globals, frame[0].f_locals) | ||||
def parameterized(input): | ||||
""" Parameterize a test case: | ||||
>>> add1_tests = [(1, 2), (2, 3)] | ||||
>>> class TestFoo(object): | ||||
... @parameterized(add1_tests) | ||||
... def test_add1(self, input, expected): | ||||
... assert_equal(add1(input), expected) | ||||
>>> @parameterized(add1_tests) | ||||
... def test_add1(input, expected): | ||||
... assert_equal(add1(input), expected) | ||||
>>> | ||||
""" | ||||
if not hasattr(input, "__iter__"): | ||||
raise ValueError("expected iterable input; got %r" % (input,)) | ||||
def parameterized_helper(f): | ||||
attached_instance_method = [False] | ||||
parent_classes = _terrible_magic_get_defining_classes() | ||||
if any(issubclass(cls, TestCase) for cls in parent_classes): | ||||
raise Exception("Warning: '@parameterized' tests won't work " | ||||
"inside subclasses of 'TestCase' - use " | ||||
"'@parameterized.expand' instead") | ||||
@wraps(f) | ||||
def parameterized_helper_method(self=None): | ||||
if self is not None and not attached_instance_method[0]: | ||||
# confusingly, we need to create a named instance method and | ||||
# attach that to the class... | ||||
cls = self.__class__ | ||||
im_f = new.instancemethod(f, None, cls) | ||||
setattr(cls, f.__name__, im_f) | ||||
attached_instance_method[0] = True | ||||
for args in input: | ||||
if isinstance(args, basestring): | ||||
args = [args] | ||||
# ... then pull that named instance method off, turning it into | ||||
# a bound method ... | ||||
if self is not None: | ||||
args = [getattr(self, f.__name__)] + list(args) | ||||
else: | ||||
args = [f] + list(args) | ||||
# ... then yield that as a tuple. If those steps aren't | ||||
# followed precicely, Nose gets upset and doesn't run the test | ||||
# or doesn't run setup methods. | ||||
yield tuple(args) | ||||
f.__name__ = "_helper_for_%s" % (f.__name__,) | ||||
parameterized_helper_method.parameterized_input = input | ||||
parameterized_helper_method.parameterized_func = f | ||||
return parameterized_helper_method | ||||
return parameterized_helper | ||||
def to_safe_name(s): | ||||
return re.sub("[^a-zA-Z0-9_]", "", s) | ||||
def parameterized_expand_helper(func_name, func, args): | ||||
def parameterized_expand_helper_helper(self=()): | ||||
if self != (): | ||||
self = (self,) | ||||
return func(*(self + args)) | ||||
r3643 | parameterized_expand_helper_helper.__name__ = str(func_name) | |||
r2465 | return parameterized_expand_helper_helper | |||
def parameterized_expand(input): | ||||
""" A "brute force" method of parameterizing test cases. Creates new test | ||||
cases and injects them into the namespace that the wrapped function | ||||
is being defined in. Useful for parameterizing tests in subclasses | ||||
of 'UnitTest', where Nose test generators don't work. | ||||
>>> @parameterized.expand([("foo", 1, 2)]) | ||||
... def test_add1(name, input, expected): | ||||
... actual = add1(input) | ||||
... assert_equal(actual, expected) | ||||
... | ||||
>>> locals() | ||||
... 'test_add1_foo_0': <function ...> ... | ||||
>>> | ||||
""" | ||||
def parameterized_expand_wrapper(f): | ||||
stack = inspect.stack() | ||||
frame = stack[1] | ||||
frame_locals = frame[0].f_locals | ||||
base_name = f.__name__ | ||||
for num, args in enumerate(input): | ||||
name_suffix = "_%s" % (num,) | ||||
if len(args) > 0 and isinstance(args[0], basestring): | ||||
name_suffix += "_" + to_safe_name(args[0]) | ||||
name = base_name + name_suffix | ||||
new_func = parameterized_expand_helper(name, f, args) | ||||
frame_locals[name] = new_func | ||||
return nottest(f) | ||||
return parameterized_expand_wrapper | ||||
parameterized.expand = parameterized_expand | ||||
def assert_contains(haystack, needle): | ||||
if needle not in haystack: | ||||
raise AssertionError("%r not in %r" % (needle, haystack)) | ||||
def assert_not_contains(haystack, needle): | ||||
if needle in haystack: | ||||
raise AssertionError("%r in %r" % (needle, haystack)) | ||||
def imported_from_test(): | ||||
""" Returns true if it looks like this module is being imported by unittest | ||||
or nose. """ | ||||
import re | ||||
import inspect | ||||
nose_re = re.compile(r"\bnose\b") | ||||
unittest_re = re.compile(r"\bunittest2?\b") | ||||
for frame in inspect.stack(): | ||||
file = frame[1] | ||||
if nose_re.search(file) or unittest_re.search(file): | ||||
return True | ||||
return False | ||||
def assert_raises(func, exc_type, str_contains=None, repr_contains=None): | ||||
try: | ||||
func() | ||||
r2556 | except exc_type, e: | |||
r2465 | if str_contains is not None and str_contains not in str(e): | |||
raise AssertionError("%s raised, but %r does not contain %r" | ||||
% (exc_type, str(e), str_contains)) | ||||
if repr_contains is not None and repr_contains not in repr(e): | ||||
raise AssertionError("%s raised, but %r does not contain %r" | ||||
% (exc_type, repr(e), repr_contains)) | ||||
return e | ||||
else: | ||||
raise AssertionError("%s not raised" % (exc_type,)) | ||||
log_handler = None | ||||
def setup_logging(): | ||||
""" Configures a log handler which will capure log messages during a test. | ||||
The ``logged_messages`` and ``assert_no_errors_logged`` functions can be | ||||
used to make assertions about these logged messages. | ||||
For example:: | ||||
from ensi_common.testing import ( | ||||
setup_logging, teardown_logging, assert_no_errors_logged, | ||||
assert_logged, | ||||
) | ||||
class TestWidget(object): | ||||
def setup(self): | ||||
setup_logging() | ||||
def teardown(self): | ||||
assert_no_errors_logged() | ||||
teardown_logging() | ||||
def test_that_will_fail(self): | ||||
log.warning("this warning message will trigger a failure") | ||||
def test_that_will_pass(self): | ||||
log.info("but info messages are ok") | ||||
assert_logged("info messages are ok") | ||||
""" | ||||
global log_handler | ||||
if log_handler is not None: | ||||
logging.getLogger().removeHandler(log_handler) | ||||
log_handler = logging.handlers.BufferingHandler(1000) | ||||
formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s") | ||||
log_handler.setFormatter(formatter) | ||||
logging.getLogger().addHandler(log_handler) | ||||
def teardown_logging(): | ||||
global log_handler | ||||
if log_handler is not None: | ||||
logging.getLogger().removeHandler(log_handler) | ||||
log_handler = None | ||||
def logged_messages(): | ||||
assert log_handler, "setup_logging not called" | ||||
return [(log_handler.format(record), record) for record in log_handler.buffer] | ||||
def assert_no_errors_logged(): | ||||
for _, record in logged_messages(): | ||||
if record.levelno >= logging.WARNING: | ||||
# Assume that the nose log capture plugin is being used, so it will | ||||
# show the exception. | ||||
raise AssertionError("an unexpected error was logged") | ||||
def assert_logged(expected_msg_contents): | ||||
for msg, _ in logged_messages(): | ||||
if expected_msg_contents in msg: | ||||
return | ||||
raise AssertionError("no logged message contains %r" | ||||
% (expected_msg_contents,)) | ||||