Skip to content

Commit 9dc96d6

Browse files
committed
Guard streamable-HTTP test modules against sse-starlette's module-global exit event
tests/shared/test_streamable_http.py and tests/server/test_streamable_http_security.py serve responses in process; on sse-starlette <3.0 (what the lowest-direct CI legs install) the first EventSourceResponse binds AppStatus.should_exit_event to that test's event loop and every later SSE response in the module fails. Port the both-sides reset fixture from tests/client/test_http_unicode.py into both files. Also give the security module the same per-test gc.collect() teardown as test_streamable_http.py so leaked-stream warnings stay inside the module's scoped filters, and drop the comment claiming a -n 0 run exits nonzero after all tests pass: with the flush in place it exits 0.
1 parent b80a9bd commit 9dc96d6

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

tests/server/test_streamable_http_security.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"""Tests for StreamableHTTP server DNS rebinding protection."""
22

3-
from collections.abc import AsyncIterator
3+
import gc
4+
from collections.abc import AsyncIterator, Iterator
45
from contextlib import asynccontextmanager
56

67
import httpx
78
import pytest
9+
from sse_starlette.sse import AppStatus
810
from starlette.applications import Starlette
911
from starlette.routing import Mount
1012

@@ -23,18 +25,51 @@
2325
# run in process; the old subprocess harness never observed them. The interaction suite registers
2426
# the same two scoped filters globally from tests/interaction/conftest.py (see the comment there),
2527
# but they only take effect when that package's conftest is loaded; these markers keep the tests
26-
# that complete the initialize handshake passing in isolated runs. Markers are item-scoped, so
27-
# they cannot cover the GC flush at session cleanup: an isolated run without xdist (`-n 0`) still
28-
# exits nonzero after all tests pass. The default xdist runs (addopts has `-n auto`) are
29-
# unaffected, as are full-suite runs, where the interaction conftest's ini-level filters apply.
30-
# The filters are scoped to anyio's MemoryObject*Stream leak signature so an unrelated leak
31-
# still fails the suite.
28+
# that complete the initialize handshake passing in isolated runs. The filters are scoped to
29+
# anyio's MemoryObject*Stream leak signature so an unrelated leak still fails the suite.
3230
pytestmark = [
3331
pytest.mark.filterwarnings("ignore:.*MemoryObject(Send|Receive)Stream:pytest.PytestUnraisableExceptionWarning"),
3432
pytest.mark.filterwarnings("ignore:.*MemoryObject(Send|Receive)Stream:ResourceWarning"),
3533
]
3634

3735

36+
@pytest.fixture(autouse=True)
37+
def _collect_leaked_streams() -> Iterator[None]:
38+
"""Garbage-collect each test's leaked memory streams inside its own teardown.
39+
40+
The filterwarnings marks above only apply while a test in this file is the
41+
active warning context. The leaked streams sit in reference cycles, so without
42+
a forced collection their deallocator warnings fire wherever the garbage
43+
collector happens to run next: during an unrelated test (failing it, since the
44+
global ``filterwarnings = ["error"]`` has no ignore there) or at pytest's
45+
session-unconfigure unraisable sweep (exit code 1 after all tests passed when
46+
running without xdist, e.g. ``-n 0`` for ``--pdb`` debugging).
47+
"""
48+
yield
49+
gc.collect()
50+
51+
52+
@pytest.fixture(autouse=True)
53+
def _reset_sse_starlette_exit_event() -> Iterator[None]:
54+
"""Reset sse-starlette's module-global exit Event around each test.
55+
56+
sse-starlette <3.0 (allowed by this branch's dependency floor; CI's lowest-direct leg
57+
installs it) stores an `anyio.Event` on the `AppStatus` class the first time an
58+
`EventSourceResponse` runs; that Event is bound to the test's event loop and breaks every
59+
subsequent in-process SSE response. sse-starlette 3.x switched to a ContextVar and has no
60+
such attribute. Resetting on both sides of the test keeps this module immune to a stale
61+
Event left behind by an earlier test on the same worker as well as cleaning up after its
62+
own. This mirrors the autouse fixtures in tests/shared/test_sse.py and
63+
tests/interaction/conftest.py.
64+
"""
65+
if hasattr(AppStatus, "should_exit_event"): # pragma: no branch
66+
# setattr keeps pyright happy: the locked sse-starlette 3.x has no such attribute.
67+
setattr(AppStatus, "should_exit_event", None) # pragma: lax no cover
68+
yield
69+
if hasattr(AppStatus, "should_exit_event"): # pragma: no branch
70+
setattr(AppStatus, "should_exit_event", None) # pragma: lax no cover
71+
72+
3873
@asynccontextmanager
3974
async def streamable_http_security_client(
4075
security_settings: TransportSecuritySettings | None = None,

tests/shared/test_streamable_http.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import pytest
2020
from httpx_sse import ServerSentEvent
2121
from pydantic import AnyUrl
22+
from sse_starlette.sse import AppStatus
2223
from starlette.applications import Starlette
2324
from starlette.requests import Request
2425
from starlette.routing import Mount
@@ -88,6 +89,27 @@ def _collect_leaked_streams() -> Iterator[None]:
8889
gc.collect()
8990

9091

92+
@pytest.fixture(autouse=True)
93+
def _reset_sse_starlette_exit_event() -> Iterator[None]:
94+
"""Reset sse-starlette's module-global exit Event around each test.
95+
96+
sse-starlette <3.0 (allowed by this branch's dependency floor; CI's lowest-direct leg
97+
installs it) stores an `anyio.Event` on the `AppStatus` class the first time an
98+
`EventSourceResponse` runs; that Event is bound to the test's event loop and breaks every
99+
subsequent in-process SSE response. sse-starlette 3.x switched to a ContextVar and has no
100+
such attribute. Resetting on both sides of the test keeps this module immune to a stale
101+
Event left behind by an earlier test on the same worker as well as cleaning up after its
102+
own. This mirrors the autouse fixtures in tests/shared/test_sse.py and
103+
tests/interaction/conftest.py.
104+
"""
105+
if hasattr(AppStatus, "should_exit_event"): # pragma: no branch
106+
# setattr keeps pyright happy: the locked sse-starlette 3.x has no such attribute.
107+
setattr(AppStatus, "should_exit_event", None) # pragma: lax no cover
108+
yield
109+
if hasattr(AppStatus, "should_exit_event"): # pragma: no branch
110+
setattr(AppStatus, "should_exit_event", None) # pragma: lax no cover
111+
112+
91113
# Test constants
92114
SERVER_NAME = "test_streamable_http_server"
93115
INIT_REQUEST = {

0 commit comments

Comments
 (0)