From 53a0e2b1187405e2b8c77c9c26b8ee4a347e04bb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 22 Aug 2016 20:21:31 -0300 Subject: [PATCH] Fix code which guesses parametrized scope based on arguments Fix #1832 --- CHANGELOG.rst | 9 ++- _pytest/python.py | 34 ++++++-- testing/python/metafunc.py | 156 ++++++++++++++++++++++++++++--------- 3 files changed, 153 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d571a2bf3db..de19e8b3654 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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. @@ -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 diff --git a/_pytest/python.py b/_pytest/python.py index 36d3720acda..bf3bf2d3819 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -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: @@ -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): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 9f1887515a8..030d1da695a 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -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 @@ -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):