Skip to content

Commit cdcb284

Browse files
authored
Upgrade websockets and FastAPI version in tests and dev (#662)
1 parent 81ec499 commit cdcb284

File tree

2 files changed

+77
-47
lines changed

2 files changed

+77
-47
lines changed

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ dev = [
4242
"trio>=0.25.0,<1.0",
4343
"trustme>=1.1.0,<2.0",
4444
"uvicorn>=0.29.0,<1.0",
45-
"websockets>=12.0,<13.0",
45+
"websockets>=14.0",
4646
"typing_extensions",
4747
]
4848
build = [
@@ -52,7 +52,7 @@ build = [
5252
test = [
5353
"charset_normalizer>=3.3.2,<4.0",
5454
"cryptography>=42.0.5,<43.0",
55-
"fastapi==0.110.0,<1.0",
55+
"fastapi>=0.110.0,<1.0",
5656
"httpx==0.23.1", # don't change, tests will raise "httpx.InvalidURL: Invalid URL component 'path'"
5757
"proxy.py>=2.4.3,<3.0",
5858
"pytest>=8.1.1,<9.0",
@@ -62,7 +62,7 @@ test = [
6262
"trio>=0.25.0,<1.0",
6363
"trustme>=1.1.0,<2.0",
6464
"uvicorn>=0.29.0,<1.0",
65-
"websockets>=12.0",
65+
"websockets>=14.0",
6666
"typing_extensions",
6767
]
6868

tests/unittest/conftest.py

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import asyncio
22
import contextlib
3+
from dataclasses import dataclass
34
import json
45
import os
6+
import queue
57
import threading
68
import time
79
import typing
@@ -15,6 +17,7 @@
1517
import trustme
1618
import uvicorn
1719
import websockets
20+
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
1821
from cryptography.hazmat.backends import default_backend
1922
from cryptography.hazmat.primitives.serialization import (
2023
BestAvailableEncryption,
@@ -613,30 +616,11 @@ async def watch_restarts(self): # pragma: nocover
613616
await self.startup()
614617

615618

616-
async def echo(websocket):
617-
while True:
618-
try:
619-
# Echo every text or binary message
620-
async for message in websocket:
621-
await websocket.send(message)
622-
623-
except websockets.exceptions.ConnectionClosed as e:
624-
# Client sent us a close frame: echo it back exactly
625-
await websocket.close(code=e.code, reason=e.reason)
626-
627-
628-
class TestWebsocketServer:
629-
def __init__(self, port):
630-
self.url = f"ws://127.0.0.1:{port}"
631-
self.port = port
632-
633-
def run(self):
634-
async def serve(port):
635-
# GitHub actions only likes 127, not localhost, wtf...
636-
async with websockets.serve(echo, "127.0.0.1", port): # pyright: ignore
637-
await asyncio.Future() # run forever
638-
639-
asyncio.run(serve(self.port))
619+
@pytest.fixture(scope="session")
620+
def server():
621+
config = Config(app=app, lifespan="off", loop="asyncio")
622+
server = TestServer(config=config)
623+
yield from serve_in_thread(server)
640624

641625

642626
def serve_in_thread(server: Server):
@@ -651,26 +635,6 @@ def serve_in_thread(server: Server):
651635
thread.join()
652636

653637

654-
@pytest.fixture(scope="session")
655-
def ws_server():
656-
server = TestWebsocketServer(port=8964)
657-
thread = threading.Thread(target=server.run, daemon=True)
658-
thread.start()
659-
try:
660-
time.sleep(2) # FIXME find a reliable way to check the server is up
661-
yield server
662-
finally:
663-
pass
664-
# thread.join()
665-
666-
667-
@pytest.fixture(scope="session")
668-
def server():
669-
config = Config(app=app, lifespan="off", loop="asyncio")
670-
server = TestServer(config=config)
671-
yield from serve_in_thread(server)
672-
673-
674638
@pytest.fixture(scope="session")
675639
def https_server(cert_pem_file, cert_private_key_file):
676640
config = Config(
@@ -685,6 +649,72 @@ def https_server(cert_pem_file, cert_private_key_file):
685649
yield from serve_in_thread(server)
686650

687651

652+
async def echo(ws):
653+
try:
654+
async for msg in ws:
655+
await ws.send(msg)
656+
except (ConnectionClosedOK, ConnectionClosedError):
657+
# Normal / abnormal close — nothing extra to do.
658+
pass
659+
660+
661+
def start_ws_server(port: int = 8964):
662+
"""
663+
Start a websockets server on 127.0.0.1:port in a background thread.
664+
Returns (url, stop) where stop() shuts it down.
665+
"""
666+
ready = threading.Event()
667+
stop_callable_q: queue.Queue[typing.Callable] = queue.Queue()
668+
669+
def _thread_target():
670+
loop = asyncio.new_event_loop()
671+
asyncio.set_event_loop(loop)
672+
673+
stop_async = asyncio.Event()
674+
675+
def _stop():
676+
# can be called from main thread
677+
loop.call_soon_threadsafe(stop_async.set)
678+
679+
async def _run():
680+
async with websockets.serve(echo, "127.0.0.1", port) as _:
681+
stop_callable_q.put(_stop)
682+
ready.set()
683+
await stop_async.wait()
684+
685+
try:
686+
loop.run_until_complete(_run())
687+
finally:
688+
loop.run_until_complete(loop.shutdown_asyncgens())
689+
loop.close()
690+
691+
t = threading.Thread(target=_thread_target, daemon=True)
692+
t.start()
693+
694+
# Wait until server is really listening and we have a stop() handle
695+
stop = stop_callable_q.get() # blocks until put()
696+
ready.wait() # the socket is bound now
697+
698+
url = f"ws://127.0.0.1:{port}"
699+
return url, stop, t
700+
701+
702+
@dataclass
703+
class WSServer:
704+
url: str
705+
stop: typing.Callable
706+
707+
708+
@pytest.fixture(scope="session")
709+
def ws_server():
710+
url, stop, thread = start_ws_server(port=8964)
711+
try:
712+
yield WSServer(url=url, stop=stop)
713+
finally:
714+
stop() # trigger graceful shutdown
715+
thread.join(5) # optional: wait up to 5s for thread to exit
716+
717+
688718
@pytest.fixture(scope="session")
689719
def proxy_server(request):
690720
ps = proxy.Proxy(port=8002, plugins=["proxy.plugin.ManInTheMiddlePlugin"])

0 commit comments

Comments
 (0)