Make test parametrization override indirect fixtures
This commit is contained in:
parent
a48c47b53b
commit
1dc5e97ac2
|
@ -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:
|
||||||
|
|
|
@ -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