Skip to content

Commit a94f3d5

Browse files
committed
Guard in-process HTTP tests against sse-starlette's global exit event
sse-starlette <3.0 stores its exit Event on the AppStatus class the first time an EventSourceResponse runs; the Event is bound to that test's event loop and breaks every later in-process SSE response on the same worker. test_http_unicode.py serves all its requests as EventSourceResponses (json_response=False) but had no reset fixture, so on a sse-starlette<3.0 install (CI's lowest-direct legs) it could poison the worker for any later SSE-based test. - Copy the autouse AppStatus reset fixture into test_http_unicode.py. - Reset on both sides of the yield, here and in test_sse.py, so each module also survives a stale Event left behind by an earlier test. - Correct the filterwarnings comments in both files: the item-scoped markers cannot cover the GC flush at session cleanup, so isolated runs without xdist (-n 0) still exit nonzero after all tests pass.
1 parent 92c4fe0 commit a94f3d5

2 files changed

Lines changed: 45 additions & 11 deletions

File tree

tests/client/test_http_unicode.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
(server→client and client→server) using the streamable HTTP transport.
66
"""
77

8-
from collections.abc import AsyncIterator
8+
from collections.abc import AsyncIterator, Iterator
99
from contextlib import asynccontextmanager
1010
from typing import Any
1111

1212
import httpx
1313
import pytest
14+
from sse_starlette.sse import AppStatus
1415
from starlette.applications import Starlette
1516
from starlette.routing import Mount
1617

@@ -28,14 +29,40 @@
2829
# v1's streamable-HTTP server transport leaks a handful of anyio memory streams on teardown when
2930
# run in process; the old subprocess harness never observed them. The interaction suite registers
3031
# the same two scoped filters globally from tests/interaction/conftest.py (see the comment there),
31-
# but they only take effect when that package's conftest is loaded; these markers keep this file
32-
# self-contained for isolated runs. The filters are scoped to anyio's MemoryObject*Stream leak
33-
# signature so an unrelated leak still fails the suite.
32+
# but they only take effect when that package's conftest is loaded; these markers keep the tests
33+
# themselves passing in isolated runs. Markers are item-scoped, so they cannot cover the GC
34+
# flush at session cleanup: an isolated run without xdist (`-n 0`) still exits nonzero after all
35+
# tests pass. The default xdist runs (addopts has `-n auto`) are unaffected, as are full-suite
36+
# runs, where the interaction conftest's ini-level filters apply. The filters are scoped to
37+
# anyio's MemoryObject*Stream leak signature so an unrelated leak still fails the suite.
3438
pytestmark = [
3539
pytest.mark.filterwarnings("ignore:.*MemoryObject(Send|Receive)Stream:pytest.PytestUnraisableExceptionWarning"),
3640
pytest.mark.filterwarnings("ignore:.*MemoryObject(Send|Receive)Stream:ResourceWarning"),
3741
]
3842

43+
44+
@pytest.fixture(autouse=True)
45+
def _reset_sse_starlette_exit_event() -> Iterator[None]:
46+
"""Reset sse-starlette's module-global exit Event around each test.
47+
48+
sse-starlette <3.0 (allowed by this branch's dependency floor; CI's lowest-direct leg
49+
installs it) stores an `anyio.Event` on the `AppStatus` class the first time an
50+
`EventSourceResponse` runs; that Event is bound to the test's event loop and breaks every
51+
subsequent in-process SSE response (and `json_response=False` below means every request
52+
in this module is served as one). sse-starlette 3.x switched to a ContextVar and has no
53+
such attribute. Resetting on both sides of the test keeps this module immune to a stale
54+
Event left behind by an earlier test on the same worker as well as cleaning up after its
55+
own. This mirrors the autouse fixtures in tests/shared/test_sse.py and
56+
tests/interaction/conftest.py.
57+
"""
58+
if hasattr(AppStatus, "should_exit_event"): # pragma: no branch
59+
# setattr keeps pyright happy: the locked sse-starlette 3.x has no such attribute.
60+
setattr(AppStatus, "should_exit_event", None) # pragma: lax no cover
61+
yield
62+
if hasattr(AppStatus, "should_exit_event"): # pragma: no branch
63+
setattr(AppStatus, "should_exit_event", None) # pragma: lax no cover
64+
65+
3966
# Test constants with various Unicode characters
4067
UNICODE_TEST_STRINGS = {
4168
"cyrillic": "Слой хранилища, где располагаются",

tests/shared/test_sse.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,12 @@
4949
# v1's HTTP server transports leak a handful of anyio memory streams on teardown when run in
5050
# process; the old subprocess harness never observed them. The interaction suite registers the
5151
# same two scoped filters globally from tests/interaction/conftest.py (see the comment there),
52-
# but they only take effect when that package's conftest is loaded; these markers keep this file
53-
# self-contained for isolated runs. The filters are scoped to anyio's MemoryObject*Stream leak
54-
# signature so an unrelated leak still fails the suite.
52+
# but they only take effect when that package's conftest is loaded; these markers keep the tests
53+
# themselves passing in isolated runs. Markers are item-scoped, so they cannot cover the GC
54+
# flush at session cleanup: an isolated run without xdist (`-n 0`) still exits nonzero after all
55+
# tests pass. The default xdist runs (addopts has `-n auto`) are unaffected, as are full-suite
56+
# runs, where the interaction conftest's ini-level filters apply. The filters are scoped to
57+
# anyio's MemoryObject*Stream leak signature so an unrelated leak still fails the suite.
5558
pytestmark = [
5659
pytest.mark.filterwarnings("ignore:.*MemoryObject(Send|Receive)Stream:pytest.PytestUnraisableExceptionWarning"),
5760
pytest.mark.filterwarnings("ignore:.*MemoryObject(Send|Receive)Stream:ResourceWarning"),
@@ -60,19 +63,23 @@
6063

6164
@pytest.fixture(autouse=True)
6265
def _reset_sse_starlette_exit_event() -> Iterator[None]:
63-
"""Reset sse-starlette's module-global exit Event after each test.
66+
"""Reset sse-starlette's module-global exit Event around each test.
6467
6568
sse-starlette <3.0 (allowed by this branch's dependency floor; CI's lowest-direct leg
6669
installs it) stores an `anyio.Event` on the `AppStatus` class the first time an
6770
`EventSourceResponse` runs; that Event is bound to the test's event loop and breaks every
6871
subsequent in-process SSE response. sse-starlette 3.x switched to a ContextVar and has no
69-
such attribute. This mirrors the autouse fixture in tests/interaction/conftest.py, which
70-
guards the interaction suite the same way.
72+
such attribute. Resetting on both sides of the test keeps this module immune to a stale
73+
Event left behind by an earlier test on the same worker as well as cleaning up after its
74+
own. This mirrors the autouse fixture in tests/interaction/conftest.py, which guards the
75+
interaction suite the same way.
7176
"""
72-
yield
7377
if hasattr(AppStatus, "should_exit_event"): # pragma: no branch
7478
# setattr keeps pyright happy: the locked sse-starlette 3.x has no such attribute.
7579
setattr(AppStatus, "should_exit_event", None) # pragma: lax no cover
80+
yield
81+
if hasattr(AppStatus, "should_exit_event"): # pragma: no branch
82+
setattr(AppStatus, "should_exit_event", None) # pragma: lax no cover
7683

7784

7885
def in_process_client_factory(app: Starlette) -> McpHttpClientFactory:

0 commit comments

Comments
 (0)