Skip to content

Commit

Permalink
Test on Trio as well
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart committed Mar 25, 2024
1 parent 1b9ba5f commit 4857134
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 98 deletions.
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ test = [
"mypy",
"pre-commit",
"pytest",
"pytest-asyncio",
"websockets >=10.0",
"uvicorn",
"httpx-ws >=0.5.2",
"hypercorn >=0.16.0",
"trio >=0.25.0",
"sniffio",
]
docs = [
"mkdocs",
Expand Down
41 changes: 22 additions & 19 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import subprocess
from functools import partial

import pytest
from anyio import Event, create_task_group
from pycrdt import Array, Doc
from websockets import serve
from hypercorn import Config
from sniffio import current_async_library

from pycrdt_websocket import WebsocketServer
from pycrdt_websocket import ASGIServer, WebsocketServer

from utils import ensure_server_running


class TestYDoc:
Expand All @@ -23,32 +28,30 @@ def update(self):


@pytest.fixture
async def yws_server(request):
async def yws_server(request, unused_tcp_port):
try:
kwargs = request.param
except Exception:
except AttributeError:
kwargs = {}
websocket_server = WebsocketServer(**kwargs)
app = ASGIServer(websocket_server)
config = Config()
config.bind = [f"localhost:{unused_tcp_port}"]
shutdown_event = Event()
if current_async_library() == "trio":
from hypercorn.trio import serve
else:
from hypercorn.asyncio import serve
try:
async with websocket_server, serve(websocket_server.serve, "127.0.0.1", 1234):
yield websocket_server
async with create_task_group() as tg, websocket_server:
tg.start_soon(partial(serve, app, config, shutdown_trigger=shutdown_event.wait, mode="asgi"))
await ensure_server_running("localhost", unused_tcp_port)
yield unused_tcp_port
shutdown_event.set()
except Exception:
pass


@pytest.fixture
def yjs_client(request):
client_id = request.param
p = subprocess.Popen(["node", f"tests/yjs_client_{client_id}.js"])
yield p
p.kill()


@pytest.fixture
def test_ydoc():
return TestYDoc()


@pytest.fixture
def anyio_backend():
return "asyncio"
73 changes: 32 additions & 41 deletions tests/test_asgi.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,34 @@
import pytest
import uvicorn
from anyio import create_task_group, sleep
from anyio import sleep
from httpx_ws import aconnect_ws
from pycrdt import Doc, Map
from websockets import connect

from pycrdt_websocket import ASGIServer, WebsocketProvider, WebsocketServer

websocket_server = WebsocketServer(auto_clean_rooms=False)
app = ASGIServer(websocket_server)


@pytest.mark.anyio
async def test_asgi(unused_tcp_port):
# server
config = uvicorn.Config("test_asgi:app", port=unused_tcp_port, log_level="info")
server = uvicorn.Server(config)
async with create_task_group() as tg, websocket_server:
tg.start_soon(server.serve)
while not server.started:
await sleep(0)

# clients
# client 1
ydoc1 = Doc()
ydoc1["map"] = ymap1 = Map()
ymap1["key"] = "value"
async with connect(
f"ws://localhost:{unused_tcp_port}/my-roomname"
) as websocket1, WebsocketProvider(ydoc1, websocket1):
await sleep(0.1)

# client 2
ydoc2 = Doc()
async with connect(
f"ws://localhost:{unused_tcp_port}/my-roomname"
) as websocket2, WebsocketProvider(ydoc2, websocket2):
await sleep(0.1)

ydoc2["map"] = ymap2 = Map()
assert str(ymap2) == '{"key":"value"}'

tg.cancel_scope.cancel()

from pycrdt_websocket import WebsocketProvider

from utils import Websocket


pytestmark = pytest.mark.anyio


@pytest.mark.parametrize("yws_server", [{"auto_clean_rooms": False}], indirect=True)
async def test_asgi(yws_server):
port = yws_server
# client 1
ydoc1 = Doc()
ydoc1["map"] = ymap1 = Map()
ymap1["key"] = "value"
async with aconnect_ws(
f"http://localhost:{port}/my-roomname"
) as websocket1, WebsocketProvider(ydoc1, Websocket(websocket1, "my-roomname")):
await sleep(0.1)

# client 2
ydoc2 = Doc()
async with aconnect_ws(
f"http://localhost:{port}/my-roomname"
) as websocket2, WebsocketProvider(ydoc2, Websocket(websocket2, "my-roomname")):
await sleep(0.1)

ydoc2["map"] = ymap2 = Map()
assert str(ymap2) == '{"key":"value"}'
65 changes: 35 additions & 30 deletions tests/test_pycrdt_yjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
import pytest
from anyio import Event, fail_after
from pycrdt import Array, Doc, Map
from websockets import connect
from httpx_ws import aconnect_ws

from pycrdt_websocket import WebsocketProvider

from utils import Websocket, yjs_client


pytestmark = pytest.mark.anyio


class Change:
def __init__(self, event, timeout, ydata, sid, key):
Expand Down Expand Up @@ -38,32 +43,32 @@ def watch(ydata, key: str | None = None, timeout: float = 1.0):
return Change(change_event, timeout, ydata, sid, key)


@pytest.mark.anyio
@pytest.mark.parametrize("yjs_client", "0", indirect=True)
async def test_pycrdt_yjs_0(yws_server, yjs_client):
ydoc = Doc()
async with connect("ws://127.0.0.1:1234/my-roomname") as websocket, WebsocketProvider(
ydoc, websocket
):
ydoc["map"] = ymap = Map()
for v_in in range(10):
ymap["in"] = float(v_in)
v_out = await watch(ymap, "out").wait()
assert v_out == v_in + 1.0


@pytest.mark.anyio
@pytest.mark.parametrize("yjs_client", "1", indirect=True)
async def test_pycrdt_yjs_1(yws_server, yjs_client):
ydoc = Doc()
ydoc["cells"] = ycells = Array()
ydoc["state"] = ystate = Map()
ycells_change = watch(ycells)
ystate_change = watch(ystate)
async with connect("ws://127.0.0.1:1234/my-roomname") as websocket, WebsocketProvider(
ydoc, websocket
):
await ycells_change.wait()
await ystate_change.wait()
assert ycells.to_py() == [{"metadata": {"foo": "bar"}, "source": "1 + 2"}]
assert ystate.to_py() == {"state": {"dirty": False}}
async def test_pycrdt_yjs_0(yws_server):
port = yws_server
with yjs_client(0, port):
ydoc = Doc()
async with aconnect_ws(
f"http://localhost:{port}/my-roomname"
) as websocket, WebsocketProvider(ydoc, Websocket(websocket, "my-roomname")):
ydoc["map"] = ymap = Map()
for v_in in range(10):
ymap["in"] = float(v_in)
v_out = await watch(ymap, "out").wait()
assert v_out == v_in + 1.0


async def test_pycrdt_yjs_1(yws_server):
port = yws_server
with yjs_client(1, port):
ydoc = Doc()
ydoc["cells"] = ycells = Array()
ydoc["state"] = ystate = Map()
ycells_change = watch(ycells)
ystate_change = watch(ystate)
async with aconnect_ws(
f"http://localhost:{port}/my-roomname"
) as websocket, WebsocketProvider(ydoc, Websocket(websocket, "my-roomname")):
await ycells_change.wait()
await ystate_change.wait()
assert ycells.to_py() == [{"metadata": {"foo": "bar"}, "source": "1 + 2"}]
assert ystate.to_py() == {"state": {"dirty": False}}
6 changes: 3 additions & 3 deletions tests/test_ystore.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from pycrdt_websocket.ystore import SQLiteYStore, TempFileYStore


pytestmark = pytest.mark.anyio


class MetadataCallback:
def __init__(self):
self.i = 0
Expand Down Expand Up @@ -37,7 +40,6 @@ def __init__(self, *args, delete_db=False, **kwargs):
super().__init__(*args, **kwargs)


@pytest.mark.anyio
@pytest.mark.parametrize("YStore", (MyTempFileYStore, MySQLiteYStore))
async def test_ystore(YStore):
store_name = "my_store"
Expand All @@ -62,7 +64,6 @@ async def test_ystore(YStore):
await ystore.stop()


@pytest.mark.anyio
async def test_document_ttl_sqlite_ystore(test_ydoc):
store_name = "my_store"
ystore = MySQLiteYStore(store_name, delete_db=True)
Expand Down Expand Up @@ -91,7 +92,6 @@ async def test_document_ttl_sqlite_ystore(test_ydoc):
await ystore.stop()


@pytest.mark.anyio
@pytest.mark.parametrize("YStore", (MyTempFileYStore, MySQLiteYStore))
async def test_version(YStore, caplog):
store_name = "my_store"
Expand Down
50 changes: 50 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import subprocess
from contextlib import contextmanager

from anyio import Lock, connect_tcp


class Websocket:
def __init__(self, websocket, path: str):
self._websocket = websocket
self._path = path
self._send_lock = Lock()

@property
def path(self) -> str:
return self._path

def __aiter__(self):
return self

async def __anext__(self) -> bytes:
try:
message = await self.recv()
except Exception:
raise StopAsyncIteration()
return message

async def send(self, message: bytes):
async with self._send_lock:
await self._websocket.send_bytes(message)

async def recv(self) -> bytes:
b = await self._websocket.receive_bytes()
return bytes(b)


@contextmanager
def yjs_client(client_id: int, port: int):
p = subprocess.Popen(["node", f"tests/yjs_client_{client_id}.js", str(port)])
yield p
p.kill()


async def ensure_server_running(host: str, port: int) -> None:
while True:
try:
await connect_tcp(host, port)
except OSError:
pass
else:
break
3 changes: 2 additions & 1 deletion tests/yjs_client_0.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const Y = require('yjs')
const WebsocketProvider = require('y-websocket').WebsocketProvider
const ws = require('ws')

const port = process.argv[2]
const ydoc = new Y.Doc()
const ymap = ydoc.getMap('map')

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

const wsProvider = new WebsocketProvider(
'ws://127.0.0.1:1234', 'my-roomname',
`ws://127.0.0.1:${port}`, 'my-roomname',
ydoc,
{ WebSocketPolyfill: ws }
)
3 changes: 2 additions & 1 deletion tests/yjs_client_1.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ const Y = require('yjs')
const WebsocketProvider = require('y-websocket').WebsocketProvider
const ws = require('ws')

const port = process.argv[2]
const ydoc = new Y.Doc()

const wsProvider = new WebsocketProvider(
'ws://127.0.0.1:1234', 'my-roomname',
`ws://127.0.0.1:${port}`, 'my-roomname',
ydoc,
{ WebSocketPolyfill: ws }
)
Expand Down

0 comments on commit 4857134

Please sign in to comment.