Skip to content

Commit 4cd8727

Browse files
authored
Merge pull request #2617 from wence-/fix/nondeterministic-fixtures
Fix nondeterminism in fixture collection order
2 parents 768edde + f047e07 commit 4cd8727

File tree

6 files changed

+52
-10
lines changed

6 files changed

+52
-10
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Kale Kundert
9191
Katarzyna Jachim
9292
Kevin Cox
9393
Kodi B. Arfer
94+
Lawrence Mitchell
9495
Lee Kamentsky
9596
Lev Maximov
9697
Llandy Riveron Del Risco

_pytest/fixtures.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
from _pytest.runner import fail
2020
from _pytest.compat import FuncargnamesCompatAttr
2121

22+
if sys.version_info[:2] == (2, 6):
23+
from ordereddict import OrderedDict
24+
else:
25+
from collections import OrderedDict
26+
2227

2328
def pytest_sessionstart(session):
2429
import _pytest.python
@@ -136,10 +141,10 @@ def get_parametrized_fixture_keys(item, scopenum):
136141
except AttributeError:
137142
pass
138143
else:
139-
# cs.indictes.items() is random order of argnames but
140-
# then again different functions (items) can change order of
141-
# arguments so it doesn't matter much probably
142-
for argname, param_index in cs.indices.items():
144+
# cs.indices.items() is random order of argnames. Need to
145+
# sort this so that different calls to
146+
# get_parametrized_fixture_keys will be deterministic.
147+
for argname, param_index in sorted(cs.indices.items()):
143148
if cs._arg2scopenum[argname] != scopenum:
144149
continue
145150
if scopenum == 0: # session
@@ -161,7 +166,7 @@ def reorder_items(items):
161166
for scopenum in range(0, scopenum_function):
162167
argkeys_cache[scopenum] = d = {}
163168
for item in items:
164-
keys = set(get_parametrized_fixture_keys(item, scopenum))
169+
keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum))
165170
if keys:
166171
d[item] = keys
167172
return reorder_items_atscope(items, set(), argkeys_cache, 0)
@@ -196,9 +201,9 @@ def slice_items(items, ignore, scoped_argkeys_cache):
196201
for i, item in enumerate(it):
197202
argkeys = scoped_argkeys_cache.get(item)
198203
if argkeys is not None:
199-
argkeys = argkeys.difference(ignore)
200-
if argkeys: # found a slicing key
201-
slicing_argkey = argkeys.pop()
204+
newargkeys = OrderedDict.fromkeys(k for k in argkeys if k not in ignore)
205+
if newargkeys: # found a slicing key
206+
slicing_argkey, _ = newargkeys.popitem()
202207
items_before = items[:i]
203208
items_same = [item]
204209
items_other = []

changelog/920.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix non-determinism in order of fixture collection. Adds new dependency (ordereddict) for Python 2.6.

doc/en/getting-started.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ Installation and Getting Started
99

1010
**dependencies**: `py <http://pypi.python.org/pypi/py>`_,
1111
`colorama (Windows) <http://pypi.python.org/pypi/colorama>`_,
12-
`argparse (py26) <http://pypi.python.org/pypi/argparse>`_.
12+
`argparse (py26) <http://pypi.python.org/pypi/argparse>`_,
13+
`ordereddict (py26) <http://pypi.python.org/pypi/ordereddict>`_.
1314

1415
**documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
1516

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ def main():
4646
install_requires = ['py>=1.4.33', 'setuptools'] # pluggy is vendored in _pytest.vendored_packages
4747
extras_require = {}
4848
if has_environment_marker_support():
49-
extras_require[':python_version=="2.6"'] = ['argparse']
49+
extras_require[':python_version=="2.6"'] = ['argparse', 'ordereddict']
5050
extras_require[':sys_platform=="win32"'] = ['colorama']
5151
else:
5252
if sys.version_info < (2, 7):
5353
install_requires.append('argparse')
54+
install_requires.append('ordereddict')
5455
if sys.platform == 'win32':
5556
install_requires.append('colorama')
5657

testing/python/fixture.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2547,6 +2547,39 @@ def test_foo(fix):
25472547
'*test_foo*alpha*',
25482548
'*test_foo*beta*'])
25492549

2550+
@pytest.mark.issue920
2551+
def test_deterministic_fixture_collection(self, testdir, monkeypatch):
2552+
testdir.makepyfile("""
2553+
import pytest
2554+
2555+
@pytest.fixture(scope="module",
2556+
params=["A",
2557+
"B",
2558+
"C"])
2559+
def A(request):
2560+
return request.param
2561+
2562+
@pytest.fixture(scope="module",
2563+
params=["DDDDDDDDD", "EEEEEEEEEEEE", "FFFFFFFFFFF", "banansda"])
2564+
def B(request, A):
2565+
return request.param
2566+
2567+
def test_foo(B):
2568+
# Something funky is going on here.
2569+
# Despite specified seeds, on what is collected,
2570+
# sometimes we get unexpected passes. hashing B seems
2571+
# to help?
2572+
assert hash(B) or True
2573+
""")
2574+
monkeypatch.setenv("PYTHONHASHSEED", "1")
2575+
out1 = testdir.runpytest_subprocess("-v")
2576+
monkeypatch.setenv("PYTHONHASHSEED", "2")
2577+
out2 = testdir.runpytest_subprocess("-v")
2578+
out1 = [line for line in out1.outlines if line.startswith("test_deterministic_fixture_collection.py::test_foo")]
2579+
out2 = [line for line in out2.outlines if line.startswith("test_deterministic_fixture_collection.py::test_foo")]
2580+
assert len(out1) == 12
2581+
assert out1 == out2
2582+
25502583

25512584
class TestRequestScopeAccess(object):
25522585
pytestmark = pytest.mark.parametrize(("scope", "ok", "error"), [

0 commit comments

Comments
 (0)