Fix nondeterminism in fixture collection order

fixtures.reorder_items is non-deterministic because it reorders based
on iteration over an (unordered) set.  Change the code to use an
OrderedDict instead so that we get deterministic behaviour, fixes #920.
This commit is contained in:
Lawrence Mitchell 2017-07-25 19:54:27 +01:00
parent b39f957b88
commit a546a612bd
5 changed files with 16 additions and 9 deletions

View File

@ -91,6 +91,7 @@ Kale Kundert
Katarzyna Jachim Katarzyna Jachim
Kevin Cox Kevin Cox
Kodi B. Arfer Kodi B. Arfer
Lawrence Mitchell
Lee Kamentsky Lee Kamentsky
Lev Maximov Lev Maximov
Llandy Riveron Del Risco Llandy Riveron Del Risco

View File

@ -19,6 +19,11 @@ from _pytest.compat import (
from _pytest.runner import fail from _pytest.runner import fail
from _pytest.compat import FuncargnamesCompatAttr from _pytest.compat import FuncargnamesCompatAttr
if sys.version_info[:2] == (2, 6):
from ordereddict import OrderedDict
else:
from collections import OrderedDict
def pytest_sessionstart(session): def pytest_sessionstart(session):
import _pytest.python import _pytest.python
@ -136,10 +141,10 @@ def get_parametrized_fixture_keys(item, scopenum):
except AttributeError: except AttributeError:
pass pass
else: else:
# cs.indictes.items() is random order of argnames but # cs.indices.items() is random order of argnames. Need to
# then again different functions (items) can change order of # sort this so that different calls to
# arguments so it doesn't matter much probably # get_parametrized_fixture_keys will be deterministic.
for argname, param_index in cs.indices.items(): for argname, param_index in sorted(cs.indices.items()):
if cs._arg2scopenum[argname] != scopenum: if cs._arg2scopenum[argname] != scopenum:
continue continue
if scopenum == 0: # session if scopenum == 0: # session
@ -161,7 +166,7 @@ def reorder_items(items):
for scopenum in range(0, scopenum_function): for scopenum in range(0, scopenum_function):
argkeys_cache[scopenum] = d = {} argkeys_cache[scopenum] = d = {}
for item in items: for item in items:
keys = set(get_parametrized_fixture_keys(item, scopenum)) keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum))
if keys: if keys:
d[item] = keys d[item] = keys
return reorder_items_atscope(items, set(), argkeys_cache, 0) return reorder_items_atscope(items, set(), argkeys_cache, 0)
@ -196,9 +201,9 @@ def slice_items(items, ignore, scoped_argkeys_cache):
for i, item in enumerate(it): for i, item in enumerate(it):
argkeys = scoped_argkeys_cache.get(item) argkeys = scoped_argkeys_cache.get(item)
if argkeys is not None: if argkeys is not None:
argkeys = argkeys.difference(ignore) newargkeys = OrderedDict.fromkeys(k for k in argkeys if k not in ignore)
if argkeys: # found a slicing key if newargkeys: # found a slicing key
slicing_argkey = argkeys.pop() slicing_argkey, _ = newargkeys.popitem()
items_before = items[:i] items_before = items[:i]
items_same = [item] items_same = [item]
items_other = [] items_other = []

1
changelog/920.bugfix Normal file
View File

@ -0,0 +1 @@
Fix non-determinism in order of fixture collection.

View File

@ -2548,7 +2548,6 @@ class TestFixtureMarker(object):
'*test_foo*beta*']) '*test_foo*beta*'])
@pytest.mark.issue920 @pytest.mark.issue920
@pytest.mark.xfail(reason="Fixture reordering not deterministic with hash randomisation")
def test_deterministic_fixture_collection(self, testdir, monkeypatch): def test_deterministic_fixture_collection(self, testdir, monkeypatch):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest

View File

@ -34,6 +34,7 @@ deps =
hypothesis<3.0 hypothesis<3.0
nose nose
mock<1.1 mock<1.1
ordereddict
[testenv:py27-subprocess] [testenv:py27-subprocess]
changedir = . changedir = .