Make test parametrization override indirect fixtures

This commit is contained in:
Serhii Mozghovyi 2018-06-28 14:32:29 +03:00
parent a48c47b53b
commit 1dc5e97ac2
3 changed files with 86 additions and 9 deletions

View File

@ -274,11 +274,45 @@ 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=lambda name: self.names_closure.index(name)
)
class FixtureRequest(FuncargnamesCompatAttr): class FixtureRequest(FuncargnamesCompatAttr):
@ -1033,11 +1067,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 +1120,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 +1147,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:

View File

@ -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(

View File

@ -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(