From ac7f3c32ed7e5bc114e77dd9060bc54b08271ba2 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Fri, 10 Nov 2023 13:24:08 +0100 Subject: [PATCH] Fix ScopeMismatch with parametrized cases (#317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * tests: add test for issue #311 * fix: propagate scope in ...ParamAlternative * fix: mangle fixture name for conftest Fixtures that are to be injected in conftest.py will be available globally. If we do not include this information in the name of the autogenerated fixtures, we may risk causing a conflict if another test/case/conftest uses parametrization on the same tests. * fix: avoid conflict also if __init__.py exists When conftest is in a package rather than in a mere folder, its name is "fully qualified". Perhaps there would be no need to actually add the scope in this case, but better safe than sorry. * Do not pass on None as scope Make sure that the default scope always is "function" so as to avoid issues with pytest <= 6, which 'translates' the scope string kwarg into an index from a list. The list does not contain None. * More explicit None scope replacement As suggested by @smarie * Add note and example for conftest qualname * Test correctness of scopes As suggested by @smarie * Add changelog for #317 * Update docs/changelog.md --------- Co-authored-by: Sylvain MariƩ --- docs/changelog.md | 8 +++ src/pytest_cases/case_parametrizer_new.py | 15 ++++++ src/pytest_cases/fixture_parametrize_plus.py | 10 ++-- tests/cases/issues/issue_311/cases.py | 6 +++ tests/cases/issues/issue_311/conftest.py | 9 ++++ .../issue_311/test_issue_311/conftest.py | 7 +++ .../test_issue_311/test_issue_311.py | 49 +++++++++++++++++++ 7 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 tests/cases/issues/issue_311/cases.py create mode 100644 tests/cases/issues/issue_311/conftest.py create mode 100644 tests/cases/issues/issue_311/test_issue_311/conftest.py create mode 100644 tests/cases/issues/issue_311/test_issue_311/test_issue_311.py diff --git a/docs/changelog.md b/docs/changelog.md index 9c9feffe..b583a8f2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # Changelog +### 3.8.1 - bugfixes + + - Fixed `ScopeMismatch` with parametrized cases in non-trivial test + trees. `scope` is now correctly handled for (i) `fixture` cases, and + (ii) fixtures defined in `conftest.py` files at any depth. Fixes + [#311](https://github.com/smarie/python-pytest-cases/issues/311). PR + [#317](https://github.com/smarie/python-pytest-cases/pull/317) by [@michele-riva](https://github.com/michele-riva). + ### 3.8.0 - async, generators and strict-markers - `@fixture` and `@parametrize` are now async and generator aware. Fixes diff --git a/src/pytest_cases/case_parametrizer_new.py b/src/pytest_cases/case_parametrizer_new.py index 361b742e..63135a06 100644 --- a/src/pytest_cases/case_parametrizer_new.py +++ b/src/pytest_cases/case_parametrizer_new.py @@ -557,6 +557,21 @@ def get_or_create_case_fixture(case_id, # type: str # Fix the problem with "case_foo(foo)" leading to the generated fixture having the same name existing_fixture_names += fixtures_in_cases_module + # If the fixture will be injected in a conftest, make sure its name + # is unique. Include also its scope to avoid conflicts. See #311. + # Notice target_host.__name__ may just be 'conftest' when tests + # are simple modules or a more complicated fully qualified name + # when the test suite is a package (i.e., with __init__.py). For + # example, target_host.__name__ would be 'tests.conftest' when + # executing tests from within 'base' in the following tree: + # base/ + # tests/ + # __init__.py + # conftest.py + if 'conftest' in target_host.__name__: + extra = target_host.__name__.replace('.', '_') + case_id = extra + '_' + case_id + '_with_scope_' + scope + def name_changer(name, i): return name + '_' * i diff --git a/src/pytest_cases/fixture_parametrize_plus.py b/src/pytest_cases/fixture_parametrize_plus.py index d70159c2..13a73431 100644 --- a/src/pytest_cases/fixture_parametrize_plus.py +++ b/src/pytest_cases/fixture_parametrize_plus.py @@ -316,6 +316,7 @@ def create(cls, i, # type: int argvalue, # type: Any id, # type: Union[str, Callable] + scope=None, # type: str hook=None, # type: Callable debug=False # type: bool ): @@ -362,7 +363,7 @@ def create(cls, # Create the fixture. IMPORTANT auto_simplify=True : we create a NON-parametrized fixture. _create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=(argvalue,), - hook=hook, auto_simplify=True, debug=debug) + scope=scope, hook=hook, auto_simplify=True, debug=debug) # Create the alternative argvals = (argvalue,) if nb_params == 1 else argvalue @@ -426,6 +427,7 @@ def create(cls, to_i, # type: int argvalues, # type: Any ids, # type: Union[Sequence[str], Callable] + scope="function", # type: str hook=None, # type: Callable debug=False # type: bool ): @@ -493,8 +495,8 @@ def create(cls, else: ids = [mini_idvalset(argnames, vals, i) for i, vals in enumerate(unmarked_argvalues)] - _create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=argvalues, ids=ids, hook=hook, - debug=debug) + _create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=argvalues, ids=ids, + scope=scope, hook=hook, debug=debug) # Create the corresponding alternative # note: as opposed to SingleParamAlternative, no need to move the custom id/marks to the ParamAlternative @@ -874,6 +876,7 @@ def _create_params_alt(fh, test_func, union_name, from_i, to_i, hook): # noqa return SingleParamAlternative.create(new_fixture_host=fh, test_func=test_func, param_union_name=union_name, argnames=argnames, i=i, argvalue=marked_argvalues[i], id=_id, + scope="function" if scope is None else scope, hook=hook, debug=debug) else: # If an explicit list of ids was provided, slice it. Otherwise the provided callable will be used later @@ -882,6 +885,7 @@ def _create_params_alt(fh, test_func, union_name, from_i, to_i, hook): # noqa return MultiParamAlternative.create(new_fixture_host=fh, test_func=test_func, param_union_name=union_name, argnames=argnames, from_i=from_i, to_i=to_i, argvalues=marked_argvalues[from_i:to_i], ids=_ids, + scope="function" if scope is None else scope, hook=hook, debug=debug) diff --git a/tests/cases/issues/issue_311/cases.py b/tests/cases/issues/issue_311/cases.py new file mode 100644 index 00000000..cf72361a --- /dev/null +++ b/tests/cases/issues/issue_311/cases.py @@ -0,0 +1,6 @@ +from pytest_cases import parametrize + + +@parametrize(arg=(1,)) +def case_parametrized(arg): + return arg diff --git a/tests/cases/issues/issue_311/conftest.py b/tests/cases/issues/issue_311/conftest.py new file mode 100644 index 00000000..f6be43e3 --- /dev/null +++ b/tests/cases/issues/issue_311/conftest.py @@ -0,0 +1,9 @@ +from pytest_cases import fixture, parametrize_with_cases + + +@fixture(scope='session') +@parametrize_with_cases('arg', cases='cases', scope='session') +def scope_mismatch(arg): + return [arg] + +session_scoped = scope_mismatch diff --git a/tests/cases/issues/issue_311/test_issue_311/conftest.py b/tests/cases/issues/issue_311/test_issue_311/conftest.py new file mode 100644 index 00000000..535ff9d2 --- /dev/null +++ b/tests/cases/issues/issue_311/test_issue_311/conftest.py @@ -0,0 +1,7 @@ +from pytest_cases import fixture, parametrize_with_cases + + +@fixture(scope='class') +@parametrize_with_cases('arg', cases='cases', scope='class') +def class_scoped(arg): + return [arg] diff --git a/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py b/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py new file mode 100644 index 00000000..1e3ad83e --- /dev/null +++ b/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py @@ -0,0 +1,49 @@ +from pytest_cases import fixture, parametrize_with_cases + + +@fixture +@parametrize_with_cases('arg', cases='cases') +def function_scoped(arg): + return [arg] + +# This tests would fail with a ScopeMismatch +# during collection before #317 +def test_scope_mismatch_collection(scope_mismatch): + assert scope_mismatch == [1] + +def test_scopes(session_scoped, function_scoped, class_scoped): + session_scoped.append(2) + function_scoped.append(2) + class_scoped.append(2) + assert session_scoped == [1, 2] + assert function_scoped == [1, 2] + assert class_scoped == [1, 2] + +def test_scopes_again(session_scoped, function_scoped, class_scoped): + session_scoped.append(3) + function_scoped.append(3) + class_scoped.append(3) + assert session_scoped == [1, 2, 3] + assert function_scoped == [1, 3] + assert class_scoped == [1, 3] + + +class TestScopesInClass: + + def test_scopes_in_class(self, session_scoped, + function_scoped, class_scoped): + session_scoped.append(4) + function_scoped.append(4) + class_scoped.append(4) + assert session_scoped == [1, 2, 3, 4] + assert function_scoped == [1, 4] + assert class_scoped == [1, 4] + + def test_scopes_in_class_again(self, session_scoped, + function_scoped, class_scoped): + session_scoped.append(5) + function_scoped.append(5) + class_scoped.append(5) + assert session_scoped == [1, 2, 3, 4, 5] + assert function_scoped == [1, 5] + assert class_scoped == [1, 4, 5]