diff --git a/rhodecode/tests/nose_parametrized.py b/rhodecode/tests/nose_parametrized.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/nose_parametrized.py @@ -0,0 +1,238 @@ +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)) + parameterized_expand_helper_helper.__name__ = func_name + 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': ... + >>> + """ + + 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() + except exc_type as e: + 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,))