From 1c3995b25da06dacdf101b18c6b16a0e5c222534 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Fri, 25 Jul 2025 20:12:34 +0100 Subject: [PATCH] Add debug option --- changelog.d/980.added.rst | 1 + pytest_asyncio/plugin.py | 30 ++++- tests/test_asyncio_debug.py | 216 ++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 changelog.d/980.added.rst create mode 100644 tests/test_asyncio_debug.py diff --git a/changelog.d/980.added.rst b/changelog.d/980.added.rst new file mode 100644 index 00000000..549b55a9 --- /dev/null +++ b/changelog.d/980.added.rst @@ -0,0 +1 @@ +``--asyncio-debug`` CLI option and ``asyncio_debug`` configuration option to enable asyncio debug mode for the default event loop. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 9bfcfc64..9d8a59e3 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -91,11 +91,24 @@ def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None metavar="MODE", help=ASYNCIO_MODE_HELP, ) + group.addoption( + "--asyncio-debug", + dest="asyncio_debug", + action="store_true", + default=None, + help="enable asyncio debug mode for the default event loop", + ) parser.addini( "asyncio_mode", help="default value for --asyncio-mode", default="strict", ) + parser.addini( + "asyncio_debug", + help="enable asyncio debug mode for the default event loop", + type="bool", + default="false", + ) parser.addini( "asyncio_default_fixture_loop_scope", type="string", @@ -195,6 +208,17 @@ def _get_asyncio_mode(config: Config) -> Mode: ) from e +def _get_asyncio_debug(config: Config) -> bool: + val = config.getoption("asyncio_debug") + if val is None: + val = config.getini("asyncio_debug") + + if isinstance(val, bool): + return val + else: + return val == "true" + + _DEFAULT_FIXTURE_LOOP_SCOPE_UNSET = """\ The configuration option "asyncio_default_fixture_loop_scope" is unset. The event loop scope for asynchronous fixtures will default to the fixture caching \ @@ -221,10 +245,12 @@ def pytest_configure(config: Config) -> None: def pytest_report_header(config: Config) -> list[str]: """Add asyncio config to pytest header.""" mode = _get_asyncio_mode(config) + debug = _get_asyncio_debug(config) default_fixture_loop_scope = config.getini("asyncio_default_fixture_loop_scope") default_test_loop_scope = _get_default_test_loop_scope(config) header = [ f"mode={mode}", + f"debug={debug}", f"asyncio_default_fixture_loop_scope={default_fixture_loop_scope}", f"asyncio_default_test_loop_scope={default_test_loop_scope}", ] @@ -751,10 +777,12 @@ def _create_scoped_runner_fixture(scope: _ScopeName) -> Callable: ) def _scoped_runner( event_loop_policy, + request: FixtureRequest, ) -> Iterator[Runner]: new_loop_policy = event_loop_policy + debug_mode = _get_asyncio_debug(request.config) with _temporary_event_loop_policy(new_loop_policy): - runner = Runner().__enter__() + runner = Runner(debug=debug_mode).__enter__() try: yield runner except Exception as e: diff --git a/tests/test_asyncio_debug.py b/tests/test_asyncio_debug.py new file mode 100644 index 00000000..b097b63c --- /dev/null +++ b/tests/test_asyncio_debug.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +from textwrap import dedent + +import pytest +from pytest import Pytester + + +def test_asyncio_debug_disabled_by_default(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.asyncio + async def test_debug_mode_disabled(): + loop = asyncio.get_running_loop() + assert not loop.get_debug() + """ + ) + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +def test_asyncio_debug_enabled_via_cli_option(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.asyncio + async def test_debug_mode_enabled(): + loop = asyncio.get_running_loop() + assert loop.get_debug() + """ + ) + ) + result = pytester.runpytest("--asyncio-debug") + result.assert_outcomes(passed=1) + + +@pytest.mark.parametrize("config_value", ("true", "1")) +def test_asyncio_debug_enabled_via_config_option(pytester: Pytester, config_value: str): + pytester.makeini( + dedent( + f"""\ + [pytest] + asyncio_default_fixture_loop_scope = function + asyncio_debug = {config_value} + """ + ) + ) + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.asyncio + async def test_debug_mode_enabled(): + loop = asyncio.get_running_loop() + assert loop.get_debug() + """ + ) + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +@pytest.mark.parametrize("config_value", ("false", "0")) +def test_asyncio_debug_disabled_via_config_option( + pytester: Pytester, + config_value: str, +): + pytester.makeini( + dedent( + f"""\ + [pytest] + asyncio_default_fixture_loop_scope = function + asyncio_debug = {config_value} + """ + ) + ) + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.asyncio + async def test_debug_mode_disabled(): + loop = asyncio.get_running_loop() + assert not loop.get_debug() + """ + ) + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +def test_asyncio_debug_cli_option_overrides_config(pytester: Pytester): + pytester.makeini( + "[pytest]\nasyncio_default_fixture_loop_scope = function\nasyncio_debug = false" + ) + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.asyncio + async def test_debug_mode_enabled(): + loop = asyncio.get_running_loop() + assert loop.get_debug() + """ + ) + ) + result = pytester.runpytest("--asyncio-debug") + result.assert_outcomes(passed=1) + + +@pytest.mark.parametrize("loop_scope", ("function", "module", "session")) +def test_asyncio_debug_with_different_loop_scopes(pytester: Pytester, loop_scope: str): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + f"""\ + import asyncio + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.asyncio(loop_scope="{loop_scope}") + async def test_debug_mode_with_scope(): + loop = asyncio.get_running_loop() + assert loop.get_debug() + """ + ) + ) + result = pytester.runpytest("--asyncio-debug") + result.assert_outcomes(passed=1) + + +def test_asyncio_debug_with_async_fixtures(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + import pytest_asyncio + + pytest_plugins = "pytest_asyncio" + + @pytest_asyncio.fixture + async def async_fixture(): + loop = asyncio.get_running_loop() + assert loop.get_debug() + return "fixture_value" + + @pytest.mark.asyncio + async def test_debug_mode_with_fixture(async_fixture): + loop = asyncio.get_running_loop() + assert loop.get_debug() + assert async_fixture == "fixture_value" + """ + ) + ) + result = pytester.runpytest("--asyncio-debug") + result.assert_outcomes(passed=1) + + +def test_asyncio_debug_multiple_test_functions(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.asyncio + async def test_debug_first(): + loop = asyncio.get_running_loop() + assert loop.get_debug() + + @pytest.mark.asyncio + async def test_debug_second(): + loop = asyncio.get_running_loop() + assert loop.get_debug() + + @pytest.mark.asyncio + async def test_debug_third(): + loop = asyncio.get_running_loop() + assert loop.get_debug() + """ + ) + ) + result = pytester.runpytest("--asyncio-debug") + result.assert_outcomes(passed=3)