Merge pull request #2617 from wence-/fix/nondeterministic-fixtures

Fix nondeterminism in fixture collection order
This commit is contained in:
Bruno Oliveira 2017-07-30 17:17:40 -03:00 committed by GitHub
commit 4cd8727379
6 changed files with 52 additions and 10 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. Adds new dependency (ordereddict) for Python 2.6.

View File

@ -9,7 +9,8 @@ Installation and Getting Started
**dependencies**: `py <http://pypi.python.org/pypi/py>`_, **dependencies**: `py <http://pypi.python.org/pypi/py>`_,
`colorama (Windows) <http://pypi.python.org/pypi/colorama>`_, `colorama (Windows) <http://pypi.python.org/pypi/colorama>`_,
`argparse (py26) <http://pypi.python.org/pypi/argparse>`_. `argparse (py26) <http://pypi.python.org/pypi/argparse>`_,
`ordereddict (py26) <http://pypi.python.org/pypi/ordereddict>`_.
**documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_ **documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_

View File

@ -46,11 +46,12 @@ def main():
install_requires = ['py>=1.4.33', 'setuptools'] # pluggy is vendored in _pytest.vendored_packages install_requires = ['py>=1.4.33', 'setuptools'] # pluggy is vendored in _pytest.vendored_packages
extras_require = {} extras_require = {}
if has_environment_marker_support(): if has_environment_marker_support():
extras_require[':python_version=="2.6"'] = ['argparse'] extras_require[':python_version=="2.6"'] = ['argparse', 'ordereddict']
extras_require[':sys_platform=="win32"'] = ['colorama'] extras_require[':sys_platform=="win32"'] = ['colorama']
else: else:
if sys.version_info < (2, 7): if sys.version_info < (2, 7):
install_requires.append('argparse') install_requires.append('argparse')
install_requires.append('ordereddict')
if sys.platform == 'win32': if sys.platform == 'win32':
install_requires.append('colorama') install_requires.append('colorama')

View File

@ -2547,6 +2547,39 @@ class TestFixtureMarker(object):
'*test_foo*alpha*', '*test_foo*alpha*',
'*test_foo*beta*']) '*test_foo*beta*'])
@pytest.mark.issue920
def test_deterministic_fixture_collection(self, testdir, monkeypatch):
testdir.makepyfile("""
import pytest
@pytest.fixture(scope="module",
params=["A",
"B",
"C"])
def A(request):
return request.param
@pytest.fixture(scope="module",
params=["DDDDDDDDD", "EEEEEEEEEEEE", "FFFFFFFFFFF", "banansda"])
def B(request, A):
return request.param
def test_foo(B):
# Something funky is going on here.
# Despite specified seeds, on what is collected,
# sometimes we get unexpected passes. hashing B seems
# to help?
assert hash(B) or True
""")
monkeypatch.setenv("PYTHONHASHSEED", "1")
out1 = testdir.runpytest_subprocess("-v")
monkeypatch.setenv("PYTHONHASHSEED", "2")
out2 = testdir.runpytest_subprocess("-v")
out1 = [line for line in out1.outlines if line.startswith("test_deterministic_fixture_collection.py::test_foo")]
out2 = [line for line in out2.outlines if line.startswith("test_deterministic_fixture_collection.py::test_foo")]
assert len(out1) == 12
assert out1 == out2
class TestRequestScopeAccess(object): class TestRequestScopeAccess(object):
pytestmark = pytest.mark.parametrize(("scope", "ok", "error"), [ pytestmark = pytest.mark.parametrize(("scope", "ok", "error"), [