Skip to content

Commit

Permalink
[feat] Test items based on asynchronous generators always exit with *…
Browse files Browse the repository at this point in the history
…xfail* status and emit a warning during the collection phase. This behavior is consistent with synchronous yield tests.

Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de>
  • Loading branch information
seifertm committed Oct 24, 2023
1 parent d697a12 commit 4bca1d7
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 38 deletions.
1 change: 1 addition & 0 deletions docs/source/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Changelog
- Deprecate redefinition of the `event_loop` fixture. `#587 <https://github.com/pytest-dev/pytest-asyncio/issues/531>`_
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 <https://github.com/pytest-dev/pytest-asyncio/issues/642>`__
- Remove support for Python 3.7
- Declare support for Python 3.12

Expand Down
29 changes: 26 additions & 3 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
Item,
Metafunc,
Parser,
PytestCollectionWarning,
PytestPluginManager,
Session,
StashKey,
Expand Down Expand Up @@ -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"):
Expand All @@ -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
Expand Down
223 changes: 223 additions & 0 deletions tests/test_asyncio_mark.py
Original file line number Diff line number Diff line change
@@ -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*"]
)
35 changes: 0 additions & 35 deletions tests/test_asyncio_mark_on_sync_function.py

This file was deleted.

0 comments on commit 4bca1d7

Please sign in to comment.