fix issue396 -- properly sort tests using class-scoped parametrization

also refix issue323 in a better way to avoid recursion for the fixture-grouping
algorithm alltogether.
This commit is contained in:
holger krekel 2013-12-07 20:55:17 +01:00
parent daec4c70b8
commit 10edfa65dc
4 changed files with 67 additions and 23 deletions

View File

@ -10,6 +10,15 @@ Unreleased
hook and expecting certain fixture instances are torn down within (very hook and expecting certain fixture instances are torn down within (very
unlikely and would have been unreliable anyway). unlikely and would have been unreliable anyway).
- fix issue396 - correctly sort and finalize class-scoped parametrized
tests independently from number of methods on the class.
- refix issue323 in a better way -- parametrization should now never
cause Runtime Recursion errors because the underlying algorithm
for re-ordering tests per-scope/per-fixture is not recursive
anymore (it was tail-call recursive before which could lead
to problems for more than >966 non-function scoped parameters).
- fix issue290 - there is preliminary support now for parametrizing - fix issue290 - there is preliminary support now for parametrizing
with repeated same values (sometimes useful to to test if calling with repeated same values (sometimes useful to to test if calling
a second time works as with the first time). a second time works as with the first time).

View File

@ -1826,41 +1826,45 @@ def getfuncargnames(function, startindex=None):
# setups and teardowns # setups and teardowns
def reorder_items(items, ignore, cache, scopenum): def reorder_items(items, ignore, cache, scopenum):
if scopenum >= scopenum_function: if scopenum >= scopenum_function or len(items) < 3:
return items return items
if len(items) < 2: items_done = []
return items while 1:
#print "\nparametrize_Sorted", items, ignore, cache, scopenum items_before, items_same, items_other, newignore = \
slice_items(items, ignore, cache, scopenum)
items_before = reorder_items(items_before, ignore, cache, scopenum+1)
if items_same is None:
# nothing to reorder in this scope
assert items_other is None
return items_done + items_before
items_done.extend(items_before)
items = items_same + items_other
ignore = newignore
def slice_items(items, ignore, cache, scopenum):
# we pick the first item which uses a fixture instance in the requested scope # we pick the first item which uses a fixture instance in the requested scope
# and which we haven't seen yet. We slice the input items list into # and which we haven't seen yet. We slice the input items list into
# a list of items_nomatch, items_using_same_fixtureinstance and # a list of items_nomatch, items_same and items_other
# items_remaining
slicing_argkey = None slicing_argkey = None
for i, item in enumerate(items): for i, item in enumerate(items):
argkeys = get_parametrized_fixture_keys(item, ignore, scopenum, cache) argkeys = get_parametrized_fixture_keys(item, ignore, scopenum, cache)
if slicing_argkey is None: if slicing_argkey is None:
if argkeys: if argkeys:
slicing_argkey = argkeys.pop() slicing_argkey = argkeys.pop()
items_using_same_fixtureinstance = [item] items_before = items[:i]
items_nomatch = items[:i] items_same = [item]
items_remaining = [] items_other = []
continue continue
if slicing_argkey in argkeys: if slicing_argkey in argkeys:
items_using_same_fixtureinstance.append(item) items_same.append(item)
else: else:
items_remaining.append(item) items_other.append(item)
if slicing_argkey is None:
if slicing_argkey is None or len(items_using_same_fixtureinstance) == 1: return items, None, None, None
# nothing to sort on this level
return reorder_items(items, ignore, cache, scopenum+1)
items_nomatch = reorder_items(items_nomatch, ignore, cache, scopenum+1)
newignore = ignore.copy() newignore = ignore.copy()
newignore.add(slicing_argkey) newignore.add(slicing_argkey)
part2 = reorder_items(items_using_same_fixtureinstance + items_remaining, return (items_before, items_same, items_other, newignore)
newignore, cache, scopenum)
return items_nomatch + part2
def get_parametrized_fixture_keys(item, ignore, scopenum, cache): def get_parametrized_fixture_keys(item, ignore, scopenum, cache):
""" return list of keys for all parametrized arguments which match """ return list of keys for all parametrized arguments which match
@ -1882,13 +1886,14 @@ def get_parametrized_fixture_keys(item, ignore, scopenum, cache):
elif scopenum == 1: # module elif scopenum == 1: # module
key = (argname, param_index, item.fspath) key = (argname, param_index, item.fspath)
elif scopenum == 2: # class elif scopenum == 2: # class
# enumerate classes per fspath
l = cache.setdefault(item.fspath, []) l = cache.setdefault(item.fspath, [])
try: try:
i = l.index(item.cls) numclass = l.index(item.cls)
except ValueError: except ValueError:
i = len(l) numclass = len(l)
l.append(item.cls) l.append(item.cls)
key = (argname, param_index, item.fspath, i) key = (argname, param_index, item.fspath, item.cls)
if key not in ignore: if key not in ignore:
keys.add(key) keys.add(key)
return keys return keys

View File

@ -1889,6 +1889,34 @@ class TestFixtureMarker:
reprec = testdir.inline_run("-lvs") reprec = testdir.inline_run("-lvs")
reprec.assertoutcome(passed=3) reprec.assertoutcome(passed=3)
@pytest.mark.issue396
def test_class_scope_parametrization_ordering(self, testdir):
testdir.makepyfile("""
import pytest
l = []
@pytest.fixture(params=["John", "Doe"], scope="class")
def human(request):
request.addfinalizer(lambda: l.append("fin %s" % request.param))
return request.param
class TestGreetings:
def test_hello(self, human):
l.append("test_hello")
class TestMetrics:
def test_name(self, human):
l.append("test_name")
def test_population(self, human):
l.append("test_population")
""")
reprec = testdir.inline_run()
reprec.assertoutcome(passed=6)
l = reprec.getcalls("pytest_runtest_call")[0].item.module.l
assert l == ["test_hello", "fin John", "test_hello", "fin Doe",
"test_name", "test_population", "fin John",
"test_name", "test_population", "fin Doe"]
def test_parametrize_setup_function(self, testdir): def test_parametrize_setup_function(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest

View File

@ -592,6 +592,8 @@ class TestMetafuncFunctional:
def test_it(foo): def test_it(foo):
pass pass
def test_it2(foo):
pass
""") """)
reprec = testdir.inline_run("--collect-only") reprec = testdir.inline_run("--collect-only")
assert not reprec.getcalls("pytest_internalerror") assert not reprec.getcalls("pytest_internalerror")