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]