Merge pull request #3629 from egnartsms/issue-2220-param-breaks-dep
Make test parametrization override indirect fixtures
This commit is contained in:
commit
8680dfc939
1
AUTHORS
1
AUTHORS
|
@ -182,6 +182,7 @@ Ryan Wooden
|
||||||
Samuel Dion-Girardeau
|
Samuel Dion-Girardeau
|
||||||
Samuele Pedroni
|
Samuele Pedroni
|
||||||
Segev Finer
|
Segev Finer
|
||||||
|
Serhii Mozghovyi
|
||||||
Simon Gomizelj
|
Simon Gomizelj
|
||||||
Skylar Downes
|
Skylar Downes
|
||||||
Srinivas Reddy Thatiparthy
|
Srinivas Reddy Thatiparthy
|
||||||
|
|
|
@ -46,3 +46,7 @@ test_script:
|
||||||
cache:
|
cache:
|
||||||
- '%LOCALAPPDATA%\pip\cache'
|
- '%LOCALAPPDATA%\pip\cache'
|
||||||
- '%USERPROFILE%\.cache\pre-commit'
|
- '%USERPROFILE%\.cache\pre-commit'
|
||||||
|
|
||||||
|
# We don't deploy anything on tags with AppVeyor, we use Travis instead, so we
|
||||||
|
# might as well save resources
|
||||||
|
skip_tags: true
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
In case a (direct) parameter of a test overrides some fixture upon which the
|
||||||
|
test depends indirectly, do the pruning of the fixture dependency tree. That
|
||||||
|
is, recompute the full set of fixtures the test function needs.
|
|
@ -274,11 +274,43 @@ def get_direct_param_fixture_func(request):
|
||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
class FuncFixtureInfo(object):
|
class FuncFixtureInfo(object):
|
||||||
def __init__(self, argnames, names_closure, name2fixturedefs):
|
# original function argument names
|
||||||
self.argnames = argnames
|
argnames = attr.ib(type=tuple)
|
||||||
self.names_closure = names_closure
|
# argnames that function immediately requires. These include argnames +
|
||||||
self.name2fixturedefs = name2fixturedefs
|
# fixture names specified via usefixtures and via autouse=True in fixture
|
||||||
|
# definitions.
|
||||||
|
initialnames = attr.ib(type=tuple)
|
||||||
|
names_closure = attr.ib(type="List[str]")
|
||||||
|
name2fixturedefs = attr.ib(type="List[str, List[FixtureDef]]")
|
||||||
|
|
||||||
|
def prune_dependency_tree(self):
|
||||||
|
"""Recompute names_closure from initialnames and name2fixturedefs
|
||||||
|
|
||||||
|
Can only reduce names_closure, which means that the new closure will
|
||||||
|
always be a subset of the old one. The order is preserved.
|
||||||
|
|
||||||
|
This method is needed because direct parametrization may shadow some
|
||||||
|
of the fixtures that were included in the originally built dependency
|
||||||
|
tree. In this way the dependency tree can get pruned, and the closure
|
||||||
|
of argnames may get reduced.
|
||||||
|
"""
|
||||||
|
closure = set()
|
||||||
|
working_set = set(self.initialnames)
|
||||||
|
while working_set:
|
||||||
|
argname = working_set.pop()
|
||||||
|
# argname may be smth not included in the original names_closure,
|
||||||
|
# in which case we ignore it. This currently happens with pseudo
|
||||||
|
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
|
||||||
|
# So they introduce the new dependency 'request' which might have
|
||||||
|
# been missing in the original tree (closure).
|
||||||
|
if argname not in closure and argname in self.names_closure:
|
||||||
|
closure.add(argname)
|
||||||
|
if argname in self.name2fixturedefs:
|
||||||
|
working_set.update(self.name2fixturedefs[argname][-1].argnames)
|
||||||
|
|
||||||
|
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
|
||||||
|
|
||||||
|
|
||||||
class FixtureRequest(FuncargnamesCompatAttr):
|
class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
|
@ -1033,11 +1065,12 @@ class FixtureManager(object):
|
||||||
usefixtures = flatten(
|
usefixtures = flatten(
|
||||||
mark.args for mark in node.iter_markers(name="usefixtures")
|
mark.args for mark in node.iter_markers(name="usefixtures")
|
||||||
)
|
)
|
||||||
initialnames = argnames
|
initialnames = tuple(usefixtures) + argnames
|
||||||
initialnames = tuple(usefixtures) + initialnames
|
|
||||||
fm = node.session._fixturemanager
|
fm = node.session._fixturemanager
|
||||||
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames, node)
|
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
|
||||||
return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
|
initialnames, node
|
||||||
|
)
|
||||||
|
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||||
|
|
||||||
def pytest_plugin_registered(self, plugin):
|
def pytest_plugin_registered(self, plugin):
|
||||||
nodeid = None
|
nodeid = None
|
||||||
|
@ -1085,6 +1118,12 @@ class FixtureManager(object):
|
||||||
fixturenames_closure.append(arg)
|
fixturenames_closure.append(arg)
|
||||||
|
|
||||||
merge(fixturenames)
|
merge(fixturenames)
|
||||||
|
|
||||||
|
# at this point, fixturenames_closure contains what we call "initialnames",
|
||||||
|
# which is a set of fixturenames the function immediately requests. We
|
||||||
|
# need to return it as well, so save this.
|
||||||
|
initialnames = tuple(fixturenames_closure)
|
||||||
|
|
||||||
arg2fixturedefs = {}
|
arg2fixturedefs = {}
|
||||||
lastlen = -1
|
lastlen = -1
|
||||||
while lastlen != len(fixturenames_closure):
|
while lastlen != len(fixturenames_closure):
|
||||||
|
@ -1106,7 +1145,7 @@ class FixtureManager(object):
|
||||||
return fixturedefs[-1].scopenum
|
return fixturedefs[-1].scopenum
|
||||||
|
|
||||||
fixturenames_closure.sort(key=sort_by_scope)
|
fixturenames_closure.sort(key=sort_by_scope)
|
||||||
return fixturenames_closure, arg2fixturedefs
|
return initialnames, fixturenames_closure, arg2fixturedefs
|
||||||
|
|
||||||
def pytest_generate_tests(self, metafunc):
|
def pytest_generate_tests(self, metafunc):
|
||||||
for argname in metafunc.fixturenames:
|
for argname in metafunc.fixturenames:
|
||||||
|
|
|
@ -439,6 +439,11 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
|
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
|
||||||
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
||||||
|
|
||||||
|
# add_funcarg_pseudo_fixture_def may have shadowed some fixtures
|
||||||
|
# with direct parametrization, so make sure we update what the
|
||||||
|
# function really needs.
|
||||||
|
fixtureinfo.prune_dependency_tree()
|
||||||
|
|
||||||
for callspec in metafunc._calls:
|
for callspec in metafunc._calls:
|
||||||
subname = "%s[%s]" % (name, callspec.id)
|
subname = "%s[%s]" % (name, callspec.id)
|
||||||
yield Function(
|
yield Function(
|
||||||
|
|
|
@ -630,6 +630,37 @@ class TestFunction(object):
|
||||||
rec = testdir.inline_run()
|
rec = testdir.inline_run()
|
||||||
rec.assertoutcome(passed=1)
|
rec.assertoutcome(passed=1)
|
||||||
|
|
||||||
|
def test_parametrize_overrides_indirect_dependency_fixture(self, testdir):
|
||||||
|
"""Test parametrization when parameter overrides a fixture that a test indirectly depends on"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
fix3_instantiated = False
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fix1(fix2):
|
||||||
|
return fix2 + '1'
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fix2(fix3):
|
||||||
|
return fix3 + '2'
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fix3():
|
||||||
|
global fix3_instantiated
|
||||||
|
fix3_instantiated = True
|
||||||
|
return '3'
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fix2', ['2'])
|
||||||
|
def test_it(fix1):
|
||||||
|
assert fix1 == '21'
|
||||||
|
assert not fix3_instantiated
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rec = testdir.inline_run()
|
||||||
|
rec.assertoutcome(passed=1)
|
||||||
|
|
||||||
@ignore_parametrized_marks
|
@ignore_parametrized_marks
|
||||||
def test_parametrize_with_mark(self, testdir):
|
def test_parametrize_with_mark(self, testdir):
|
||||||
items = testdir.getitems(
|
items = testdir.getitems(
|
||||||
|
|
Loading…
Reference in New Issue