Skip to content

Commit fac9092

Browse files
committed
[fix] Fixes a bug related to the ordering of the "event_loop" fixture in connection with parametrized tests.
The fixture evaluation order changed for parametrizations of the same test. The reason is probably the fact that `event_loop` was inserted at position 0 in the pytest fixture closure for the current test. Since the synchronization wrapper for async tests uses the currently installed event loop rather than an explicit reference as of commit 36b2269, we can drop the insertion of the event_loop fixture as the first fixture to be evaluated. This patch also addresses an issue that caused RuntimeErrors when the event loop was set to None in a fixture that is requested by an async test. This can occur due to the use of asyncio.run, for example. Signed-off-by: Michael Seifert <[email protected]>
1 parent c42eb2a commit fac9092

File tree

6 files changed

+182
-11
lines changed

6 files changed

+182
-11
lines changed

pytest_asyncio/plugin.py

+10-11
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,10 @@ def _removesuffix(s: str, suffix: str) -> str:
623623
@contextlib.contextmanager
624624
def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]:
625625
old_loop_policy = asyncio.get_event_loop_policy()
626-
old_loop = asyncio.get_event_loop()
626+
try:
627+
old_loop = asyncio.get_event_loop()
628+
except RuntimeError:
629+
old_loop = None
627630
asyncio.set_event_loop_policy(policy)
628631
try:
629632
yield
@@ -634,7 +637,10 @@ def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[No
634637
# will already have installed a fresh event loop, in order to shield
635638
# subsequent tests from side-effects. We close this loop before restoring
636639
# the old loop to avoid ResourceWarnings.
637-
asyncio.get_event_loop().close()
640+
try:
641+
asyncio.get_event_loop().close()
642+
except RuntimeError:
643+
pass
638644
asyncio.set_event_loop(old_loop)
639645

640646

@@ -908,15 +914,8 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
908914
else:
909915
event_loop_fixture_id = "event_loop"
910916
fixturenames = item.fixturenames # type: ignore[attr-defined]
911-
# inject an event loop fixture for all async tests
912-
if "event_loop" in fixturenames:
913-
# Move the "event_loop" fixture to the beginning of the fixture evaluation
914-
# closure for backwards compatibility
915-
fixturenames.remove("event_loop")
916-
fixturenames.insert(0, "event_loop")
917-
else:
918-
if event_loop_fixture_id not in fixturenames:
919-
fixturenames.append(event_loop_fixture_id)
917+
if event_loop_fixture_id not in fixturenames:
918+
fixturenames.append(event_loop_fixture_id)
920919
obj = getattr(item, "obj", None)
921920
if not getattr(obj, "hypothesis", False) and getattr(
922921
obj, "is_hypothesis_test", False

tests/markers/test_class_scope.py

+35
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,38 @@ async def test_runs_in_different_loop_as_fixture(self, async_fixture):
251251
)
252252
result = pytester.runpytest("--asyncio-mode=strict")
253253
result.assert_outcomes(passed=1)
254+
255+
256+
def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
257+
pytester: pytest.Pytester,
258+
):
259+
pytester.makepyfile(
260+
dedent(
261+
"""\
262+
import pytest
263+
import asyncio
264+
265+
class TestClass:
266+
@pytest.fixture(scope="class")
267+
def sets_event_loop_to_none(self):
268+
# asyncio.run() creates a new event loop without closing the
269+
# existing one. For any test, but the first one, this leads to
270+
# a ResourceWarning when the discarded loop is destroyed by the
271+
# garbage collector. We close the current loop to avoid this.
272+
try:
273+
asyncio.get_event_loop().close()
274+
except RuntimeError:
275+
pass
276+
return asyncio.run(asyncio.sleep(0))
277+
# asyncio.run() sets the current event loop to None when finished
278+
279+
@pytest.mark.asyncio(scope="class")
280+
# parametrization may impact fixture ordering
281+
@pytest.mark.parametrize("n", (0, 1))
282+
async def test_does_not_fail(self, sets_event_loop_to_none, n):
283+
pass
284+
"""
285+
)
286+
)
287+
result = pytester.runpytest("--asyncio-mode=strict")
288+
result.assert_outcomes(passed=2)

tests/markers/test_function_scope.py

+34
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,37 @@ async def test_runs_is_same_loop_as_fixture(my_fixture):
145145
)
146146
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
147147
result.assert_outcomes(passed=1)
148+
149+
150+
def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
151+
pytester: Pytester,
152+
):
153+
pytester.makepyfile(
154+
dedent(
155+
"""\
156+
import pytest
157+
import asyncio
158+
159+
@pytest.fixture
160+
def sets_event_loop_to_none():
161+
# asyncio.run() creates a new event loop without closing the existing
162+
# one. For any test, but the first one, this leads to a ResourceWarning
163+
# when the discarded loop is destroyed by the garbage collector.
164+
# We close the current loop to avoid this
165+
try:
166+
asyncio.get_event_loop().close()
167+
except RuntimeError:
168+
pass
169+
return asyncio.run(asyncio.sleep(0))
170+
# asyncio.run() sets the current event loop to None when finished
171+
172+
@pytest.mark.asyncio
173+
# parametrization may impact fixture ordering
174+
@pytest.mark.parametrize("n", (0, 1))
175+
async def test_does_not_fail(sets_event_loop_to_none, n):
176+
pass
177+
"""
178+
)
179+
)
180+
result = pytester.runpytest("--asyncio-mode=strict")
181+
result.assert_outcomes(passed=2)

