diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c7fc28adb..bda0acc46 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -16,6 +16,7 @@ from typing import Final from typing import final from typing import Generator from typing import Generic +from typing import Hashable from typing import Iterable from typing import Iterator from typing import List @@ -157,13 +158,24 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: @dataclasses.dataclass(frozen=True) -class FixtureArgKey: +class FixtureArgKeyByIndex: argname: str param_index: int scoped_item_path: Optional[Path] item_cls: Optional[type] +@dataclasses.dataclass(frozen=True) +class FixtureArgKeyByValue: + argname: str + param_value: Hashable + scoped_item_path: Optional[Path] + item_cls: Optional[type] + + +FixtureArgKey = Union[FixtureArgKeyByIndex, FixtureArgKeyByValue] + + def get_parametrized_fixture_keys( item: nodes.Item, scope: Scope ) -> Iterator[FixtureArgKey]: @@ -197,7 +209,15 @@ def get_parametrized_fixture_keys( assert_never(scope) param_index = cs.indices[argname] - yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls) + param_value = cs.params[argname] + if isinstance(param_value, Hashable): + yield FixtureArgKeyByValue( + argname, param_value, scoped_item_path, item_cls + ) + else: + yield FixtureArgKeyByIndex( # type: ignore[unreachable] + argname, param_index, scoped_item_path, item_cls + ) # Algorithm for sorting on a per-parametrized resource setup basis. diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index a8f36cb9f..40e46dcf9 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -11,6 +11,7 @@ from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import get_public_names from _pytest.pytester import Pytester from _pytest.python import Function +from _pytest.scope import Scope def test_getfuncargnames_functions(): @@ -4531,3 +4532,201 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None: result.assert_outcomes(errors=1) result.stdout.fnmatch_lines([expected]) assert result.ret == ExitCode.TESTS_FAILED + + +@pytest.mark.parametrize("scope", ["module", "package"]) +def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices( + scope, + pytester: Pytester, +): + package = pytester.mkdir("package") + package.joinpath("__init__.py").write_text("", encoding="utf-8") + package.joinpath("test_a.py").write_text( + textwrap.dedent( + f"""\ + import pytest + + @pytest.fixture(scope='{scope}') + def fixture1(request): + pass + + @pytest.mark.parametrize("fixture1", [1, 0], indirect=True) + def test_0(fixture1): + pass + + @pytest.mark.parametrize("fixture1", [2, 1], indirect=True) + def test_1(fixture1): + pass + + def test_2(): + pass + + @pytest.mark.parametrize("param", [0, 1, 2], scope='{scope}') + def test_3(param): + pass + + @pytest.mark.parametrize("param", [2, 1, 0], scope='{scope}') + def test_4(param): + pass + """ + ), + encoding="utf-8", + ) + result = pytester.runpytest("--collect-only") + result.stdout.re_match_lines( + [ + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + ] + ) + + +def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices_2( + pytester: Pytester, +): + pytester.makepyfile( + """ + import pytest + + @pytest.fixture(scope='module') + def fixture1(request): + pass + + @pytest.fixture(scope='module') + def fixture2(request): + pass + + @pytest.mark.parametrize("fixture1, fixture2", [("a", 0), ("b", 1), ("a", 2)], indirect=True) + def test_1(fixture1, fixture2): + pass + + @pytest.mark.parametrize("fixture1, fixture2", [("c", 4), ("a", 3)], indirect=True) + def test_2(fixture1, fixture2): + pass + + def test_3(): + pass + + @pytest.mark.parametrize("param1, param2", [("a", 0), ("b", 1), ("a", 2)], scope='module') + def test_4(param1, param2): + pass + + @pytest.mark.parametrize("param1, param2", [("c", 4), ("a", 3)], scope='module') + def test_5(param1, param2): + pass + """ + ) + result = pytester.runpytest("--collect-only") + result.stdout.re_match_lines( + [ + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + ] + ) + + +@pytest.mark.xfail( + reason="It isn't differentiated between direct `fixture` param and fixture `fixture`. Will be" + "solved by adding `baseid` to `FixtureArgKey`." +) +def test_reorder_with_high_scoped_direct_and_fixture_parametrization( + pytester: Pytester, +): + pytester.makepyfile( + """ + import pytest + + @pytest.fixture(params=[0, 1], scope='module') + def fixture(request): + pass + + def test_1(fixture): + pass + + def test_2(): + pass + + @pytest.mark.parametrize("fixture", [1, 2], scope='module') + def test_3(fixture): + pass + """ + ) + result = pytester.runpytest("--collect-only") + result.stdout.re_match_lines( + [ + r" ", + r" ", + r" ", + r" ", + r" ", + ] + ) + + +def test_get_parametrized_fixture_keys_with_unhashable_params( + pytester: Pytester, +) -> None: + module = pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("arg", [[1], [2]], scope='module') + def test(arg): + pass + """ + ) + test_0, test_1 = pytester.genitems((pytester.getmodulecol(module),)) + test_0_keys = list(fixtures.get_parametrized_fixture_keys(test_0, Scope.Module)) + test_1_keys = list(fixtures.get_parametrized_fixture_keys(test_1, Scope.Module)) + assert len(test_0_keys) == len(test_1_keys) == 1 + assert isinstance(test_0_keys[0], fixtures.FixtureArgKeyByIndex) + assert test_0_keys[0].param_index == 0 + assert isinstance(test_1_keys[0], fixtures.FixtureArgKeyByIndex) + assert test_1_keys[0].param_index == 1 + + +def test_reordering_with_unhashable_parametrize_args(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("arg", [[1], [2]], scope='module') + def test_1(arg): + print(arg) + + def test_2(): + print("test_2") + + @pytest.mark.parametrize("arg", [[3], [4]], scope='module') + def test_3(arg): + print(arg) + """ + ) + result = pytester.runpytest("-s") + result.stdout.fnmatch_lines( + [ + r"*1*", + r"*3*", + r"*2*", + r"*4*", + r"*test_2*", + ] + )