diff --git a/pyproject.toml b/pyproject.toml index b653acb..9ec75d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,8 @@ test = [ "pre-commit", "pytest", "pytest-asyncio", - "websockets >=10.0", + "httpx-ws >=0.5.2", + "hypercorn >=0.16.0", "pycrdt-websocket >=0.15.0,<0.16.0", ] docs = [ diff --git a/tests/conftest.py b/tests/conftest.py index f4ae9f0..bc9bc41 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,11 +3,15 @@ import json import subprocess +from functools import partial from pathlib import Path import pytest -from pycrdt_websocket import WebsocketServer -from websockets import serve +from anyio import Event, create_task_group +from hypercorn import Config +from hypercorn.asyncio import serve +from pycrdt_websocket import ASGIServer, WebsocketServer +from utils import ensure_server_running # workaround until these PRs are merged: # - https://github.com/yjs/y-websocket/pull/104 @@ -27,15 +31,26 @@ def update_json_file(path: Path, d: dict): @pytest.fixture -async def yws_server(request): +async def yws_server(request, unused_tcp_port): try: - kwargs = request.param - except Exception: - kwargs = {} - websocket_server = WebsocketServer(**kwargs) - try: - async with websocket_server, serve(websocket_server.serve, "localhost", 1234): - yield websocket_server + async with create_task_group() as tg: + try: + kwargs = request.param + except Exception: + kwargs = {} + websocket_server = WebsocketServer(**kwargs) + app = ASGIServer(websocket_server) + config = Config() + config.bind = [f"localhost:{unused_tcp_port}"] + shutdown_event = Event() + async with websocket_server as websocket_server: + tg.start_soon( + partial(serve, app, config, shutdown_trigger=shutdown_event.wait, mode="asgi") + ) + await ensure_server_running("localhost", unused_tcp_port) + pytest.port = unused_tcp_port + yield unused_tcp_port, websocket_server + shutdown_event.set() except Exception: pass @@ -43,7 +58,7 @@ async def yws_server(request): @pytest.fixture def yjs_client(request): client_id = request.param - p = subprocess.Popen(["node", f"{here / 'yjs_client_'}{client_id}.js"]) + p = subprocess.Popen(["node", f"{here / 'yjs_client_'}{client_id}.js", str(pytest.port)]) yield p p.terminate() try: diff --git a/tests/test_pycrdt_yjs.py b/tests/test_pycrdt_yjs.py index a9f3720..e099b3b 100644 --- a/tests/test_pycrdt_yjs.py +++ b/tests/test_pycrdt_yjs.py @@ -6,9 +6,10 @@ import pytest from anyio import Event, create_task_group, move_on_after +from httpx_ws import aconnect_ws from pycrdt import Doc, Map from pycrdt_websocket import WebsocketProvider -from websockets import connect +from utils import Websocket from jupyter_ydoc import YNotebook from jupyter_ydoc.utils import cast_all @@ -61,10 +62,12 @@ def source(self): @pytest.mark.asyncio @pytest.mark.parametrize("yjs_client", "0", indirect=True) async def test_ypy_yjs_0(yws_server, yjs_client): + port, _ = yws_server ydoc = Doc() ynotebook = YNotebook(ydoc) - async with connect("ws://localhost:1234/my-roomname") as websocket, WebsocketProvider( - ydoc, websocket + room_name = "my-roomname" + async with aconnect_ws(f"http://localhost:{port}/{room_name}") as websocket, WebsocketProvider( + ydoc, Websocket(websocket, room_name) ): nb = stringify_source(json.loads((files_dir / "nb0.ipynb").read_text())) ynotebook.source = nb @@ -77,12 +80,14 @@ async def test_ypy_yjs_0(yws_server, yjs_client): @pytest.mark.asyncio @pytest.mark.parametrize("yjs_client", "1", indirect=True) async def test_ypy_yjs_1(yws_server, yjs_client): + port, _ = yws_server ydoc = Doc() ynotebook = YNotebook(ydoc) nb = stringify_source(json.loads((files_dir / "nb1.ipynb").read_text())) ynotebook.source = nb - async with connect("ws://localhost:1234/my-roomname") as websocket, WebsocketProvider( - ydoc, websocket + room_name = "my-roomname" + async with aconnect_ws(f"http://localhost:{port}/{room_name}") as websocket, WebsocketProvider( + ydoc, Websocket(websocket, room_name) ): output_text = ynotebook.ycells[0]["outputs"][0]["text"] assert output_text.to_py() == ["Hello,"] diff --git a/tests/test_ydocs.py b/tests/test_ydocs.py index cef3230..6757f89 100644 --- a/tests/test_ydocs.py +++ b/tests/test_ydocs.py @@ -14,7 +14,6 @@ def test_yblob(): changes = [] def callback(topic, event): - print(topic, event) changes.append((topic, event)) yblob.observe(callback) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..6cac6c0 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,43 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +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) + + +async def ensure_server_running(host: str, port: int) -> None: + while True: + try: + await connect_tcp(host, port) + except OSError: + pass + else: + break diff --git a/tests/yjs_client_0.js b/tests/yjs_client_0.js index e4e53b7..737b7cc 100644 --- a/tests/yjs_client_0.js +++ b/tests/yjs_client_0.js @@ -6,12 +6,13 @@ import { YNotebook } from '@jupyter/ydoc' import { WebsocketProvider } from 'y-websocket' +const port = process.argv[2] const notebook = new YNotebook() const ytest = notebook.ydoc.getMap('_test') import ws from 'ws' const wsProvider = new WebsocketProvider( - 'ws://localhost:1234', 'my-roomname', + `ws://127.0.0.1:${port}`, 'my-roomname', notebook.ydoc, { WebSocketPolyfill: ws } ) diff --git a/tests/yjs_client_1.js b/tests/yjs_client_1.js index 04daa23..d3ded09 100644 --- a/tests/yjs_client_1.js +++ b/tests/yjs_client_1.js @@ -7,17 +7,26 @@ import { YNotebook } from '@jupyter/ydoc' import { WebsocketProvider } from 'y-websocket' import ws from 'ws' +const port = process.argv[2] const notebook = new YNotebook() const wsProvider = new WebsocketProvider( - 'ws://localhost:1234', 'my-roomname', + `ws://127.0.0.1:${port}`, 'my-roomname', notebook.ydoc, { WebSocketPolyfill: ws } ) -wsProvider.on('sync', (isSynced) => { +wsProvider.on('status', event => { + console.log(event.status) +}) + +notebook.changed.connect(() => { const cell = notebook.getCell(0) - const youtput = cell.youtputs.get(0) - const text = youtput.get('text') - text.insert(1, [' World!']) + if (cell) { + const youtput = cell.youtputs.get(0) + const text = youtput.get('text') + if (text.length === 1) { + text.insert(1, [' World!']) + } + } })