Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor integration tests to improve legibility #11

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
dlqqq marked this conversation as resolved.
Show resolved Hide resolved
except Exception:
pass

Expand Down
112 changes: 66 additions & 46 deletions tests/test_pycrdt_yjs.py
Original file line number Diff line number Diff line change
@@ -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}}
dlqqq marked this conversation as resolved.
Show resolved Hide resolved
44 changes: 20 additions & 24 deletions tests/yjs_client_0.js
Original file line number Diff line number Diff line change
@@ -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);
});
56 changes: 29 additions & 27 deletions tests/yjs_client_1.js
Original file line number Diff line number Diff line change
@@ -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);
});
Loading