Merge branch 'pull-50'
diff --git a/.travis.yml b/.travis.yml
index f872f70..b277b4b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,25 +1,46 @@
language: python
sudo: false
-env:
- - TOXENV=py26-nose
- - TOXENV=py26-nose2
- - TOXENV=py26-pytest
- - TOXENV=py26-unit
- - TOXENV=py26-unit2
- - TOXENV=py27-nose
- - TOXENV=py27-nose2
- - TOXENV=py27-pytest
- - TOXENV=py27-unit
- - TOXENV=py27-unit2
- - TOXENV=py33-nose
- - TOXENV=py33-nose2
- - TOXENV=py33-pytest
- - TOXENV=py33-unit
- - TOXENV=py33-unit2
- - TOXENV=pypy-nose
- - TOXENV=pypy-nose2
- - TOXENV=pypy-pytest
- - TOXENV=pypy-unit
- - TOXENV=pypy-unit2
+matrix:
+ include:
+ - env: "TOXENV=py27-nose"
+ python: "2.7"
+ - env: "TOXENV=py27-nose2"
+ python: "2.7"
+ - env: "TOXENV=py27-pytest"
+ python: "2.7"
+ - env: "TOXENV=py27-unit"
+ python: "2.7"
+ - env: "TOXENV=py27-unit2"
+ python: "2.7"
+ - env: "TOXENV=py35-nose"
+ python: "3.5"
+ - env: "TOXENV=py35-nose2"
+ python: "3.5"
+ - env: "TOXENV=py35-pytest"
+ python: "3.5"
+ - env: "TOXENV=py35-unit"
+ python: "3.5"
+ - env: "TOXENV=py35-unit2"
+ python: "3.5"
+ - env: "TOXENV=py36-nose"
+ python: "3.6"
+ - env: "TOXENV=py36-nose2"
+ python: "3.6"
+ - env: "TOXENV=py36-pytest"
+ python: "3.6"
+ - env: "TOXENV=py36-unit"
+ python: "3.6"
+ - env: "TOXENV=py36-unit2"
+ python: "3.6"
+ - env: "TOXENV=pypy-nose"
+ python: "pypy"
+ - env: "TOXENV=pypy-nose2"
+ python: "pypy"
+ - env: "TOXENV=pypy-pytest"
+ python: "pypy"
+ - env: "TOXENV=pypy-unit"
+ python: "pypy"
+ - env: "TOXENV=pypy-unit2"
+ python: "pypy"
install: pip install tox
script: tox
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index b4fc834..c4628f1 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,9 +1,13 @@
-0.6.3 (2017-11-18)
- * Added parameterized_class feature. Now it supports
- parameterized for an entire test class setting new attributes
- and values.
-0.6.2 (???)
+0.7.0 (2018-12-30)
+ * Added parameterized_class feature, for parameterizing entire test
+ classes.
+
+0.6.2 (2018-03-11)
* Make sure that `setUp` and `tearDown` methods work correctly (#40)
+ * Raise a ValueError when input is empty (thanks @danielbradburn;
+ https://github.com/wolever/parameterized/pull/48)
+ * Fix the order when number of cases exceeds 10 (thanks @ntflc;
+ https://github.com/wolever/parameterized/pull/49)
0.6.1 (2017-03-21)
* Rename package from nose-parameterized to parameterized. A
diff --git a/parameterized/parameterized.py b/parameterized/parameterized.py
index a4383a7..fae361b 100644
--- a/parameterized/parameterized.py
+++ b/parameterized/parameterized.py
@@ -13,6 +13,12 @@
from unittest import TestCase
+try:
+ from unittest import SkipTest
+except ImportError:
+ class SkipTest(Exception):
+ pass
+
PY3 = sys.version_info[0] == 3
PY2 = sys.version_info[0] == 2
@@ -40,6 +46,30 @@
_param = namedtuple("param", "args kwargs")
+def skip_on_empty_helper(*a, **kw):
+ raise SkipTest("parameterized input is empty")
+
+def reapply_patches_if_need(func):
+
+ def dummy_wrapper(orgfunc):
+ @wraps(orgfunc)
+ def dummy_func(*args, **kwargs):
+ return orgfunc(*args, **kwargs)
+ return dummy_func
+
+ if hasattr(func, 'patchings'):
+ func = dummy_wrapper(func)
+ tmp_patchings = func.patchings
+ delattr(func, 'patchings')
+ for patch_obj in tmp_patchings:
+ func = patch_obj.decorate_callable(func)
+ return func
+
+def delete_patches_if_need(func):
+ if hasattr(func, 'patchings'):
+ func.patchings[:] = []
+
+
class param(_param):
""" Represents a single parameter to a test case.
@@ -283,9 +313,10 @@
assert_equal(a + b, expected)
"""
- def __init__(self, input, doc_func=None):
+ def __init__(self, input, doc_func=None, skip_on_empty=False):
self.get_input = self.input_as_callable(input)
self.doc_func = doc_func or default_doc_func
+ self.skip_on_empty = skip_on_empty
def __call__(self, test_func):
self.assert_not_in_testcase_subclass()
@@ -321,9 +352,21 @@
if test_self is not None:
delattr(test_cls, test_func.__name__)
wrapper.__doc__ = original_doc
- wrapper.parameterized_input = self.get_input()
+
+ input = self.get_input()
+ if not input:
+ if not self.skip_on_empty:
+ raise ValueError(
+ "Parameters iterable is empty (hint: use "
+ "`parameterized([], skip_on_empty=True)` to skip "
+ "this test when the input is empty)"
+ )
+ wrapper = wraps(test_func)(lambda: skip_on_empty_helper())
+
+ wrapper.parameterized_input = input
wrapper.parameterized_func = test_func
test_func.__name__ = "_parameterized_original_%s" %(test_func.__name__, )
+
return wrapper
def param_as_nose_tuple(self, test_self, func, num, p):
@@ -388,7 +431,8 @@
return [ param.from_decorator(p) for p in input_values ]
@classmethod
- def expand(cls, input, name_func=None, doc_func=None, **legacy):
+ def expand(cls, input, name_func=None, doc_func=None, skip_on_empty=False,
+ **legacy):
""" 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
@@ -425,11 +469,31 @@
frame_locals = frame[0].f_locals
paramters = cls.input_as_callable(input)()
+
+ if not paramters:
+ if not skip_on_empty:
+ raise ValueError(
+ "Parameters iterable is empty (hint: use "
+ "`parameterized.expand([], skip_on_empty=True)` to skip "
+ "this test when the input is empty)"
+ )
+ return wraps(f)(lambda: skip_on_empty_helper())
+
+ digits = len(str(len(paramters) - 1))
for num, p in enumerate(paramters):
- name = name_func(f, num, p)
- frame_locals[name] = cls.param_as_standalone_func(p, f, name)
+ name = name_func(f, "{num:0>{digits}}".format(digits=digits, num=num), p)
+ # If the original function has patches applied by 'mock.patch',
+ # re-construct all patches on the just former decoration layer
+ # of param_as_standalone_func so as not to share
+ # patch objects between new functions
+ nf = reapply_patches_if_need(f)
+ frame_locals[name] = cls.param_as_standalone_func(p, nf, name)
frame_locals[name].__doc__ = doc_func(f, num, p)
+ # Delete original patches to prevent new function from evaluating
+ # original patching object as well as re-constructed patches.
+ delete_patches_if_need(f)
+
f.__test__ = False
return parameterized_expand_wrapper
diff --git a/parameterized/test.py b/parameterized/test.py
index a3b5e2c..7bc6b49 100644
--- a/parameterized/test.py
+++ b/parameterized/test.py
@@ -1,13 +1,13 @@
# coding=utf-8
import inspect
+import mock
from unittest import TestCase
-from nose.tools import assert_equal
-from nose.plugins.skip import SkipTest
+from nose.tools import assert_equal, assert_raises
from .parameterized import (
PY3, PY2, parameterized, param, parameterized_argument_value_pairs,
- short_repr, detect_runner, parameterized_class,
+ short_repr, detect_runner, parameterized_class, SkipTest,
)
def assert_contains(haystack, needle):
@@ -101,6 +101,74 @@
return custom_naming_func
+@mock.patch("os.getpid")
+class TestParameterizedExpandWithMockPatchForClass(TestCase):
+ expect([
+ "test_one_function_patch_decorator('foo1', 'umask', 'getpid')",
+ "test_one_function_patch_decorator('foo0', 'umask', 'getpid')",
+ "test_one_function_patch_decorator(42, 'umask', 'getpid')",
+ ])
+
+ @parameterized.expand([(42, ), "foo0", param("foo1")])
+ @mock.patch("os.umask")
+ def test_one_function_patch_decorator(self, foo, mock_umask, mock_getpid):
+ missing_tests.remove("test_one_function_patch_decorator(%r, %r, %r)" %
+ (foo, mock_umask._mock_name,
+ mock_getpid._mock_name))
+
+ expect([
+ "test_multiple_function_patch_decorator"
+ "(42, 51, 'umask', 'fdopen', 'getpid')",
+ "test_multiple_function_patch_decorator"
+ "('foo0', 'bar0', 'umask', 'fdopen', 'getpid')",
+ "test_multiple_function_patch_decorator"
+ "('foo1', 'bar1', 'umask', 'fdopen', 'getpid')",
+ ])
+
+ @parameterized.expand([(42, 51), ("foo0", "bar0"), param("foo1", "bar1")])
+ @mock.patch("os.fdopen")
+ @mock.patch("os.umask")
+ def test_multiple_function_patch_decorator(self, foo, bar, mock_umask,
+ mock_fdopen, mock_getpid):
+ missing_tests.remove("test_multiple_function_patch_decorator"
+ "(%r, %r, %r, %r, %r)" %
+ (foo, bar, mock_umask._mock_name,
+ mock_fdopen._mock_name, mock_getpid._mock_name))
+
+
+class TestParameterizedExpandWithNoMockPatchForClass(TestCase):
+ expect([
+ "test_one_function_patch_decorator('foo1', 'umask')",
+ "test_one_function_patch_decorator('foo0', 'umask')",
+ "test_one_function_patch_decorator(42, 'umask')",
+ ])
+
+ @parameterized.expand([(42, ), "foo0", param("foo1")])
+ @mock.patch("os.umask")
+ def test_one_function_patch_decorator(self, foo, mock_umask):
+ missing_tests.remove("test_one_function_patch_decorator(%r, %r)" %
+ (foo, mock_umask._mock_name))
+
+ expect([
+ "test_multiple_function_patch_decorator"
+ "(42, 51, 'umask', 'fdopen')",
+ "test_multiple_function_patch_decorator"
+ "('foo0', 'bar0', 'umask', 'fdopen')",
+ "test_multiple_function_patch_decorator"
+ "('foo1', 'bar1', 'umask', 'fdopen')",
+ ])
+
+ @parameterized.expand([(42, 51), ("foo0", "bar0"), param("foo1", "bar1")])
+ @mock.patch("os.fdopen")
+ @mock.patch("os.umask")
+ def test_multiple_function_patch_decorator(self, foo, bar, mock_umask,
+ mock_fdopen):
+ missing_tests.remove("test_multiple_function_patch_decorator"
+ "(%r, %r, %r, %r)" %
+ (foo, bar, mock_umask._mock_name,
+ mock_fdopen._mock_name))
+
+
class TestParamerizedOnTestCase(TestCase):
expect([
"test_on_TestCase('foo0', bar=None)",
@@ -214,6 +282,32 @@
else:
raise AssertionError("Expected exception not raised")
+
+def test_helpful_error_on_empty_iterable_input():
+ try:
+ parameterized([])(lambda: None)
+ except ValueError as e:
+ assert_contains(str(e), "iterable is empty")
+ else:
+ raise AssertionError("Expected exception not raised")
+
+def test_skip_test_on_empty_iterable():
+ func = parameterized([], skip_on_empty=True)(lambda: None)
+ assert_raises(SkipTest, func)
+
+
+def test_helpful_error_on_empty_iterable_input_expand():
+ try:
+ class ExpectErrorOnEmptyInput(TestCase):
+ @parameterized.expand([])
+ def test_expect_error(self):
+ pass
+ except ValueError as e:
+ assert_contains(str(e), "iterable is empty")
+ else:
+ raise AssertionError("Expected exception not raised")
+
+
expect("generator", [
"test_wrapped_iterable_input('foo')",
])
@@ -302,6 +396,13 @@
pass
+cases_over_10 = [(i, i+1) for i in range(11)]
+
+@parameterized(cases_over_10)
+def test_cases_over_10(input, expected):
+ assert_equal(input, expected-1)
+
+
@parameterized_class(("a", "b", "c"), [
("foo", 1, 2),
("bar", 3, 0),
@@ -354,4 +455,3 @@
self.foo,
self.bar,
))
-
diff --git a/tox.ini b/tox.ini
index fade720..8bceac2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,9 @@
[tox]
-envlist=py{26,27,35,36,37,py}-{nose,nose2,pytest,unit,unit2}
-
+envlist=py{27,35,36,37,py}-{nose,nose2,pytest,unit,unit2}
[testenv]
deps=
nose
+ mock
nose2: nose2
pytest: pytest>=2
unit2: unittest2