Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix code which guesses parametrized scope based on arguments #1847

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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