From af6d5c0bee7c4127d26445743dd16672ec0c1bb5 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 29 Nov 2024 11:16:41 +0100 Subject: [PATCH] Backport PR #288 on branch 2.x (Fix tests) --- pyproject.toml | 3 ++- tests/conftest.py | 37 ++++++++++++++++++++++++---------- tests/test_pycrdt_yjs.py | 9 ++++++--- tests/utils.py | 43 ++++++++++++++++++++++++++++++++++++++++ tests/yjs_client_0.js | 3 ++- 5 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 tests/utils.py diff --git a/pyproject.toml b/pyproject.toml index c75b5e7..b0bac8e 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 2b58566..f7fb1de 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 # type: ignore +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,6 +58,6 @@ async def yws_server(request): @pytest.fixture def yjs_client(request): client_id = request.param - p = subprocess.Popen(f"yarn node {here / 'yjs_client_'}{client_id}.js", shell=True) + p = subprocess.Popen(["node", f"{here / 'yjs_client_'}{client_id}.js", str(pytest.port)]) yield p p.kill() diff --git a/tests/test_pycrdt_yjs.py b/tests/test_pycrdt_yjs.py index 77cda03..f36dd76 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 # type: ignore +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 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 } )