tests/markers/test_module_scope.py

+34
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,37 @@ async def test_runs_in_different_loop_as_fixture(async_fixture):
280280
)
281281
result = pytester.runpytest("--asyncio-mode=strict")
282282
result.assert_outcomes(passed=1)
283+
284+
285+
def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
286+
pytester: Pytester,
287+
):
288+
pytester.makepyfile(
289+
dedent(
290+
"""\
291+
import pytest
292+
import asyncio
293+
294+
@pytest.fixture(scope="module")
295+
def sets_event_loop_to_none():
296+
# asyncio.run() creates a new event loop without closing the existing
297+
# one. For any test, but the first one, this leads to a ResourceWarning
298+
# when the discarded loop is destroyed by the garbage collector.
299+
# We close the current loop to avoid this
300+
try:
301+
asyncio.get_event_loop().close()
302+
except RuntimeError:
303+
pass
304+
return asyncio.run(asyncio.sleep(0))
305+
# asyncio.run() sets the current event loop to None when finished
306+
307+
@pytest.mark.asyncio(scope="module")
308+
# parametrization may impact fixture ordering
309+
@pytest.mark.parametrize("n", (0, 1))
310+
async def test_does_not_fail(sets_event_loop_to_none, n):
311+
pass
312+
"""
313+
)
314+
)
315+
result = pytester.runpytest("--asyncio-mode=strict")
316+
result.assert_outcomes(passed=2)

tests/markers/test_package_scope.py

+35
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,38 @@ async def test_runs_in_different_loop_as_fixture(async_fixture):
314314
)
315315
result = pytester.runpytest("--asyncio-mode=strict")
316316
result.assert_outcomes(passed=1)
317+
318+
319+
def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
320+
pytester: Pytester,
321+
):
322+
pytester.makepyfile(
323+
__init__="",
324+
test_loop_is_none=dedent(
325+
"""\
326+
import pytest
327+
import asyncio
328+
329+
@pytest.fixture(scope="package")
330+
def sets_event_loop_to_none():
331+
# asyncio.run() creates a new event loop without closing the existing
332+
# one. For any test, but the first one, this leads to a ResourceWarning
333+
# when the discarded loop is destroyed by the garbage collector.
334+
# We close the current loop to avoid this
335+
try:
336+
asyncio.get_event_loop().close()
337+
except RuntimeError:
338+
pass
339+
return asyncio.run(asyncio.sleep(0))
340+
# asyncio.run() sets the current event loop to None when finished
341+
342+
@pytest.mark.asyncio(scope="package")
343+
# parametrization may impact fixture ordering
344+
@pytest.mark.parametrize("n", (0, 1))
345+
async def test_does_not_fail(sets_event_loop_to_none, n):
346+
pass
347+
"""
348+
),
349+
)
350+
result = pytester.runpytest("--asyncio-mode=strict")
351+
result.assert_outcomes(passed=2)

tests/markers/test_session_scope.py

+34
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,37 @@ async def test_runs_in_different_loop_as_fixture(async_fixture):
348348
)
349349
result = pytester.runpytest("--asyncio-mode=strict")
350350
result.assert_outcomes(passed=1)
351+
352+
353+
def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
354+
pytester: Pytester,
355+
):
356+
pytester.makepyfile(
357+
dedent(
358+
"""\
359+
import pytest
360+
import asyncio
361+
362+
@pytest.fixture(scope="session")
363+
def sets_event_loop_to_none():
364+
# asyncio.run() creates a new event loop without closing the existing
365+
# one. For any test, but the first one, this leads to a ResourceWarning
366+
# when the discarded loop is destroyed by the garbage collector.
367+
# We close the current loop to avoid this
368+
try:
369+
asyncio.get_event_loop().close()
370+
except RuntimeError:
371+
pass
372+
return asyncio.run(asyncio.sleep(0))
373+
# asyncio.run() sets the current event loop to None when finished
374+
375+
@pytest.mark.asyncio(scope="session")
376+
# parametrization may impact fixture ordering
377+
@pytest.mark.parametrize("n", (0, 1))
378+
async def test_does_not_fail(sets_event_loop_to_none, n):
379+
pass
380+
"""
381+
)
382+
)
383+
result = pytester.runpytest("--asyncio-mode=strict")
384+
result.assert_outcomes(passed=2)

0 commit comments

Comments
 (0)