Skip to content

Commit 8680dfc

Browse files
authored
Merge pull request #3629 from egnartsms/issue-2220-param-breaks-dep
Make test parametrization override indirect fixtures
2 parents 7b47dfb + 76ac670 commit 8680dfc

File tree

6 files changed

+92
-9
lines changed

6 files changed

+92
-9
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ Ryan Wooden
182182
Samuel Dion-Girardeau
183183
Samuele Pedroni
184184
Segev Finer
185+
Serhii Mozghovyi
185186
Simon Gomizelj
186187
Skylar Downes
187188
Srinivas Reddy Thatiparthy

appveyor.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@ test_script:
4646
cache:
4747
- '%LOCALAPPDATA%\pip\cache'
4848
- '%USERPROFILE%\.cache\pre-commit'
49+
50+
# We don't deploy anything on tags with AppVeyor, we use Travis instead, so we
51+
# might as well save resources
52+
skip_tags: true

changelog/2220.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In case a (direct) parameter of a test overrides some fixture upon which the
2+
test depends indirectly, do the pruning of the fixture dependency tree. That
3+
is, recompute the full set of fixtures the test function needs.

src/_pytest/fixtures.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,43 @@ def get_direct_param_fixture_func(request):
274274
return request.param
275275

276276

277+
@attr.s(slots=True)
277278
class FuncFixtureInfo(object):
278-
def __init__(self, argnames, names_closure, name2fixturedefs):
279-
self.argnames = argnames
280-
self.names_closure = names_closure
281-
self.name2fixturedefs = name2fixturedefs
279+
# original function argument names
280+
argnames = attr.ib(type=tuple)
281+
# argnames that function immediately requires. These include argnames +
282+
# fixture names specified via usefixtures and via autouse=True in fixture
283+
# definitions.
284+
initialnames = attr.ib(type=tuple)
285+
names_closure = attr.ib(type="List[str]")
286+
name2fixturedefs = attr.ib(type="List[str, List[FixtureDef]]")
287+
288+
def prune_dependency_tree(self):
289+
"""Recompute names_closure from initialnames and name2fixturedefs
290+
291+
Can only reduce names_closure, which means that the new closure will
292+
always be a subset of the old one. The order is preserved.
293+
294+
This method is needed because direct parametrization may shadow some
295+
of the fixtures that were included in the originally built dependency
296+
tree. In this way the dependency tree can get pruned, and the closure
297+
of argnames may get reduced.
298+
"""
299+
closure = set()
300+
working_set = set(self.initialnames)
301+
while working_set:
302+
argname = working_set.pop()
303+
# argname may be smth not included in the original names_closure,
304+
# in which case we ignore it. This currently happens with pseudo
305+
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
306+
# So they introduce the new dependency 'request' which might have
307+
# been missing in the original tree (closure).
308+
if argname not in closure and argname in self.names_closure:
309+
closure.add(argname)
310+
if argname in self.name2fixturedefs:
311+
working_set.update(self.name2fixturedefs[argname][-1].argnames)
312+
313+
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
282314

283315

284316
class FixtureRequest(FuncargnamesCompatAttr):
@@ -1033,11 +1065,12 @@ def getfixtureinfo(self, node, func, cls, funcargs=True):
10331065
usefixtures = flatten(
10341066
mark.args for mark in node.iter_markers(name="usefixtures")
10351067
)
1036-
initialnames = argnames
1037-
initialnames = tuple(usefixtures) + initialnames
1068+
initialnames = tuple(usefixtures) + argnames
10381069
fm = node.session._fixturemanager
1039-
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames, node)
1040-
return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
1070+
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
1071+
initialnames, node
1072+
)
1073+
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
10411074

10421075
def pytest_plugin_registered(self, plugin):
10431076
nodeid = None
@@ -1085,6 +1118,12 @@ def merge(otherlist):
10851118
fixturenames_closure.append(arg)
10861119

10871120
merge(fixturenames)
1121+
1122+
# at this point, fixturenames_closure contains what we call "initialnames",
1123+
# which is a set of fixturenames the function immediately requests. We
1124+
# need to return it as well, so save this.
1125+
initialnames = tuple(fixturenames_closure)
1126+
10881127
arg2fixturedefs = {}
10891128
lastlen = -1
10901129
while lastlen != len(fixturenames_closure):
@@ -1106,7 +1145,7 @@ def sort_by_scope(arg_name):
11061145
return fixturedefs[-1].scopenum
11071146

11081147
fixturenames_closure.sort(key=sort_by_scope)
1109-
return fixturenames_closure, arg2fixturedefs
1148+
return initialnames, fixturenames_closure, arg2fixturedefs
11101149

11111150
def pytest_generate_tests(self, metafunc):
11121151
for argname in metafunc.fixturenames:

src/_pytest/python.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,11 @@ def _genfunctions(self, name, funcobj):
439439
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
440440
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
441441

442+
# add_funcarg_pseudo_fixture_def may have shadowed some fixtures
443+
# with direct parametrization, so make sure we update what the
444+
# function really needs.
445+
fixtureinfo.prune_dependency_tree()
446+
442447
for callspec in metafunc._calls:
443448
subname = "%s[%s]" % (name, callspec.id)
444449
yield Function(

testing/python/collect.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,37 @@ def test_overridden_via_param(value):
630630
rec = testdir.inline_run()
631631
rec.assertoutcome(passed=1)
632632

633+
def test_parametrize_overrides_indirect_dependency_fixture(self, testdir):
634+
"""Test parametrization when parameter overrides a fixture that a test indirectly depends on"""
635+
testdir.makepyfile(
636+
"""
637+
import pytest
638+
639+
fix3_instantiated = False
640+
641+
@pytest.fixture
642+
def fix1(fix2):
643+
return fix2 + '1'
644+
645+
@pytest.fixture
646+
def fix2(fix3):
647+
return fix3 + '2'
648+
649+
@pytest.fixture
650+
def fix3():
651+
global fix3_instantiated
652+
fix3_instantiated = True
653+
return '3'
654+
655+
@pytest.mark.parametrize('fix2', ['2'])
656+
def test_it(fix1):
657+
assert fix1 == '21'
658+
assert not fix3_instantiated
659+
"""
660+
)
661+
rec = testdir.inline_run()
662+
rec.assertoutcome(passed=1)
663+
633664
@ignore_parametrized_marks
634665
def test_parametrize_with_mark(self, testdir):
635666
items = testdir.getitems(

0 commit comments

Comments
 (0)