From 955e44e95c9a1dc5c6be526b158d0b4eaf506c2f Mon Sep 17 00:00:00 2001 From: aranvir <75439739+aranvir@users.noreply.github.com> Date: Sat, 3 Aug 2024 14:09:45 +0200 Subject: [PATCH 1/6] added subprocess test client --- docs/examples/testing/subprocess_sse_app.py | 38 ++++++ docs/examples/testing/test_subprocess_sse.py | 72 ++++++++++++ docs/usage/testing.rst | 13 +++ litestar/testing/__init__.py | 3 + litestar/testing/client/subprocess_client.py | 70 ++++++++++++ .../test_testing/test_sub_client/__init__.py | 0 .../unit/test_testing/test_sub_client/demo.py | 38 ++++++ .../test_sub_client/test_subprocess_client.py | 108 ++++++++++++++++++ 8 files changed, 342 insertions(+) create mode 100644 docs/examples/testing/subprocess_sse_app.py create mode 100644 docs/examples/testing/test_subprocess_sse.py create mode 100644 litestar/testing/client/subprocess_client.py create mode 100644 tests/unit/test_testing/test_sub_client/__init__.py create mode 100644 tests/unit/test_testing/test_sub_client/demo.py create mode 100644 tests/unit/test_testing/test_sub_client/test_subprocess_client.py diff --git a/docs/examples/testing/subprocess_sse_app.py b/docs/examples/testing/subprocess_sse_app.py new file mode 100644 index 0000000000..0e922a1f93 --- /dev/null +++ b/docs/examples/testing/subprocess_sse_app.py @@ -0,0 +1,38 @@ +""" +Assemble components into an app that shall be tested +""" + +from typing import AsyncIterator + +from redis.asyncio import Redis + +from litestar import Litestar, get +from litestar.channels import ChannelsPlugin +from litestar.channels.backends.redis import RedisChannelsPubSubBackend +from litestar.response import ServerSentEvent + + +@get("/notify/{topic:str}") +async def get_notified(topic: str, channels: ChannelsPlugin) -> ServerSentEvent: + async def generator() -> AsyncIterator[bytes]: + async with channels.start_subscription([topic]) as subscriber: + async for event in subscriber.iter_events(): + yield event + + return ServerSentEvent(generator(), event_type="Notifier") + + +def create_test_app() -> Litestar: + redis_instance = Redis() + channels_backend = RedisChannelsPubSubBackend(redis=redis_instance) + channels_instance = ChannelsPlugin(backend=channels_backend, arbitrary_channels_allowed=True) + + return Litestar( + route_handlers=[ + get_notified, + ], + plugins=[channels_instance], + ) + + +app = create_test_app() diff --git a/docs/examples/testing/test_subprocess_sse.py b/docs/examples/testing/test_subprocess_sse.py new file mode 100644 index 0000000000..900b6b9896 --- /dev/null +++ b/docs/examples/testing/test_subprocess_sse.py @@ -0,0 +1,72 @@ +""" +Test the app running in a subprocess +""" + +import asyncio +import pathlib +import sys +from typing import AsyncIterator + +import httpx +import httpx_sse +import pytest +from redis.asyncio import Redis + +from litestar.channels import ChannelsPlugin +from litestar.channels.backends.redis import RedisChannelsPubSubBackend +from litestar.testing import subprocess_async_client + +if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +pytestmark = pytest.mark.anyio + + +@pytest.fixture(scope="session") +def anyio_backend() -> str: + return "asyncio" + + +ROOT = pathlib.Path(__file__).parent + + +@pytest.fixture(name="async_client", scope="session") +async def fx_async_client() -> AsyncIterator[httpx.AsyncClient]: + async with subprocess_async_client(workdir=ROOT, app="subprocess_sse_app:app") as client: + yield client + + +@pytest.fixture(name="redis_channels") +async def fx_redis_channels() -> AsyncIterator[ChannelsPlugin]: + # Expects separate redis set-up + redis_instance = Redis() + channels_backend = RedisChannelsPubSubBackend(redis=redis_instance) + channels_instance = ChannelsPlugin(backend=channels_backend, arbitrary_channels_allowed=True) + await channels_instance._on_startup() + yield channels_instance + await channels_instance._on_shutdown() + + +async def test_subprocess_async_client(async_client: httpx.AsyncClient, redis_channels: ChannelsPlugin) -> None: + """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the + regular async test client. + """ + topic = "demo" + message = "hello" + + running = asyncio.Event() + running.set() + + async def send_notifications() -> None: + while running.is_set(): + await redis_channels.wait_published(message, channels=[topic]) + await asyncio.sleep(0.1) + + task = asyncio.create_task(send_notifications()) + + async with httpx_sse.aconnect_sse(async_client, "GET", f"/notify/{topic}") as event_source: + async for event in event_source.aiter_sse(): + assert event.data == message + running.clear() + break + await task diff --git a/docs/usage/testing.rst b/docs/usage/testing.rst index 90102f640d..5bc5da8639 100644 --- a/docs/usage/testing.rst +++ b/docs/usage/testing.rst @@ -279,6 +279,19 @@ But also this: assert response.text == "healthy" +Creating an external test app +------------------- + +The test clients make use of the capability to directly load an ASGI app into an httpx Client without having to run an actual server like uvicorn. For most test cases, this is sufficient but there are some situations where this will not work. For example, when using server-sent events with an infinite generator, it will lock up the test client, since it tries to first exhaust the generator and then return to the test function. + +Litestar offers two helper functions called :func:`subprocess_sync_client ` and :func:`subprocess_async_client ` that will launch a litestar instance with uvicorn in a subprocess and set up an httpx client for running tests. You can either load your actual app file or create subsets from it as you would with the regular test client setup. An example is shown below. + +.. literalinclude:: /examples/testing/subprocess_sse_app.py + :language: python + +.. literalinclude:: /examples/testing/test_subprocess_sse.py + :language: python + RequestFactory -------------- diff --git a/litestar/testing/__init__.py b/litestar/testing/__init__.py index 55af446ac5..10aa2c6bf7 100644 --- a/litestar/testing/__init__.py +++ b/litestar/testing/__init__.py @@ -1,5 +1,6 @@ from litestar.testing.client.async_client import AsyncTestClient from litestar.testing.client.base import BaseTestClient +from litestar.testing.client.subprocess_client import subprocess_async_client, subprocess_sync_client from litestar.testing.client.sync_client import TestClient from litestar.testing.helpers import create_async_test_client, create_test_client from litestar.testing.request_factory import RequestFactory @@ -13,4 +14,6 @@ "RequestFactory", "TestClient", "WebSocketTestSession", + "subprocess_sync_client", + "subprocess_async_client", ) diff --git a/litestar/testing/client/subprocess_client.py b/litestar/testing/client/subprocess_client.py new file mode 100644 index 0000000000..337e302a59 --- /dev/null +++ b/litestar/testing/client/subprocess_client.py @@ -0,0 +1,70 @@ +import pathlib +import socket +import subprocess +import time +from contextlib import asynccontextmanager, contextmanager +from typing import AsyncIterator, Iterator + +import httpx + + +class StartupError(RuntimeError): + pass + + +def _get_available_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + # Bind to a free port provided by the host + try: + sock.bind(("localhost", 0)) + except OSError as e: + raise StartupError("Could not find an open port") from e + else: + port: int = sock.getsockname()[1] + return port + + +@contextmanager +def run_app(workdir: pathlib.Path, app: str) -> Iterator[str]: + """Launch a litestar application in a subprocess with a random available port.""" + port = _get_available_port() + proc = subprocess.Popen( + args=["litestar", "--app", app, "run", "--port", str(port)], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + cwd=workdir, + ) + url = f"http://127.0.0.1:{port}" + for _ in range(100): + try: + httpx.get(url, timeout=0.1) + break + except httpx.TransportError: + time.sleep(1) + yield url + proc.kill() + + +@asynccontextmanager +async def subprocess_async_client(workdir: pathlib.Path, app: str) -> AsyncIterator[httpx.AsyncClient]: + """Provides an async httpx client for a litestar app launched in a subprocess. + + Args: + workdir: Path to the directory in which the app module resides. + app: Uvicorn app string that can be resolved in the provided working directory, e.g.: "app:app" + """ + with run_app(workdir=workdir, app=app) as url: + async with httpx.AsyncClient(base_url=url) as client: + yield client + + +@contextmanager +def subprocess_sync_client(workdir: pathlib.Path, app: str) -> Iterator[httpx.Client]: + """Provides a sync httpx client for a litestar app launched in a subprocess. + + Args: + workdir: Path to the directory in which the app module resides. + app: Uvicorn app string that can be resolved in the provided working directory, e.g.: "app:app" + """ + with run_app(workdir=workdir, app=app) as url, httpx.Client(base_url=url) as client: + yield client diff --git a/tests/unit/test_testing/test_sub_client/__init__.py b/tests/unit/test_testing/test_sub_client/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_testing/test_sub_client/demo.py b/tests/unit/test_testing/test_sub_client/demo.py new file mode 100644 index 0000000000..0e922a1f93 --- /dev/null +++ b/tests/unit/test_testing/test_sub_client/demo.py @@ -0,0 +1,38 @@ +""" +Assemble components into an app that shall be tested +""" + +from typing import AsyncIterator + +from redis.asyncio import Redis + +from litestar import Litestar, get +from litestar.channels import ChannelsPlugin +from litestar.channels.backends.redis import RedisChannelsPubSubBackend +from litestar.response import ServerSentEvent + + +@get("/notify/{topic:str}") +async def get_notified(topic: str, channels: ChannelsPlugin) -> ServerSentEvent: + async def generator() -> AsyncIterator[bytes]: + async with channels.start_subscription([topic]) as subscriber: + async for event in subscriber.iter_events(): + yield event + + return ServerSentEvent(generator(), event_type="Notifier") + + +def create_test_app() -> Litestar: + redis_instance = Redis() + channels_backend = RedisChannelsPubSubBackend(redis=redis_instance) + channels_instance = ChannelsPlugin(backend=channels_backend, arbitrary_channels_allowed=True) + + return Litestar( + route_handlers=[ + get_notified, + ], + plugins=[channels_instance], + ) + + +app = create_test_app() diff --git a/tests/unit/test_testing/test_sub_client/test_subprocess_client.py b/tests/unit/test_testing/test_sub_client/test_subprocess_client.py new file mode 100644 index 0000000000..9c8d38338b --- /dev/null +++ b/tests/unit/test_testing/test_sub_client/test_subprocess_client.py @@ -0,0 +1,108 @@ +""" +Test the app running in a subprocess +""" + +import asyncio +import pathlib +import sys +import threading +from typing import Any, AsyncIterator, Iterator + +import httpx +import httpx_sse +import pytest +from redis.asyncio import Redis + +from litestar.channels import ChannelsPlugin +from litestar.channels.backends.redis import RedisChannelsPubSubBackend +from litestar.testing import subprocess_async_client, subprocess_sync_client + +if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +pytestmark = pytest.mark.anyio + + +@pytest.fixture(scope="session") +def anyio_backend() -> str: + return "asyncio" + + +ROOT = pathlib.Path(__file__).parent + + +@pytest.fixture(name="async_client", scope="session") +async def fx_async_client() -> AsyncIterator[httpx.AsyncClient]: + async with subprocess_async_client(workdir=ROOT, app="demo:app") as client: + yield client + + +@pytest.fixture(name="sync_client", scope="session") +def fx_sync_client() -> Iterator[httpx.Client]: + with subprocess_sync_client(workdir=ROOT, app="demo:app") as client: + yield client + + +@pytest.fixture(name="redis_channels") +async def fx_redis_channels(redis_service: Any) -> AsyncIterator[ChannelsPlugin]: + redis_instance = Redis() + channels_backend = RedisChannelsPubSubBackend(redis=redis_instance) + channels_instance = ChannelsPlugin(backend=channels_backend, arbitrary_channels_allowed=True) + await channels_instance._on_startup() + yield channels_instance + await channels_instance._on_shutdown() + + +async def test_subprocess_async_client(async_client: httpx.AsyncClient, redis_channels: ChannelsPlugin) -> None: + """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the + regular async test client. + """ + topic = "demo" + message = "hello" + + running = asyncio.Event() + running.set() + + async def send_notifications() -> None: + while running.is_set(): + await redis_channels.wait_published(message, channels=[topic]) + await asyncio.sleep(0.1) + + task = asyncio.create_task(send_notifications()) + + async with httpx_sse.aconnect_sse(async_client, "GET", f"/notify/{topic}") as event_source: + async for event in event_source.aiter_sse(): + assert event.data == message + running.clear() + break + await task + + +async def test_subprocess_sync_client(sync_client: httpx.Client, redis_channels: ChannelsPlugin) -> None: + """Demonstrates functionality of the sync client with an infinite SSE source that cannot be tested with the + regular sync test client. + """ + topic = "demo" + message = "hello" + + running = threading.Event() + running.set() + + async def send_notifications() -> None: + while running.is_set(): + await redis_channels.wait_published(message, channels=[topic]) + await asyncio.sleep(0.1) + + task = asyncio.create_task(send_notifications()) + + def consume_notifications() -> None: + with httpx_sse.connect_sse(sync_client, "GET", f"/notify/{topic}") as event_source: + for event in event_source.iter_sse(): + assert event.data == message + running.clear() + break + + thread_consume = threading.Thread(target=consume_notifications, daemon=True) + thread_consume.start() + await task + thread_consume.join() From 2474a99034387876c48c4550c585b6da8cd78fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= Date: Sun, 15 Sep 2024 15:40:53 +0200 Subject: [PATCH 2/6] Apply suggestions from code review --- docs/usage/testing.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage/testing.rst b/docs/usage/testing.rst index 5bc5da8639..35826cc7b6 100644 --- a/docs/usage/testing.rst +++ b/docs/usage/testing.rst @@ -280,11 +280,11 @@ But also this: Creating an external test app -------------------- +------------------------------ -The test clients make use of the capability to directly load an ASGI app into an httpx Client without having to run an actual server like uvicorn. For most test cases, this is sufficient but there are some situations where this will not work. For example, when using server-sent events with an infinite generator, it will lock up the test client, since it tries to first exhaust the generator and then return to the test function. +The test clients make use of the capability to directly load an ASGI app into an httpx Client without having to run an actual server like uvicorn. For most test cases, this is sufficient but there are some situations where this will not work. For example, when using server-sent events with an infinite generator, due to the way HTTPX (which the Litestar test clients are built upon) works internally with ASGI applications, it will lock up the test client, since it tries to first exhaust the generator and then return to the test function. -Litestar offers two helper functions called :func:`subprocess_sync_client ` and :func:`subprocess_async_client ` that will launch a litestar instance with uvicorn in a subprocess and set up an httpx client for running tests. You can either load your actual app file or create subsets from it as you would with the regular test client setup. An example is shown below. +Litestar offers two helper functions, :func:`~litestar.testing.client.subprocess_client.subprocess_sync_client` and :func:`~litestar.testing.client.subprocess_client.subprocess_async_client` that will launch a Litestar instance with in a subprocess and set up an httpx client for running tests. You can either load your actual app file or create subsets from it as you would with the regular test client setup. An example is shown below. .. literalinclude:: /examples/testing/subprocess_sse_app.py :language: python From b41444bd4faf188893243fed63968faa2425e5ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= <25355197+provinzkraut@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:56:50 +0200 Subject: [PATCH 3/6] Fix tests --- .../unit/test_testing/test_sub_client/demo.py | 22 ++---- .../test_sub_client/test_subprocess_client.py | 78 +++---------------- 2 files changed, 18 insertions(+), 82 deletions(-) diff --git a/tests/unit/test_testing/test_sub_client/demo.py b/tests/unit/test_testing/test_sub_client/demo.py index 0e922a1f93..7a14dea65e 100644 --- a/tests/unit/test_testing/test_sub_client/demo.py +++ b/tests/unit/test_testing/test_sub_client/demo.py @@ -2,36 +2,26 @@ Assemble components into an app that shall be tested """ +import asyncio from typing import AsyncIterator -from redis.asyncio import Redis - from litestar import Litestar, get -from litestar.channels import ChannelsPlugin -from litestar.channels.backends.redis import RedisChannelsPubSubBackend from litestar.response import ServerSentEvent @get("/notify/{topic:str}") -async def get_notified(topic: str, channels: ChannelsPlugin) -> ServerSentEvent: +async def get_notified(topic: str) -> ServerSentEvent: async def generator() -> AsyncIterator[bytes]: - async with channels.start_subscription([topic]) as subscriber: - async for event in subscriber.iter_events(): - yield event + yield topic + while True: + await asyncio.sleep(0.1) return ServerSentEvent(generator(), event_type="Notifier") def create_test_app() -> Litestar: - redis_instance = Redis() - channels_backend = RedisChannelsPubSubBackend(redis=redis_instance) - channels_instance = ChannelsPlugin(backend=channels_backend, arbitrary_channels_allowed=True) - return Litestar( - route_handlers=[ - get_notified, - ], - plugins=[channels_instance], + route_handlers=[get_notified], ) diff --git a/tests/unit/test_testing/test_sub_client/test_subprocess_client.py b/tests/unit/test_testing/test_sub_client/test_subprocess_client.py index 9c8d38338b..ff3784fef8 100644 --- a/tests/unit/test_testing/test_sub_client/test_subprocess_client.py +++ b/tests/unit/test_testing/test_sub_client/test_subprocess_client.py @@ -5,28 +5,17 @@ import asyncio import pathlib import sys -import threading -from typing import Any, AsyncIterator, Iterator +from typing import AsyncIterator, Iterator import httpx import httpx_sse import pytest -from redis.asyncio import Redis -from litestar.channels import ChannelsPlugin -from litestar.channels.backends.redis import RedisChannelsPubSubBackend from litestar.testing import subprocess_async_client, subprocess_sync_client if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) -pytestmark = pytest.mark.anyio - - -@pytest.fixture(scope="session") -def anyio_backend() -> str: - return "asyncio" - ROOT = pathlib.Path(__file__).parent @@ -43,66 +32,23 @@ def fx_sync_client() -> Iterator[httpx.Client]: yield client -@pytest.fixture(name="redis_channels") -async def fx_redis_channels(redis_service: Any) -> AsyncIterator[ChannelsPlugin]: - redis_instance = Redis() - channels_backend = RedisChannelsPubSubBackend(redis=redis_instance) - channels_instance = ChannelsPlugin(backend=channels_backend, arbitrary_channels_allowed=True) - await channels_instance._on_startup() - yield channels_instance - await channels_instance._on_shutdown() - - -async def test_subprocess_async_client(async_client: httpx.AsyncClient, redis_channels: ChannelsPlugin) -> None: +async def test_subprocess_async_client(async_client: httpx.AsyncClient) -> None: """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the regular async test client. """ - topic = "demo" - message = "hello" - - running = asyncio.Event() - running.set() - async def send_notifications() -> None: - while running.is_set(): - await redis_channels.wait_published(message, channels=[topic]) - await asyncio.sleep(0.1) - - task = asyncio.create_task(send_notifications()) - - async with httpx_sse.aconnect_sse(async_client, "GET", f"/notify/{topic}") as event_source: + async with httpx_sse.aconnect_sse(async_client, "GET", "/notify/hello") as event_source: async for event in event_source.aiter_sse(): - assert event.data == message - running.clear() + assert event.data == "hello" break - await task -async def test_subprocess_sync_client(sync_client: httpx.Client, redis_channels: ChannelsPlugin) -> None: - """Demonstrates functionality of the sync client with an infinite SSE source that cannot be tested with the - regular sync test client. +def test_subprocess_sync_client(sync_client: httpx.AsyncClient) -> None: + """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the + regular async test client. """ - topic = "demo" - message = "hello" - - running = threading.Event() - running.set() - - async def send_notifications() -> None: - while running.is_set(): - await redis_channels.wait_published(message, channels=[topic]) - await asyncio.sleep(0.1) - - task = asyncio.create_task(send_notifications()) - - def consume_notifications() -> None: - with httpx_sse.connect_sse(sync_client, "GET", f"/notify/{topic}") as event_source: - for event in event_source.iter_sse(): - assert event.data == message - running.clear() - break - - thread_consume = threading.Thread(target=consume_notifications, daemon=True) - thread_consume.start() - await task - thread_consume.join() + + with httpx_sse.connect_sse(sync_client, "GET", "/notify/hello") as event_source: + for event in event_source.iter_sse(): + assert event.data == "hello" + break From 23ac52708ac6854d00a6523833dfd5e786c1f87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= <25355197+provinzkraut@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:56:56 +0200 Subject: [PATCH 4/6] Reword docs --- docs/reference/testing.rst | 2 +- docs/usage/testing.rst | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/reference/testing.rst b/docs/reference/testing.rst index 85767df5bf..ebc9bc1044 100644 --- a/docs/reference/testing.rst +++ b/docs/reference/testing.rst @@ -3,7 +3,7 @@ testing .. automodule:: litestar.testing - :members: RequestFactory, BaseTestClient, TestClient, AsyncTestClient, create_async_test_client, create_test_client + :members: RequestFactory, BaseTestClient, TestClient, AsyncTestClient, create_async_test_client, create_test_client, subprocess_sync_client, subprocess_async_client :undoc-members: WebSocketTestSession diff --git a/docs/usage/testing.rst b/docs/usage/testing.rst index 35826cc7b6..4eb7b8623e 100644 --- a/docs/usage/testing.rst +++ b/docs/usage/testing.rst @@ -279,12 +279,24 @@ But also this: assert response.text == "healthy" -Creating an external test app ------------------------------- - -The test clients make use of the capability to directly load an ASGI app into an httpx Client without having to run an actual server like uvicorn. For most test cases, this is sufficient but there are some situations where this will not work. For example, when using server-sent events with an infinite generator, due to the way HTTPX (which the Litestar test clients are built upon) works internally with ASGI applications, it will lock up the test client, since it tries to first exhaust the generator and then return to the test function. - -Litestar offers two helper functions, :func:`~litestar.testing.client.subprocess_client.subprocess_sync_client` and :func:`~litestar.testing.client.subprocess_client.subprocess_async_client` that will launch a Litestar instance with in a subprocess and set up an httpx client for running tests. You can either load your actual app file or create subsets from it as you would with the regular test client setup. An example is shown below. +Running a live server +--------------------- + +The test clients make use of HTTPX's ability to directly call into an ASGI app, without +having to run an actual server. In most cases this is sufficient but there are some +exceptions where this won't work, due to the limitations of the emulated client-server +communication. + +For example, when using server-sent events with an infinite generator, it will lock up +the test client, since HTTPX tries to consume the full response before returning a +request. + +Litestar offers two helper functions, +:func:`~litestar.testing.client.subprocess_client.subprocess_sync_client` and +:func:`~litestar.testing.client.subprocess_client.subprocess_async_client` that will +launch a Litestar instance with in a subprocess and set up an httpx client for running +tests. You can either load your actual app file or create subsets from it as you would +with the regular test client setup: .. literalinclude:: /examples/testing/subprocess_sse_app.py :language: python From 34bbe6c229ec827e0f6eed849a73a83eed75b976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= <25355197+provinzkraut@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:58:19 +0200 Subject: [PATCH 5/6] Use context manager --- litestar/testing/client/subprocess_client.py | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/litestar/testing/client/subprocess_client.py b/litestar/testing/client/subprocess_client.py index 337e302a59..7f91c8f338 100644 --- a/litestar/testing/client/subprocess_client.py +++ b/litestar/testing/client/subprocess_client.py @@ -28,21 +28,21 @@ def _get_available_port() -> int: def run_app(workdir: pathlib.Path, app: str) -> Iterator[str]: """Launch a litestar application in a subprocess with a random available port.""" port = _get_available_port() - proc = subprocess.Popen( + with subprocess.Popen( args=["litestar", "--app", app, "run", "--port", str(port)], stderr=subprocess.PIPE, stdout=subprocess.PIPE, cwd=workdir, - ) - url = f"http://127.0.0.1:{port}" - for _ in range(100): - try: - httpx.get(url, timeout=0.1) - break - except httpx.TransportError: - time.sleep(1) - yield url - proc.kill() + ) as proc: + url = f"http://127.0.0.1:{port}" + for _ in range(100): + try: + httpx.get(url, timeout=0.1) + break + except httpx.TransportError: + time.sleep(1) + yield url + proc.kill() @asynccontextmanager From 7a0659b79944effd8e4b7ee780c515fe9e892947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= <25355197+provinzkraut@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:59:59 +0200 Subject: [PATCH 6/6] Fix types --- tests/unit/test_testing/test_sub_client/demo.py | 2 +- .../unit/test_testing/test_sub_client/test_subprocess_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_testing/test_sub_client/demo.py b/tests/unit/test_testing/test_sub_client/demo.py index 7a14dea65e..dce3de56cb 100644 --- a/tests/unit/test_testing/test_sub_client/demo.py +++ b/tests/unit/test_testing/test_sub_client/demo.py @@ -11,7 +11,7 @@ @get("/notify/{topic:str}") async def get_notified(topic: str) -> ServerSentEvent: - async def generator() -> AsyncIterator[bytes]: + async def generator() -> AsyncIterator[str]: yield topic while True: await asyncio.sleep(0.1) diff --git a/tests/unit/test_testing/test_sub_client/test_subprocess_client.py b/tests/unit/test_testing/test_sub_client/test_subprocess_client.py index ff3784fef8..4096ee1a61 100644 --- a/tests/unit/test_testing/test_sub_client/test_subprocess_client.py +++ b/tests/unit/test_testing/test_sub_client/test_subprocess_client.py @@ -43,7 +43,7 @@ async def test_subprocess_async_client(async_client: httpx.AsyncClient) -> None: break -def test_subprocess_sync_client(sync_client: httpx.AsyncClient) -> None: +def test_subprocess_sync_client(sync_client: httpx.Client) -> None: """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the regular async test client. """