diff --git a/tests/conftest.py b/tests/conftest.py index d7cddd9..439291f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,7 @@ async def yws_server(request): try: async with websocket_server, serve(websocket_server.serve, "127.0.0.1", 1234): yield websocket_server + websocket_server.stop() except Exception: pass diff --git a/tests/test_pycrdt_yjs.py b/tests/test_pycrdt_yjs.py index 9e438a5..58c5abf 100644 --- a/tests/test_pycrdt_yjs.py +++ b/tests/test_pycrdt_yjs.py @@ -1,75 +1,95 @@ import pytest -from anyio import Event, create_task_group, move_on_after, sleep +from anyio import Event, move_on_after from pycrdt import Array, Doc, Map from websockets import connect from pycrdt_websocket import WebsocketProvider -class YTest: - def __init__(self, ydoc: Doc, timeout: float = 1.0): +class YClient: + """ + A representation of a `pycrdt` client. The constructor accepts a YDoc + instance, defines a shared YMap type named `ymap`, and binds it to + `self.ymap`. + """ + + def __init__(self, ydoc: Doc): self.ydoc = ydoc - self.timeout = timeout - self.ydoc["_test"] = self.ytest = Map() - self.clock = -1.0 + self.ymap = Map() + self.ydoc["ymap"] = self.ymap + self.ymap["clock"] = 0.0 + + @property + def clock(self): + return self.ymap["clock"] - def run_clock(self): - self.clock = max(self.clock, 0.0) - self.ytest["clock"] = self.clock + def inc_clock(self): + """ + Increments `ymap["clock"]`. + """ + self.ymap["clock"] = self.clock + 1 - async def clock_run(self): + async def wait_for_reply(self): + """ + Wait for the JS client to reply in response to a previous `inc_clock()` + call by also incrementing `ymap["clock"]`. + """ change = Event() def callback(event): if "clock" in event.keys: - clk = event.keys["clock"]["newValue"] - if clk > self.clock: - self.clock = clk + 1.0 - change.set() + change.set() - subscription_id = self.ytest.observe(callback) - async with create_task_group(): - with move_on_after(self.timeout): - await change.wait() - - self.ytest.unobserve(subscription_id) + subscription_id = self.ymap.observe(callback) + # wait up to 1000ms for a reply + with move_on_after(1.0): + await change.wait() + self.ymap.unobserve(subscription_id) @pytest.mark.anyio @pytest.mark.parametrize("yjs_client", "0", indirect=True) async def test_pycrdt_yjs_0(yws_server, yjs_client): + """ + The JS client (`yjs_client_0.js`) should set `ymap["out"] := ymap["in"] + 1` + whenever the clock is incremented via `yclient.inc_clock()`. + """ + MSG_COUNT = 10 ydoc = Doc() - ytest = YTest(ydoc) + yclient = YClient(ydoc) + ymap = yclient.ymap + async with connect("ws://127.0.0.1:1234/my-roomname") as websocket, WebsocketProvider( ydoc, websocket ): - ydoc["map"] = ymap = Map() - # set a value in "in" - for v_in in range(10): - ymap["in"] = float(v_in) - ytest.run_clock() - await ytest.clock_run() - v_out = ymap["out"] - assert v_out == v_in + 1.0 + for msg_num in range(1, MSG_COUNT + 1): + # set `ymap["in"]` + ymap["in"] = float(msg_num) + yclient.inc_clock() + await yclient.wait_for_reply() + + # assert JS client increments `ymap["out"]` in response + assert ymap["out"] == ymap["in"] + 1.0 + + assert yclient.clock == MSG_COUNT * 2 @pytest.mark.anyio @pytest.mark.parametrize("yjs_client", "1", indirect=True) async def test_pycrdt_yjs_1(yws_server, yjs_client): - # wait for the JS client to connect - tt, dt = 0, 0.1 - while True: - await sleep(dt) - if "/my-roomname" in yws_server.rooms: - break - tt += dt - if tt >= 1: - raise RuntimeError("Timeout waiting for client to connect") - ydoc = yws_server.rooms["/my-roomname"].ydoc - ytest = YTest(ydoc) - ytest.run_clock() - await ytest.clock_run() - ydoc["cells"] = ycells = Array() - ydoc["state"] = ystate = Map() - assert ycells.to_py() == [{"metadata": {"foo": "bar"}, "source": "1 + 2"}] - assert ystate.to_py() == {"state": {"dirty": False}} + """ + The JS client (`yjs_client_1.js`) should set `ydoc["cells"]` and + `ydoc["state"]` whenever the clock is incremented via `yclient.inc_clock()`. + """ + yroom = await yws_server.get_room("/my-roomname") + ydoc = yroom.ydoc + yclient = YClient(ydoc) + + yclient.inc_clock() + await yclient.wait_for_reply() + + # TODO: remove the need to set a root type before accessing it + ydoc["cells"] = Array() + ydoc["state"] = Map() + assert ydoc["cells"].to_py() == [{"metadata": {"foo": "bar"}, "source": "1 + 2"}] + assert ydoc["state"].to_py() == {"state": {"dirty": False}} diff --git a/tests/yjs_client_0.js b/tests/yjs_client_0.js index 3e8f7a6..63aefff 100644 --- a/tests/yjs_client_0.js +++ b/tests/yjs_client_0.js @@ -1,32 +1,28 @@ -const Y = require('yjs') -const WebsocketProvider = require('y-websocket').WebsocketProvider +const Y = require("yjs"); +const WebsocketProvider = require("y-websocket").WebsocketProvider; -const ydoc = new Y.Doc() -const ytest = ydoc.getMap('_test') -const ymap = ydoc.getMap('map') -const ws = require('ws') +const ydoc = new Y.Doc(); +const ymap = ydoc.getMap("ymap"); +const ws = require("ws"); const wsProvider = new WebsocketProvider( - 'ws://127.0.0.1:1234', 'my-roomname', + "ws://127.0.0.1:1234", + "my-roomname", ydoc, { WebSocketPolyfill: ws } -) +); -wsProvider.on('status', event => { - console.log(event.status) -}) +wsProvider.on("status", (event) => { + console.log(event.status); +}); -var clock = -1 +ymap.observe((event) => { + // only do something when another client updates `ymap.clock` + if (event.transaction.local || !event.changes.keys.has("clock")) { + return; + } -ytest.observe(event => { - event.changes.keys.forEach((change, key) => { - if (key === 'clock') { - const clk = ytest.get('clock') - if (clk > clock) { - ymap.set('out', ymap.get('in') + 1) - clock = clk + 1 - ytest.set('clock', clock) - } - } - }) -}) + const clock = ymap.get("clock"); + ymap.set("out", ymap.get("in") + 1); + ymap.set("clock", clock + 1); +}); diff --git a/tests/yjs_client_1.js b/tests/yjs_client_1.js index 655331d..6422401 100644 --- a/tests/yjs_client_1.js +++ b/tests/yjs_client_1.js @@ -1,35 +1,37 @@ -const Y = require('yjs') -const WebsocketProvider = require('y-websocket').WebsocketProvider +const Y = require("yjs"); +const WebsocketProvider = require("y-websocket").WebsocketProvider; -const ydoc = new Y.Doc() -const ytest = ydoc.getMap('_test') -const ycells = ydoc.getArray("cells") -const ystate = ydoc.getMap("state") -const ws = require('ws') +const ydoc = new Y.Doc(); +const ymap = ydoc.getMap("ymap"); +const ycells = ydoc.getArray("cells"); +const ystate = ydoc.getMap("state"); +const ws = require("ws"); const wsProvider = new WebsocketProvider( - 'ws://127.0.0.1:1234', 'my-roomname', + "ws://127.0.0.1:1234", + "my-roomname", ydoc, { WebSocketPolyfill: ws } -) +); -wsProvider.on('status', event => { - console.log(event.status) -}) +wsProvider.on("status", (event) => { + console.log(event.status); +}); -var clock = -1 +ymap.observe((event) => { + // only do something when another client updates `ymap.clock` + if (event.transaction.local || !event.changes.keys.has("clock")) { + return; + } -ytest.observe(event => { - event.changes.keys.forEach((change, key) => { - if (key === 'clock') { - const clk = ytest.get('clock') - if (clk > clock) { - const cells = [new Y.Map([['source', new Y.Text('1 + 2')], ['metadata', {'foo': 'bar'}]])] - ycells.push(cells) - ystate.set('state', {'dirty': false}) - clock = clk + 1 - ytest.set('clock', clock) - } - } - }) -}) + const clock = ymap.get("clock"); + const cells = [ + new Y.Map([ + ["source", new Y.Text("1 + 2")], + ["metadata", { foo: "bar" }], + ]), + ]; + ycells.push(cells); + ystate.set("state", { dirty: false }); + ymap.set("clock", clock + 1); +});