Initial commit
This commit is contained in:
16
.hgignore
Normal file
16
.hgignore
Normal file
@@ -0,0 +1,16 @@
|
||||
syntax: glob
|
||||
*.class
|
||||
*.o
|
||||
*.pyc
|
||||
*.sqlite3
|
||||
*.sw[op]
|
||||
*~
|
||||
.DS_Store
|
||||
bin-debug/*
|
||||
bin-release/*
|
||||
bin/*
|
||||
tags
|
||||
*.beam
|
||||
*.dump
|
||||
env/
|
||||
*egg-info*
|
1
nose_parameterized/__init__.py
Normal file
1
nose_parameterized/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .parameterized import parameterized
|
222
nose_parameterized/parameterized.py
Normal file
222
nose_parameterized/parameterized.py
Normal file
@@ -0,0 +1,222 @@
|
||||
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()
|
||||
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': <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()
|
||||
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, ))
|
43
nose_parameterized/test.py
Normal file
43
nose_parameterized/test.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from .parameterized import parameterized
|
||||
|
||||
def assert_contains(haystack, needle):
|
||||
if needle not in haystack:
|
||||
raise AssertionError("%r not in %r" %(needle, haystack))
|
||||
|
||||
missing_tests = set([
|
||||
"test_parameterized_naked_function",
|
||||
"test_parameterized_instance_method",
|
||||
"test_parameterized_on_TestCase",
|
||||
])
|
||||
|
||||
@parameterized([(42, )])
|
||||
def test_parameterized_naked_function(foo):
|
||||
missing_tests.remove("test_parameterized_naked_function")
|
||||
|
||||
class TestParameterized(object):
|
||||
@parameterized([(42, )])
|
||||
def test_parameterized_instance_method(self, foo):
|
||||
missing_tests.remove("test_parameterized_instance_method")
|
||||
|
||||
|
||||
def test_warns_on_bad_use_of_parameterized():
|
||||
try:
|
||||
class TestTestCaseWarnsOnBadUseOfParameterized(TestCase):
|
||||
@parameterized([42])
|
||||
def test_should_throw_error(self, param):
|
||||
pass
|
||||
except Exception, e:
|
||||
assert_contains(str(e), "parameterized.expand")
|
||||
else:
|
||||
raise AssertionError("Expected exception not raised")
|
||||
|
||||
|
||||
class TestParamerizedOnTestCase(TestCase):
|
||||
@parameterized.expand([("stuff", )])
|
||||
def test_parameterized_on_TestCase(self, input):
|
||||
assert_equal(input, "stuff")
|
||||
missing_tests.remove("test_parameterized_on_TestCase")
|
18
setup.py
Normal file
18
setup.py
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
os.chdir(os.path.dirname(sys.argv[0]) or ".")
|
||||
|
||||
setup(
|
||||
name="nose-parameterized",
|
||||
version="0.1",
|
||||
url="https://github.com/wolever/nose-parameterized",
|
||||
author="David Wolever",
|
||||
author_email="david@wolever.net",
|
||||
description="Nose decorator for parameterized testing",
|
||||
packages=find_packages(),
|
||||
)
|
Reference in New Issue
Block a user