Skip to content

Async fixtures request wrong event loop #675

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
class TestClassScopedLoop:
loop: asyncio.AbstractEventLoop

@pytest_asyncio.fixture
@pytest_asyncio.fixture(scope="class")
async def my_fixture(self):
TestClassScopedLoop.loop = asyncio.get_running_loop()

38 changes: 29 additions & 9 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
@@ -204,13 +204,6 @@ def _preprocess_async_fixtures(
config = collector.config
asyncio_mode = _get_asyncio_mode(config)
fixturemanager = config.pluginmanager.get_plugin("funcmanage")
marker = collector.get_closest_marker("asyncio")
scope = marker.kwargs.get("scope", "function") if marker else "function"
if scope == "function":
event_loop_fixture_id = "event_loop"
else:
event_loop_node = _retrieve_scope_root(collector, scope)
event_loop_fixture_id = event_loop_node.stash.get(_event_loop_fixture_id, None)
for fixtures in fixturemanager._arg2fixturedefs.values():
for fixturedef in fixtures:
func = fixturedef.func
@@ -222,6 +215,14 @@ def _preprocess_async_fixtures(
# Ignore async fixtures without explicit asyncio mark in strict mode
# This applies to pytest_trio fixtures, for example
continue
scope = fixturedef.scope
if scope == "function":
event_loop_fixture_id = "event_loop"
else:
event_loop_node = _retrieve_scope_root(collector, scope)
event_loop_fixture_id = event_loop_node.stash.get(
_event_loop_fixture_id, None
)
_make_asyncio_fixture_function(func)
function_signature = inspect.signature(func)
if "event_loop" in function_signature.parameters:
@@ -589,6 +590,12 @@ def scoped_event_loop(
yield loop
loop.close()
asyncio.set_event_loop_policy(old_loop_policy)
# When a test uses both a scoped event loop and the event_loop fixture,
# the "_provide_clean_event_loop" finalizer of the event_loop fixture
# will already have installed a fresh event loop, in order to shield
# subsequent tests from side-effects. We close this loop before restoring
# the old loop to avoid ResourceWarnings.
asyncio.get_event_loop().close()
asyncio.set_event_loop(old_loop)

# @pytest.fixture does not register the fixture anywhere, so pytest doesn't
@@ -680,7 +687,9 @@ def pytest_generate_tests(metafunc: Metafunc) -> None:
)
# Add the scoped event loop fixture to Metafunc's list of fixture names and
# fixturedefs and leave the actual parametrization to pytest
metafunc.fixturenames.insert(0, event_loop_fixture_id)
# The fixture needs to be appended to avoid messing up the fixture evaluation
# order
metafunc.fixturenames.append(event_loop_fixture_id)
metafunc._arg2fixturedefs[
event_loop_fixture_id
] = fixturemanager._arg2fixturedefs[event_loop_fixture_id]
@@ -885,8 +894,13 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
fixturenames = item.fixturenames # type: ignore[attr-defined]
# inject an event loop fixture for all async tests
if "event_loop" in fixturenames:
# Move the "event_loop" fixture to the beginning of the fixture evaluation
# closure for backwards compatibility
fixturenames.remove("event_loop")
fixturenames.insert(0, event_loop_fixture_id)
fixturenames.insert(0, "event_loop")
else:
if event_loop_fixture_id not in fixturenames:
fixturenames.append(event_loop_fixture_id)
obj = getattr(item, "obj", None)
if not getattr(obj, "hypothesis", False) and getattr(
obj, "is_hypothesis_test", False
@@ -944,6 +958,12 @@ def _session_event_loop(
yield loop
loop.close()
asyncio.set_event_loop_policy(old_loop_policy)
# When a test uses both a scoped event loop and the event_loop fixture,
# the "_provide_clean_event_loop" finalizer of the event_loop fixture
# will already have installed a fresh event loop, in order to shield
# subsequent tests from side-effects. We close this loop before restoring
# the old loop to avoid ResourceWarnings.
asyncio.get_event_loop().close()
asyncio.set_event_loop(old_loop)


4 changes: 2 additions & 2 deletions tests/async_fixtures/test_async_fixtures_with_finalizer.py
Original file line number Diff line number Diff line change
@@ -4,13 +4,13 @@
import pytest


@pytest.mark.asyncio
@pytest.mark.asyncio(scope="module")
async def test_module_with_event_loop_finalizer(port_with_event_loop_finalizer):
await asyncio.sleep(0.01)
assert port_with_event_loop_finalizer


@pytest.mark.asyncio
@pytest.mark.asyncio(scope="module")
async def test_module_with_get_event_loop_finalizer(port_with_get_event_loop_finalizer):
await asyncio.sleep(0.01)
assert port_with_get_event_loop_finalizer
30 changes: 24 additions & 6 deletions tests/hypothesis/test_base.py
Original file line number Diff line number Diff line change
@@ -8,10 +8,22 @@
from pytest import Pytester


@given(st.integers())
@pytest.mark.asyncio
async def test_mark_inner(n):
assert isinstance(n, int)
def test_hypothesis_given_decorator_before_asyncio_mark(pytester: Pytester):
pytester.makepyfile(
dedent(
"""\
import pytest
from hypothesis import given, strategies as st
@given(st.integers())
@pytest.mark.asyncio
async def test_mark_inner(n):
assert isinstance(n, int)
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1)


@pytest.mark.asyncio
@@ -54,8 +66,14 @@ async def test_explicit_fixture_request(event_loop, n):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=2)
result.stdout.fnmatch_lines(
[
'*is asynchronous and explicitly requests the "event_loop" fixture*',
"*event_loop fixture provided by pytest-asyncio has been redefined*",
]
)


def test_async_auto_marked(pytester: Pytester):
31 changes: 31 additions & 0 deletions tests/markers/test_class_scope.py
Original file line number Diff line number Diff line change
@@ -220,3 +220,34 @@ async def test_runs_is_same_loop_as_fixture(self, my_fixture):
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_class_scoped_fixture_with_function_scoped_test(
pytester: pytest.Pytester,
):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
class TestMixedScopes:
@pytest_asyncio.fixture(scope="class")
async def async_fixture(self):
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio(scope="function")
async def test_runs_in_different_loop_as_fixture(self, async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
2 changes: 1 addition & 1 deletion tests/markers/test_function_scope.py
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ async def test_remember_loop(event_loop):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
'*is asynchronous and explicitly requests the "event_loop" fixture*'
68 changes: 66 additions & 2 deletions tests/markers/test_module_scope.py
Original file line number Diff line number Diff line change
@@ -48,8 +48,11 @@ def sample_fixture():
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=2)
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=2, warnings=2)
result.stdout.fnmatch_lines(
'*is asynchronous and explicitly requests the "event_loop" fixture*'
)


def test_asyncio_mark_provides_module_scoped_loop_strict_mode(pytester: Pytester):
@@ -216,3 +219,64 @@ async def test_runs_is_same_loop_as_fixture(my_fixture):
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_module_scoped_fixture_with_class_scoped_test(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture(scope="module")
async def async_fixture():
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio(scope="class")
class TestMixedScopes:
async def test_runs_in_different_loop_as_fixture(self, async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_module_scoped_fixture_with_function_scoped_test(
pytester: Pytester,
):
pytester.makepyfile(
__init__="",
test_mixed_scopes=dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture(scope="module")
async def async_fixture():
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio(scope="function")
async def test_runs_in_different_loop_as_fixture(async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
91 changes: 91 additions & 0 deletions tests/markers/test_package_scope.py
Original file line number Diff line number Diff line change
@@ -223,3 +223,94 @@ async def test_runs_in_same_loop_as_fixture(my_fixture):
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_package_scoped_fixture_with_module_scoped_test(
pytester: Pytester,
):
pytester.makepyfile(
__init__="",
test_mixed_scopes=dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture(scope="package")
async def async_fixture():
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio(scope="module")
async def test_runs_in_different_loop_as_fixture(async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_package_scoped_fixture_with_class_scoped_test(
pytester: Pytester,
):
pytester.makepyfile(
__init__="",
test_mixed_scopes=dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture(scope="package")
async def async_fixture():
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio(scope="class")
class TestMixedScopes:
async def test_runs_in_different_loop_as_fixture(self, async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_package_scoped_fixture_with_function_scoped_test(
pytester: Pytester,
):
pytester.makepyfile(
__init__="",
test_mixed_scopes=dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture(scope="package")
async def async_fixture():
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio
async def test_runs_in_different_loop_as_fixture(async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
121 changes: 121 additions & 0 deletions tests/markers/test_session_scope.py
Original file line number Diff line number Diff line change
@@ -227,3 +227,124 @@ async def test_runs_in_same_loop_as_fixture(my_fixture):
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_session_scoped_fixture_with_package_scoped_test(
pytester: Pytester,
):
pytester.makepyfile(
__init__="",
test_mixed_scopes=dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture(scope="session")
async def async_fixture():
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio(scope="package")
async def test_runs_in_different_loop_as_fixture(async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_session_scoped_fixture_with_module_scoped_test(
pytester: Pytester,
):
pytester.makepyfile(
__init__="",
test_mixed_scopes=dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture(scope="session")
async def async_fixture():
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio(scope="module")
async def test_runs_in_different_loop_as_fixture(async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_session_scoped_fixture_with_class_scoped_test(
pytester: Pytester,
):
pytester.makepyfile(
__init__="",
test_mixed_scopes=dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture(scope="session")
async def async_fixture():
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio(scope="class")
class TestMixedScopes:
async def test_runs_in_different_loop_as_fixture(self, async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_allows_combining_session_scoped_fixture_with_function_scoped_test(
pytester: Pytester,
):
pytester.makepyfile(
__init__="",
test_mixed_scopes=dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture(scope="session")
async def async_fixture():
global loop
loop = asyncio.get_running_loop()
@pytest.mark.asyncio
async def test_runs_in_different_loop_as_fixture(async_fixture):
global loop
assert asyncio.get_running_loop() is not loop
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
7 changes: 5 additions & 2 deletions tests/test_event_loop_fixture_finalizer.py
Original file line number Diff line number Diff line change
@@ -84,8 +84,11 @@ async def test_async_with_explicit_fixture_request(event_loop):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
'*is asynchronous and explicitly requests the "event_loop" fixture*'
)


def test_event_loop_fixture_finalizer_raises_warning_when_fixture_leaves_loop_unclosed(
6 changes: 3 additions & 3 deletions tests/test_event_loop_fixture_override_deprecation.py
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ async def test_emits_warning():
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
["*event_loop fixture provided by pytest-asyncio has been redefined*"]
@@ -50,7 +50,7 @@ async def test_emits_warning_when_requested_explicitly(event_loop):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=2)
result.stdout.fnmatch_lines(
["*event_loop fixture provided by pytest-asyncio has been redefined*"]
@@ -107,5 +107,5 @@ def test_emits_warning(uses_event_loop):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=1)
10 changes: 5 additions & 5 deletions tests/test_explicit_event_loop_fixture_request.py
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ async def test_coroutine_emits_warning(event_loop):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
['*is asynchronous and explicitly requests the "event_loop" fixture*']
@@ -39,7 +39,7 @@ async def test_coroutine_emits_warning(self, event_loop):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
['*is asynchronous and explicitly requests the "event_loop" fixture*']
@@ -62,7 +62,7 @@ async def test_coroutine_emits_warning(event_loop):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
['*is asynchronous and explicitly requests the "event_loop" fixture*']
@@ -88,7 +88,7 @@ async def test_uses_fixture(emits_warning):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
['*is asynchronous and explicitly requests the "event_loop" fixture*']
@@ -114,7 +114,7 @@ async def test_uses_fixture(emits_warning):
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result = pytester.runpytest("--asyncio-mode=strict", "-W default")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
['*is asynchronous and explicitly requests the "event_loop" fixture*']