-
-
Notifications
You must be signed in to change notification settings - Fork 397
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added subprocess test client (#3655)
--------- Co-authored-by: Janek Nouvertné <[email protected]>
- Loading branch information
1 parent
93aa6bf
commit 8d67510
Showing
9 changed files
with
250 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
""" | ||
Assemble components into an app that shall be tested | ||
""" | ||
|
||
from typing import AsyncGenerator | ||
|
||
from litestar import Litestar, get | ||
from litestar.response import ServerSentEvent | ||
from litestar.types import SSEData | ||
|
||
|
||
async def generator(topic: str) -> AsyncGenerator[SSEData, None]: | ||
count = 0 | ||
while count < 2: | ||
yield topic | ||
count += 1 | ||
|
||
|
||
@get("/notify/{topic:str}") | ||
async def get_notified(topic: str) -> ServerSentEvent: | ||
return ServerSentEvent(generator(topic), event_type="Notifier") | ||
|
||
|
||
app = Litestar(route_handlers=[get_notified]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
""" | ||
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 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 | ||
|
||
|
||
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" | ||
|
||
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 == topic | ||
break |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: # pragma: no cover | ||
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() | ||
with subprocess.Popen( | ||
args=["litestar", "--app", app, "run", "--port", str(port)], | ||
stderr=subprocess.PIPE, | ||
stdout=subprocess.PIPE, | ||
cwd=workdir, | ||
) as proc: | ||
url = f"http://127.0.0.1:{port}" | ||
for _ in range(100): # pragma: no cover | ||
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 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
""" | ||
Assemble components into an app that shall be tested | ||
""" | ||
|
||
import asyncio | ||
from typing import AsyncIterator | ||
|
||
from litestar import Litestar, get | ||
from litestar.response import ServerSentEvent | ||
|
||
|
||
@get("/notify/{topic:str}") | ||
async def get_notified(topic: str) -> ServerSentEvent: | ||
async def generator() -> AsyncIterator[str]: | ||
yield topic | ||
while True: | ||
await asyncio.sleep(0.1) | ||
|
||
return ServerSentEvent(generator(), event_type="Notifier") | ||
|
||
|
||
def create_test_app() -> Litestar: | ||
return Litestar( | ||
route_handlers=[get_notified], | ||
) | ||
|
||
|
||
app = create_test_app() |
54 changes: 54 additions & 0 deletions
54
tests/unit/test_testing/test_sub_client/test_subprocess_client.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
""" | ||
Test the app running in a subprocess | ||
""" | ||
|
||
import asyncio | ||
import pathlib | ||
import sys | ||
from typing import AsyncIterator, Iterator | ||
|
||
import httpx | ||
import httpx_sse | ||
import pytest | ||
|
||
from litestar.testing import subprocess_async_client, subprocess_sync_client | ||
|
||
if sys.platform == "win32": | ||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) | ||
|
||
|
||
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 | ||
|
||
|
||
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. | ||
""" | ||
|
||
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 == "hello" | ||
break | ||
|
||
|
||
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. | ||
""" | ||
|
||
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 |