diff --git a/changelog/3941.bugfix.rst b/changelog/3941.bugfix.rst new file mode 100644 index 000000000..1cd3e843f --- /dev/null +++ b/changelog/3941.bugfix.rst @@ -0,0 +1 @@ +Fix bug where indirect parametrization would consider the scope of all fixtures used by the test function to determine the parametrization scope, and not only the scope of the fixtures being parametrized. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1976be80c..e83ea7a26 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1141,13 +1141,18 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): """ 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 isinstance(indirect, (list, tuple)): + all_arguments_are_fixtures = len(indirect) == len(argnames) + else: + all_arguments_are_fixtures = bool(indirect) + if all_arguments_are_fixtures: fixturedefs = arg2fixturedefs or {} - used_scopes = [fixturedef[0].scope for name, fixturedef in fixturedefs.items()] + used_scopes = [ + fixturedef[0].scope + for name, fixturedef in fixturedefs.items() + if name in argnames + ] if used_scopes: # Takes the most narrow scope from used fixtures for scope in reversed(scopes): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 7ff1e2283..2172c5e0c 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -132,6 +132,52 @@ class TestMetafunc(object): except ValueError as ve: assert "has an unsupported scope value 'doggy'" in str(ve) + def test_find_parametrized_scope(self): + """unittest for _find_parametrized_scope (#3941)""" + from _pytest.python import _find_parametrized_scope + + @attr.s + class DummyFixtureDef(object): + scope = attr.ib() + + fixtures_defs = dict( + session_fix=[DummyFixtureDef("session")], + package_fix=[DummyFixtureDef("package")], + module_fix=[DummyFixtureDef("module")], + class_fix=[DummyFixtureDef("class")], + func_fix=[DummyFixtureDef("function")], + ) + + # use arguments to determine narrow scope; the cause of the bug is that it would look on all + # fixture defs given to the method + def find_scope(argnames, indirect): + return _find_parametrized_scope(argnames, fixtures_defs, indirect=indirect) + + assert find_scope(["func_fix"], indirect=True) == "function" + assert find_scope(["class_fix"], indirect=True) == "class" + assert find_scope(["module_fix"], indirect=True) == "module" + assert find_scope(["package_fix"], indirect=True) == "package" + assert find_scope(["session_fix"], indirect=True) == "session" + + assert find_scope(["class_fix", "func_fix"], indirect=True) == "function" + assert find_scope(["func_fix", "session_fix"], indirect=True) == "function" + assert find_scope(["session_fix", "class_fix"], indirect=True) == "class" + assert find_scope(["package_fix", "session_fix"], indirect=True) == "package" + assert find_scope(["module_fix", "session_fix"], indirect=True) == "module" + + # when indirect is False or is not for all scopes, always use function + assert find_scope(["session_fix", "module_fix"], indirect=False) == "function" + assert ( + find_scope(["session_fix", "module_fix"], indirect=["module_fix"]) + == "function" + ) + assert ( + find_scope( + ["session_fix", "module_fix"], indirect=["session_fix", "module_fix"] + ) + == "module" + ) + def test_parametrize_and_id(self): def func(x, y): pass @@ -1383,6 +1429,39 @@ class TestMetafuncFunctionalAuto(object): result = testdir.runpytest() result.stdout.fnmatch_lines(["* 3 passed *"]) + def test_parametrize_some_arguments_auto_scope(self, testdir, monkeypatch): + """Integration test for (#3941)""" + class_fix_setup = [] + monkeypatch.setattr(sys, "class_fix_setup", class_fix_setup, raising=False) + func_fix_setup = [] + monkeypatch.setattr(sys, "func_fix_setup", func_fix_setup, raising=False) + + testdir.makepyfile( + """ + import pytest + import sys + + @pytest.fixture(scope='class', autouse=True) + def class_fix(request): + sys.class_fix_setup.append(request.param) + + @pytest.fixture(autouse=True) + def func_fix(): + sys.func_fix_setup.append(True) + + @pytest.mark.parametrize('class_fix', [10, 20], indirect=True) + class Test: + def test_foo(self): + pass + def test_bar(self): + pass + """ + ) + result = testdir.runpytest_inprocess() + result.stdout.fnmatch_lines(["* 4 passed in *"]) + assert func_fix_setup == [True] * 4 + assert class_fix_setup == [10, 20] + def test_parametrize_issue634(self, testdir): testdir.makepyfile( """