|
1 | 1 | """Tests for StreamableHTTP server DNS rebinding protection.""" |
2 | 2 |
|
3 | | -from collections.abc import AsyncIterator |
| 3 | +import gc |
| 4 | +from collections.abc import AsyncIterator, Iterator |
4 | 5 | from contextlib import asynccontextmanager |
5 | 6 |
|
6 | 7 | import httpx |
7 | 8 | import pytest |
| 9 | +from sse_starlette.sse import AppStatus |
8 | 10 | from starlette.applications import Starlette |
9 | 11 | from starlette.routing import Mount |
10 | 12 |
|
|
23 | 25 | # run in process; the old subprocess harness never observed them. The interaction suite registers |
24 | 26 | # the same two scoped filters globally from tests/interaction/conftest.py (see the comment there), |
25 | 27 | # 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. |
32 | 30 | pytestmark = [ |
33 | 31 | pytest.mark.filterwarnings("ignore:.*MemoryObject(Send|Receive)Stream:pytest.PytestUnraisableExceptionWarning"), |
34 | 32 | pytest.mark.filterwarnings("ignore:.*MemoryObject(Send|Receive)Stream:ResourceWarning"), |
35 | 33 | ] |
36 | 34 |
|
37 | 35 |
|
| 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 | + |
38 | 73 | @asynccontextmanager |
39 | 74 | async def streamable_http_security_client( |
40 | 75 | security_settings: TransportSecuritySettings | None = None, |
|
0 commit comments