From 10edfa65dcba82b5f4f9af55c467c2bc33e52683 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sat, 7 Dec 2013 20:55:17 +0100 Subject: [PATCH] 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. --- CHANGELOG | 9 +++++++ _pytest/python.py | 51 +++++++++++++++++++++----------------- testing/python/fixture.py | 28 +++++++++++++++++++++ testing/python/metafunc.py | 2 ++ 4 files changed, 67 insertions(+), 23 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5487b5b91..a684035dc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,15 @@ Unreleased hook and expecting certain fixture instances are torn down within (very 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 with repeated same values (sometimes useful to to test if calling a second time works as with the first time). diff --git a/_pytest/python.py b/_pytest/python.py index dd818859e..49f4f8ccb 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1826,41 +1826,45 @@ def getfuncargnames(function, startindex=None): # setups and teardowns def reorder_items(items, ignore, cache, scopenum): - if scopenum >= scopenum_function: + if scopenum >= scopenum_function or len(items) < 3: return items - if len(items) < 2: - return items - #print "\nparametrize_Sorted", items, ignore, cache, scopenum + items_done = [] + while 1: + 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 # and which we haven't seen yet. We slice the input items list into - # a list of items_nomatch, items_using_same_fixtureinstance and - # items_remaining + # a list of items_nomatch, items_same and items_other slicing_argkey = None for i, item in enumerate(items): argkeys = get_parametrized_fixture_keys(item, ignore, scopenum, cache) if slicing_argkey is None: if argkeys: slicing_argkey = argkeys.pop() - items_using_same_fixtureinstance = [item] - items_nomatch = items[:i] - items_remaining = [] + items_before = items[:i] + items_same = [item] + items_other = [] continue if slicing_argkey in argkeys: - items_using_same_fixtureinstance.append(item) + items_same.append(item) else: - items_remaining.append(item) - - if slicing_argkey is None or len(items_using_same_fixtureinstance) == 1: - # nothing to sort on this level - return reorder_items(items, ignore, cache, scopenum+1) - - items_nomatch = reorder_items(items_nomatch, ignore, cache, scopenum+1) + items_other.append(item) + if slicing_argkey is None: + return items, None, None, None newignore = ignore.copy() newignore.add(slicing_argkey) - part2 = reorder_items(items_using_same_fixtureinstance + items_remaining, - newignore, cache, scopenum) - return items_nomatch + part2 + return (items_before, items_same, items_other, newignore) def get_parametrized_fixture_keys(item, ignore, scopenum, cache): """ 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 key = (argname, param_index, item.fspath) elif scopenum == 2: # class + # enumerate classes per fspath l = cache.setdefault(item.fspath, []) try: - i = l.index(item.cls) + numclass = l.index(item.cls) except ValueError: - i = len(l) + numclass = len(l) l.append(item.cls) - key = (argname, param_index, item.fspath, i) + key = (argname, param_index, item.fspath, item.cls) if key not in ignore: keys.add(key) return keys diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 1c0bfad11..ac6258fea 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1889,6 +1889,34 @@ class TestFixtureMarker: reprec = testdir.inline_run("-lvs") 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): testdir.makepyfile(""" import pytest diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index b0804220d..dc37a03a6 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -592,6 +592,8 @@ class TestMetafuncFunctional: def test_it(foo): pass + def test_it2(foo): + pass """) reprec = testdir.inline_run("--collect-only") assert not reprec.getcalls("pytest_internalerror")