diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index fb7c5d00..7da71868 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -9,6 +9,7 @@ Changelog - Deprecate redefinition of the `event_loop` fixture. `#587 `_ Users requiring a class-scoped or module-scoped asyncio event loop for their tests should mark the corresponding class or module with `asyncio_event_loop`. +- Test items based on asynchronous generators always exit with *xfail* status and emit a warning during the collection phase. This behavior is consistent with synchronous yield tests. `#642 `__ - Remove support for Python 3.7 - Declare support for Python 3.12 diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index f403ecb6..2babd96a 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -34,6 +34,7 @@ Item, Metafunc, Parser, + PytestCollectionWarning, PytestPluginManager, Session, StashKey, @@ -387,13 +388,13 @@ def _can_substitute(item: Function) -> bool: raise NotImplementedError() -class AsyncFunction(PytestAsyncioFunction): - """Pytest item that is a coroutine or an asynchronous generator""" +class Coroutine(PytestAsyncioFunction): + """Pytest item created by a coroutine""" @staticmethod def _can_substitute(item: Function) -> bool: func = item.obj - return _is_coroutine_or_asyncgen(func) + return asyncio.iscoroutinefunction(func) def runtest(self) -> None: if self.get_closest_marker("asyncio"): @@ -404,6 +405,28 @@ def runtest(self) -> None: super().runtest() +class AsyncGenerator(PytestAsyncioFunction): + """Pytest item created by an asynchronous generator""" + + @staticmethod + def _can_substitute(item: Function) -> bool: + func = item.obj + return inspect.isasyncgenfunction(func) + + @classmethod + def _from_function(cls, function: Function, /) -> Function: + async_gen_item = super()._from_function(function) + unsupported_item_type_message = ( + f"Tests based on asynchronous generators are not supported. " + f"{function.name} will be ignored." + ) + async_gen_item.warn(PytestCollectionWarning(unsupported_item_type_message)) + async_gen_item.add_marker( + pytest.mark.xfail(run=False, reason=unsupported_item_type_message) + ) + return async_gen_item + + class AsyncStaticMethod(PytestAsyncioFunction): """ Pytest item that is a coroutine or an asynchronous generator diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py new file mode 100644 index 00000000..65e54861 --- /dev/null +++ b/tests/test_asyncio_mark.py @@ -0,0 +1,223 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_asyncio_mark_on_sync_function_emits_warning(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.asyncio + def test_a(): + pass + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] + ) + + +def test_asyncio_mark_on_async_generator_function_emits_warning_in_strict_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.asyncio + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_function_emits_warning_in_auto_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = auto + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_method_emits_warning_in_strict_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + class TestAsyncGenerator: + @pytest.mark.asyncio + async def test_a(self): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_method_emits_warning_in_auto_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + class TestAsyncGenerator: + @staticmethod + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = auto + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_strict_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + class TestAsyncGenerator: + @staticmethod + @pytest.mark.asyncio + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_auto_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + class TestAsyncGenerator: + @staticmethod + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = auto + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) diff --git a/tests/test_asyncio_mark_on_sync_function.py b/tests/test_asyncio_mark_on_sync_function.py deleted file mode 100644 index 70152b47..00000000 --- a/tests/test_asyncio_mark_on_sync_function.py +++ /dev/null @@ -1,35 +0,0 @@ -from textwrap import dedent - -from pytest import Pytester - - -def test_warn_asyncio_marker_for_regular_func(pytester: Pytester): - pytester.makepyfile( - dedent( - """\ - import pytest - - pytest_plugins = 'pytest_asyncio' - - @pytest.mark.asyncio - def test_a(): - pass - """ - ) - ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() - result.assert_outcomes(passed=1) - result.stdout.fnmatch_lines( - ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] - )