Skip to content
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

Async fixtures request wrong event loop #675

Merged
Merged
Show file tree
Hide file tree
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
Expand Up @@ -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()

Expand Down
38 changes: 29 additions & 9 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down
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
Expand Up @@ -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
Expand Down
30 changes: 24 additions & 6 deletions tests/hypothesis/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
31 changes: 31 additions & 0 deletions tests/markers/test_class_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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*'
Expand Down
68 changes: 66 additions & 2 deletions tests/markers/test_module_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Up @@ -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)
Loading