Skip to content

Commit 44c4077

Browse files
committed
Test on Trio as well
1 parent 1b9ba5f commit 44c4077

8 files changed

+149
-94
lines changed

pyproject.toml

+4-3
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ test = [
3838
"mypy",
3939
"pre-commit",
4040
"pytest",
41-
"pytest-asyncio",
42-
"websockets >=10.0",
43-
"uvicorn",
41+
"httpx-ws >=0.5.2",
42+
"hypercorn >=0.16.0",
43+
"trio >=0.25.0",
44+
"sniffio",
4445
]
4546
docs = [
4647
"mkdocs",

tests/conftest.py

+28-17
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import subprocess
1+
from functools import partial
2+
from socket import socket
23

34
import pytest
5+
from anyio import Event, create_task_group
6+
from hypercorn import Config
47
from pycrdt import Array, Doc
5-
from websockets import serve
8+
from sniffio import current_async_library
9+
from utils import ensure_server_running
610

7-
from pycrdt_websocket import WebsocketServer
11+
from pycrdt_websocket import ASGIServer, WebsocketServer
812

913

1014
class TestYDoc:
@@ -23,32 +27,39 @@ def update(self):
2327

2428

2529
@pytest.fixture
26-
async def yws_server(request):
30+
async def yws_server(request, unused_tcp_port):
2731
try:
2832
kwargs = request.param
29-
except Exception:
33+
except AttributeError:
3034
kwargs = {}
3135
websocket_server = WebsocketServer(**kwargs)
36+
app = ASGIServer(websocket_server)
37+
config = Config()
38+
config.bind = [f"localhost:{unused_tcp_port}"]
39+
shutdown_event = Event()
40+
if current_async_library() == "trio":
41+
from hypercorn.trio import serve
42+
else:
43+
from hypercorn.asyncio import serve
3244
try:
33-
async with websocket_server, serve(websocket_server.serve, "127.0.0.1", 1234):
34-
yield websocket_server
45+
async with create_task_group() as tg, websocket_server:
46+
tg.start_soon(
47+
partial(serve, app, config, shutdown_trigger=shutdown_event.wait, mode="asgi")
48+
)
49+
await ensure_server_running("localhost", unused_tcp_port)
50+
yield unused_tcp_port
51+
shutdown_event.set()
3552
except Exception:
3653
pass
3754

3855

39-
@pytest.fixture
40-
def yjs_client(request):
41-
client_id = request.param
42-
p = subprocess.Popen(["node", f"tests/yjs_client_{client_id}.js"])
43-
yield p
44-
p.kill()
45-
46-
4756
@pytest.fixture
4857
def test_ydoc():
4958
return TestYDoc()
5059

5160

5261
@pytest.fixture
53-
def anyio_backend():
54-
return "asyncio"
62+
def unused_tcp_port() -> int:
63+
with socket() as sock:
64+
sock.bind(("localhost", 0))
65+
return sock.getsockname()[1]

tests/test_asgi.py

+30-41
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,32 @@
11
import pytest
2-
import uvicorn
3-
from anyio import create_task_group, sleep
2+
from anyio import sleep
3+
from httpx_ws import aconnect_ws
44
from pycrdt import Doc, Map
5-
from websockets import connect
6-
7-
from pycrdt_websocket import ASGIServer, WebsocketProvider, WebsocketServer
8-
9-
websocket_server = WebsocketServer(auto_clean_rooms=False)
10-
app = ASGIServer(websocket_server)
11-
12-
13-
@pytest.mark.anyio
14-
async def test_asgi(unused_tcp_port):
15-
# server
16-
config = uvicorn.Config("test_asgi:app", port=unused_tcp_port, log_level="info")
17-
server = uvicorn.Server(config)
18-
async with create_task_group() as tg, websocket_server:
19-
tg.start_soon(server.serve)
20-
while not server.started:
21-
await sleep(0)
22-
23-
# clients
24-
# client 1
25-
ydoc1 = Doc()
26-
ydoc1["map"] = ymap1 = Map()
27-
ymap1["key"] = "value"
28-
async with connect(
29-
f"ws://localhost:{unused_tcp_port}/my-roomname"
30-
) as websocket1, WebsocketProvider(ydoc1, websocket1):
31-
await sleep(0.1)
32-
33-
# client 2
34-
ydoc2 = Doc()
35-
async with connect(
36-
f"ws://localhost:{unused_tcp_port}/my-roomname"
37-
) as websocket2, WebsocketProvider(ydoc2, websocket2):
38-
await sleep(0.1)
39-
40-
ydoc2["map"] = ymap2 = Map()
41-
assert str(ymap2) == '{"key":"value"}'
42-
43-
tg.cancel_scope.cancel()
5+
from utils import Websocket
6+
7+
from pycrdt_websocket import WebsocketProvider
8+
9+
pytestmark = pytest.mark.anyio
10+
11+
12+
@pytest.mark.parametrize("yws_server", [{"auto_clean_rooms": False}], indirect=True)
13+
async def test_asgi(yws_server):
14+
port = yws_server
15+
# client 1
16+
ydoc1 = Doc()
17+
ydoc1["map"] = ymap1 = Map()
18+
ymap1["key"] = "value"
19+
async with aconnect_ws(
20+
f"http://localhost:{port}/my-roomname"
21+
) as websocket1, WebsocketProvider(ydoc1, Websocket(websocket1, "my-roomname")):
22+
await sleep(0.1)
23+
24+
# client 2
25+
ydoc2 = Doc()
26+
async with aconnect_ws(
27+
f"http://localhost:{port}/my-roomname"
28+
) as websocket2, WebsocketProvider(ydoc2, Websocket(websocket2, "my-roomname")):
29+
await sleep(0.1)
30+
31+
ydoc2["map"] = ymap2 = Map()
32+
assert str(ymap2) == '{"key":"value"}'

tests/test_pycrdt_yjs.py

+31-28
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
import pytest
66
from anyio import Event, fail_after
7+
from httpx_ws import aconnect_ws
78
from pycrdt import Array, Doc, Map
8-
from websockets import connect
9+
from utils import Websocket, yjs_client
910

1011
from pycrdt_websocket import WebsocketProvider
1112

13+
pytestmark = pytest.mark.anyio
14+
1215

1316
class Change:
1417
def __init__(self, event, timeout, ydata, sid, key):
@@ -38,32 +41,32 @@ def watch(ydata, key: str | None = None, timeout: float = 1.0):
3841
return Change(change_event, timeout, ydata, sid, key)
3942

4043

41-
@pytest.mark.anyio
42-
@pytest.mark.parametrize("yjs_client", "0", indirect=True)
43-
async def test_pycrdt_yjs_0(yws_server, yjs_client):
44-
ydoc = Doc()
45-
async with connect("ws://127.0.0.1:1234/my-roomname") as websocket, WebsocketProvider(
46-
ydoc, websocket
47-
):
48-
ydoc["map"] = ymap = Map()
49-
for v_in in range(10):
50-
ymap["in"] = float(v_in)
51-
v_out = await watch(ymap, "out").wait()
52-
assert v_out == v_in + 1.0
44+
async def test_pycrdt_yjs_0(yws_server):
45+
port = yws_server
46+
with yjs_client(0, port):
47+
ydoc = Doc()
48+
async with aconnect_ws(
49+
f"http://localhost:{port}/my-roomname"
50+
) as websocket, WebsocketProvider(ydoc, Websocket(websocket, "my-roomname")):
51+
ydoc["map"] = ymap = Map()
52+
for v_in in range(10):
53+
ymap["in"] = float(v_in)
54+
v_out = await watch(ymap, "out").wait()
55+
assert v_out == v_in + 1.0
5356

5457

55-
@pytest.mark.anyio
56-
@pytest.mark.parametrize("yjs_client", "1", indirect=True)
57-
async def test_pycrdt_yjs_1(yws_server, yjs_client):
58-
ydoc = Doc()
59-
ydoc["cells"] = ycells = Array()
60-
ydoc["state"] = ystate = Map()
61-
ycells_change = watch(ycells)
62-
ystate_change = watch(ystate)
63-
async with connect("ws://127.0.0.1:1234/my-roomname") as websocket, WebsocketProvider(
64-
ydoc, websocket
65-
):
66-
await ycells_change.wait()
67-
await ystate_change.wait()
68-
assert ycells.to_py() == [{"metadata": {"foo": "bar"}, "source": "1 + 2"}]
69-
assert ystate.to_py() == {"state": {"dirty": False}}
58+
async def test_pycrdt_yjs_1(yws_server):
59+
port = yws_server
60+
with yjs_client(1, port):
61+
ydoc = Doc()
62+
ydoc["cells"] = ycells = Array()
63+
ydoc["state"] = ystate = Map()
64+
ycells_change = watch(ycells)
65+
ystate_change = watch(ystate)
66+
async with aconnect_ws(
67+
f"http://localhost:{port}/my-roomname"
68+
) as websocket, WebsocketProvider(ydoc, Websocket(websocket, "my-roomname")):
69+
await ycells_change.wait()
70+
await ystate_change.wait()
71+
assert ycells.to_py() == [{"metadata": {"foo": "bar"}, "source": "1 + 2"}]
72+
assert ystate.to_py() == {"state": {"dirty": False}}

tests/test_ystore.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from pycrdt_websocket.ystore import SQLiteYStore, TempFileYStore
1111

12+
pytestmark = pytest.mark.anyio
13+
1214

1315
class MetadataCallback:
1416
def __init__(self):
@@ -37,7 +39,6 @@ def __init__(self, *args, delete_db=False, **kwargs):
3739
super().__init__(*args, **kwargs)
3840

3941

40-
@pytest.mark.anyio
4142
@pytest.mark.parametrize("YStore", (MyTempFileYStore, MySQLiteYStore))
4243
async def test_ystore(YStore):
4344
store_name = "my_store"
@@ -62,7 +63,6 @@ async def test_ystore(YStore):
6263
await ystore.stop()
6364

6465

65-
@pytest.mark.anyio
6666
async def test_document_ttl_sqlite_ystore(test_ydoc):
6767
store_name = "my_store"
6868
ystore = MySQLiteYStore(store_name, delete_db=True)
@@ -91,7 +91,6 @@ async def test_document_ttl_sqlite_ystore(test_ydoc):
9191
await ystore.stop()
9292

9393

94-
@pytest.mark.anyio
9594
@pytest.mark.parametrize("YStore", (MyTempFileYStore, MySQLiteYStore))
9695
async def test_version(YStore, caplog):
9796
store_name = "my_store"

tests/utils.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import subprocess
2+
from contextlib import contextmanager
3+
4+
from anyio import Lock, connect_tcp
5+
6+
7+
class Websocket:
8+
def __init__(self, websocket, path: str):
9+
self._websocket = websocket
10+
self._path = path
11+
self._send_lock = Lock()
12+
13+
@property
14+
def path(self) -> str:
15+
return self._path
16+
17+
def __aiter__(self):
18+
return self
19+
20+
async def __anext__(self) -> bytes:
21+
try:
22+
message = await self.recv()
23+
except Exception:
24+
raise StopAsyncIteration()
25+
return message
26+
27+
async def send(self, message: bytes):
28+
async with self._send_lock:
29+
await self._websocket.send_bytes(message)
30+
31+
async def recv(self) -> bytes:
32+
b = await self._websocket.receive_bytes()
33+
return bytes(b)
34+
35+
36+
@contextmanager
37+
def yjs_client(client_id: int, port: int):
38+
p = subprocess.Popen(["node", f"tests/yjs_client_{client_id}.js", str(port)])
39+
yield p
40+
p.kill()
41+
42+
43+
async def ensure_server_running(host: str, port: int) -> None:
44+
while True:
45+
try:
46+
await connect_tcp(host, port)
47+
except OSError:
48+
pass
49+
else:
50+
break

tests/yjs_client_0.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const Y = require('yjs')
22
const WebsocketProvider = require('y-websocket').WebsocketProvider
33
const ws = require('ws')
44

5+
const port = process.argv[2]
56
const ydoc = new Y.Doc()
67
const ymap = ydoc.getMap('map')
78

@@ -18,7 +19,7 @@ ymap.observe(event => {
1819
})
1920

2021
const wsProvider = new WebsocketProvider(
21-
'ws://127.0.0.1:1234', 'my-roomname',
22+
`ws://127.0.0.1:${port}`, 'my-roomname',
2223
ydoc,
2324
{ WebSocketPolyfill: ws }
2425
)

tests/yjs_client_1.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ const Y = require('yjs')
22
const WebsocketProvider = require('y-websocket').WebsocketProvider
33
const ws = require('ws')
44

5+
const port = process.argv[2]
56
const ydoc = new Y.Doc()
67

78
const wsProvider = new WebsocketProvider(
8-
'ws://127.0.0.1:1234', 'my-roomname',
9+
`ws://127.0.0.1:${port}`, 'my-roomname',
910
ydoc,
1011
{ WebSocketPolyfill: ws }
1112
)

0 commit comments

Comments
 (0)