Skip to content

Commit

Permalink
Fix code which guesses parametrized scope based on arguments
Browse files Browse the repository at this point in the history
Fix #1832
  • Loading branch information
nicoddemus committed Aug 23, 2016
1 parent d99ceb1 commit 53a0e2b
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 46 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
* Fix regression when ``importorskip`` is used at module level (`#1822`_).
Thanks `@jaraco`_ and `@The-Compiler`_ for the report and `@nicoddemus`_ for the PR.

*
* Fix parametrization scope when session fixtures are used in conjunction
with normal parameters in the same call (`#1832`_).
Thanks `@The-Compiler`_ for the report, `@Kingdread`_ and `@nicoddemus`_ for the PR.

* Fix loader error when running ``pytest`` embedded in a zipfile.
Thanks `@mbachry`_ for the PR.
Expand All @@ -13,10 +15,15 @@

*

*

*

.. _@Kingdread: https://github.com/Kingdread
.. _@mbachry: https://github.com/mbachry

.. _#1822: https://github.com/pytest-dev/pytest/issues/1822
.. _#1832: https://github.com/pytest-dev/pytest/issues/1832


3.0.0
Expand Down
34 changes: 26 additions & 8 deletions _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,14 +802,8 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None,
newkeywords = [{newmark.markname: newmark}]

if scope is None:
if self._arg2fixturedefs:
# Takes the most narrow scope from used fixtures
fixtures_scopes = [fixturedef[0].scope for fixturedef in self._arg2fixturedefs.values()]
for scope in reversed(scopes):
if scope in fixtures_scopes:
break
else:
scope = 'function'
scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)

scopenum = scopes.index(scope)
valtypes = {}
for arg in argnames:
Expand Down Expand Up @@ -889,6 +883,30 @@ def addcall(self, funcargs=None, id=NOTSET, param=NOTSET):
self._calls.append(cs)


def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
"""Find the most appropriate scope for a parametrized call based on its arguments.
When there's at least one direct argument, always use "function" scope.
When a test function is parametrized and all its arguments are indirect
(e.g. fixtures), return the most narrow scope based on the fixtures used.
Related to issue #1832, based on code posted by @Kingdread.
"""
from _pytest.fixtures import scopes
indirect_as_list = isinstance(indirect, (list, tuple))
all_arguments_are_fixtures = indirect is True or \
indirect_as_list and len(indirect) == argnames
if all_arguments_are_fixtures:
fixturedefs = arg2fixturedefs or {}
used_scopes = [fixturedef[0].scope for name, fixturedef in fixturedefs.items()]
if used_scopes:
# Takes the most narrow scope from used fixtures
for scope in reversed(scopes):
if scope in used_scopes:
return scope

return 'function'


def _idval(val, argname, idx, idfn, config=None):
Expand Down
156 changes: 119 additions & 37 deletions testing/python/metafunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,43 +930,6 @@ def test_checklength():
reprec = testdir.inline_run()
reprec.assertoutcome(passed=5)

def test_parametrize_issue634(self, testdir):
testdir.makepyfile('''
import pytest
@pytest.fixture(scope='module')
def foo(request):
print('preparing foo-%d' % request.param)
return 'foo-%d' % request.param
def test_one(foo):
pass
def test_two(foo):
pass
test_two.test_with = (2, 3)
def pytest_generate_tests(metafunc):
params = (1, 2, 3, 4)
if not 'foo' in metafunc.fixturenames:
return
test_with = getattr(metafunc.function, 'test_with', None)
if test_with:
params = test_with
metafunc.parametrize('foo', params, indirect=True)
''')
result = testdir.runpytest("-s")
output = result.stdout.str()
assert output.count('preparing foo-2') == 1
assert output.count('preparing foo-3') == 1

def test_parametrize_issue323(self, testdir):
testdir.makepyfile("""
import pytest
Expand Down Expand Up @@ -1047,6 +1010,125 @@ def test_foo(x):
assert expectederror in failures[0].longrepr.reprcrash.message


class TestMetafuncFunctionalAuto:
"""
Tests related to automatically find out the correct scope for parametrized tests (#1832).
"""

def test_parametrize_auto_scope(self, testdir):
testdir.makepyfile('''
import pytest
@pytest.fixture(scope='session', autouse=True)
def fixture():
return 1
@pytest.mark.parametrize('animal', ["dog", "cat"])
def test_1(animal):
assert animal in ('dog', 'cat')
@pytest.mark.parametrize('animal', ['fish'])
def test_2(animal):
assert animal == 'fish'
''')
result = testdir.runpytest()
result.stdout.fnmatch_lines(['* 3 passed *'])

def test_parametrize_auto_scope_indirect(self, testdir):
testdir.makepyfile('''
import pytest
@pytest.fixture(scope='session')
def echo(request):
return request.param
@pytest.mark.parametrize('animal, echo', [("dog", 1), ("cat", 2)], indirect=['echo'])
def test_1(animal, echo):
assert animal in ('dog', 'cat')
assert echo in (1, 2, 3)
@pytest.mark.parametrize('animal, echo', [('fish', 3)], indirect=['echo'])
def test_2(animal, echo):
assert animal == 'fish'
assert echo in (1, 2, 3)
''')
result = testdir.runpytest()
result.stdout.fnmatch_lines(['* 3 passed *'])

def test_parametrize_auto_scope_override_fixture(self, testdir):
testdir.makepyfile('''
import pytest
@pytest.fixture(scope='session', autouse=True)
def animal():
return 'fox'
@pytest.mark.parametrize('animal', ["dog", "cat"])
def test_1(animal):
assert animal in ('dog', 'cat')
''')
result = testdir.runpytest()
result.stdout.fnmatch_lines(['* 2 passed *'])

def test_parametrize_all_indirects(self, testdir):
testdir.makepyfile('''
import pytest
@pytest.fixture()
def animal(request):
return request.param
@pytest.fixture(scope='session')
def echo(request):
return request.param
@pytest.mark.parametrize('animal, echo', [("dog", 1), ("cat", 2)], indirect=True)
def test_1(animal, echo):
assert animal in ('dog', 'cat')
assert echo in (1, 2, 3)
@pytest.mark.parametrize('animal, echo', [("fish", 3)], indirect=True)
def test_2(animal, echo):
assert animal == 'fish'
assert echo in (1, 2, 3)
''')
result = testdir.runpytest()
result.stdout.fnmatch_lines(['* 3 passed *'])

def test_parametrize_issue634(self, testdir):
testdir.makepyfile('''
import pytest
@pytest.fixture(scope='module')
def foo(request):
print('preparing foo-%d' % request.param)
return 'foo-%d' % request.param
def test_one(foo):
pass
def test_two(foo):
pass
test_two.test_with = (2, 3)
def pytest_generate_tests(metafunc):
params = (1, 2, 3, 4)
if not 'foo' in metafunc.fixturenames:
return
test_with = getattr(metafunc.function, 'test_with', None)
if test_with:
params = test_with
metafunc.parametrize('foo', params, indirect=True)
''')
result = testdir.runpytest("-s")
output = result.stdout.str()
assert output.count('preparing foo-2') == 1
assert output.count('preparing foo-3') == 1


class TestMarkersWithParametrization:
pytestmark = pytest.mark.issue308
def test_simple_mark(self, testdir):
Expand Down

0 comments on commit 53a0e2b

Please sign in to comment.