Skip to content

Commit f7b15f4

Browse files
committed
Fix backend fixture
1 parent 3d2941e commit f7b15f4

File tree

3 files changed

+45
-49
lines changed

3 files changed

+45
-49
lines changed

src/reactpy/testing/backend.py

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import asyncio
44
import logging
5-
from contextlib import AsyncExitStack, suppress
5+
from collections.abc import Coroutine
6+
from contextlib import AsyncExitStack
7+
from threading import Thread
68
from types import TracebackType
79
from typing import Any, Callable
810
from urllib.parse import urlencode, urlunparse
911

10-
from reactpy.asgi import default as default_server
11-
from reactpy.asgi.utils import find_available_port
12+
import uvicorn
13+
14+
from reactpy.asgi.standalone import ReactPy
1215
from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT
1316
from reactpy.core.component import component
1417
from reactpy.core.hooks import use_callback, use_effect, use_state
@@ -17,7 +20,8 @@
1720
capture_reactpy_logs,
1821
list_logged_exceptions,
1922
)
20-
from reactpy.types import BackendType, ComponentConstructor
23+
from reactpy.testing.utils import find_available_port
24+
from reactpy.types import ComponentConstructor
2125
from reactpy.utils import Ref
2226

2327

@@ -39,11 +43,9 @@ class BackendFixture:
3943

4044
def __init__(
4145
self,
46+
app: Callable[..., Coroutine] | None = None,
4247
host: str = "127.0.0.1",
4348
port: int | None = None,
44-
app: Any | None = None,
45-
implementation: BackendType[Any] | None = None,
46-
options: Any | None = None,
4749
timeout: float | None = None,
4850
) -> None:
4951
self.host = host
@@ -52,14 +54,12 @@ def __init__(
5254
self.timeout = (
5355
REACTPY_TESTS_DEFAULT_TIMEOUT.current if timeout is None else timeout
5456
)
55-
56-
if app is not None and implementation is None:
57-
msg = "If an application instance its corresponding server implementation must be provided too."
58-
raise ValueError(msg)
59-
60-
self._app = app
61-
self.implementation = implementation or default_server
62-
self._options = options
57+
self._app = app or ReactPy(self._root_component)
58+
self.webserver = uvicorn.Server(
59+
uvicorn.Config(
60+
app=self._app, host=self.host, port=self.port, loop="asyncio"
61+
)
62+
)
6363

6464
@property
6565
def log_records(self) -> list[logging.LogRecord]:
@@ -109,30 +109,7 @@ def list_logged_exceptions(
109109
async def __aenter__(self) -> BackendFixture:
110110
self._exit_stack = AsyncExitStack()
111111
self._records = self._exit_stack.enter_context(capture_reactpy_logs())
112-
113-
app = self._app or self.implementation.create_development_app()
114-
self.implementation.configure(app, self._root_component, self._options)
115-
116-
started = asyncio.Event()
117-
server_future = asyncio.create_task(
118-
self.implementation.serve_development_app(
119-
app, self.host, self.port, started
120-
)
121-
)
122-
123-
async def stop_server() -> None:
124-
server_future.cancel()
125-
with suppress(asyncio.CancelledError):
126-
await asyncio.wait_for(server_future, timeout=self.timeout)
127-
128-
self._exit_stack.push_async_callback(stop_server)
129-
130-
try:
131-
await asyncio.wait_for(started.wait(), timeout=self.timeout)
132-
except Exception: # nocov
133-
# see if we can await the future for a more helpful error
134-
await asyncio.wait_for(server_future, timeout=self.timeout)
135-
raise
112+
Thread(target=self.webserver.run, daemon=True).start()
136113

137114
return self
138115

@@ -151,6 +128,8 @@ async def __aexit__(
151128
msg = "Unexpected logged exception"
152129
raise LogAssertionError(msg) from logged_errors[0]
153130

131+
await asyncio.wait_for(self.webserver.shutdown(), timeout=5)
132+
154133

155134
_MountFunc = Callable[["Callable[[], Any] | None"], None]
156135

src/reactpy/testing/display.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ def __init__(
2626
self,
2727
backend: BackendFixture | None = None,
2828
driver: Browser | BrowserContext | Page | None = None,
29-
url_prefix: str = "",
3029
) -> None:
3130
if backend is not None:
3231
self.backend = backend
@@ -35,7 +34,6 @@ def __init__(
3534
self.page = driver
3635
else:
3736
self._browser = driver
38-
self.url_prefix = url_prefix
3937

4038
async def show(
4139
self,
@@ -45,14 +43,8 @@ async def show(
4543
await self.goto("/")
4644
await self.root_element() # check that root element is attached
4745

48-
async def goto(
49-
self, path: str, query: Any | None = None, add_url_prefix: bool = True
50-
) -> None:
51-
await self.page.goto(
52-
self.backend.url(
53-
f"{self.url_prefix}{path}" if add_url_prefix else path, query
54-
)
55-
)
46+
async def goto(self, path: str, query: Any | None = None) -> None:
47+
await self.page.goto(self.backend.url(path, query))
5648

5749
async def root_element(self) -> ElementHandle:
5850
element = await self.page.wait_for_selector("#app", state="attached")

src/reactpy/testing/utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
import socket
4+
import sys
5+
from contextlib import closing
6+
7+
8+
def find_available_port(host: str, port_min: int = 8000, port_max: int = 9000) -> int:
9+
"""Get a port that's available for the given host and port range"""
10+
for port in range(port_min, port_max):
11+
with closing(socket.socket()) as sock:
12+
try:
13+
if sys.platform in ("linux", "darwin"):
14+
# Fixes bug on Unix-like systems where every time you restart the
15+
# server you'll get a different port on Linux. This cannot be set
16+
# on Windows otherwise address will always be reused.
17+
# Ref: https://stackoverflow.com/a/19247688/3159288
18+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
19+
sock.bind((host, port))
20+
except OSError:
21+
pass
22+
else:
23+
return port
24+
msg = f"Host {host!r} has no available port in range {port_max}-{port_max}"
25+
raise RuntimeError(msg)

0 commit comments

Comments
 (0)