From 0654281207f14af23e8305c040bbd7ed4f135a99 Mon Sep 17 00:00:00 2001 From: Carlos Herrero <26092748+hbcarlos@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:49:30 +0200 Subject: [PATCH 1/6] Create new fixtures and fix current_user deprecation warning --- jupyter_collaboration/handlers.py | 4 +- pyproject.toml | 3 +- tests/conftest.py | 153 ++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 3 deletions(-) diff --git a/jupyter_collaboration/handlers.py b/jupyter_collaboration/handlers.py index 42223417..4f000041 100644 --- a/jupyter_collaboration/handlers.py +++ b/jupyter_collaboration/handlers.py @@ -155,7 +155,7 @@ async def get(self, *args, **kwargs): """ Overrides default behavior to check whether the client is authenticated or not. """ - if self.get_current_user() is None: + if self.current_user is None: self.log.warning("Couldn't authenticate WebSocket connection") raise web.HTTPError(403) return await super().get(*args, **kwargs) @@ -258,7 +258,7 @@ async def on_message(self, message): if message_type == MessageType.CHAT: msg = message[2:].decode("utf-8") - user = self.get_current_user() + user = self.current_user data = json.dumps( {"sender": user.username, "timestamp": time.time(), "content": json.loads(msg)} ).encode("utf8") diff --git a/pyproject.toml b/pyproject.toml index bff7d5e0..ded035a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,8 @@ test = [ "jupyter_server[test]>=2.0.0", "pytest>=7.0", "pytest-cov", - "pytest-asyncio" + "pytest-asyncio", + "websockets" ] docs = [ "jupyterlab>=4.0.0", diff --git a/tests/conftest.py b/tests/conftest.py index e6876d12..e91b7cf5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,157 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +import json +from asyncio import Event, sleep +from distutils.dir_util import copy_tree +from pathlib import Path +from typing import Any + +import nbformat +import pytest +from jupyter_ydoc import YNotebook, YUnicode +from websockets import connect +from ypy_websocket import WebsocketProvider + pytest_plugins = ["jupyter_server.pytest_plugin"] + +HERE = Path(__file__).parent.resolve() +RESOURCES_PATH = f"{HERE}/resources" + + +@pytest.fixture +def jp_server_config(jp_root_dir, jp_server_config): + return { + "ServerApp": { + "jpserver_extensions": {"jupyter_collaboration": True, "jupyter_server_fileid": True}, + "token": "", + "password": "", + "disable_check_xsrf": True, + }, + "SQLiteYStore": {"db_path": str(jp_root_dir.joinpath(".rtc_test.db"))}, + } + + +@pytest.fixture(autouse=True) +def rtc_move_resources(jp_root_dir): + copy_tree(RESOURCES_PATH, str(jp_root_dir)) + + +@pytest.fixture +def rtc_create_file(jp_root_dir, jp_serverapp, rtc_add_doc_to_store): + """Creates a text file in the test's home directory.""" + fim = jp_serverapp.web_app.settings["file_id_manager"] + + async def _inner(path: str, content: str = None, index=False, store=False) -> (str, str): + file_path = jp_root_dir.joinpath(path) + # If the file path has a parent directory, make sure it's created. + parent = file_path.parent + parent.mkdir(parents=True, exist_ok=True) + + if content == None: + content = "" + + file_path.write_text(content) + + if index: + fim.index(path) + + if store: + await rtc_add_doc_to_store("text", "file", path) + + return path, content + + return _inner + + +@pytest.fixture +def rtc_create_notebook(jp_root_dir, jp_serverapp): + """Creates a notebook in the test's home directory.""" + fim = jp_serverapp.web_app.settings["file_id_manager"] + + async def _inner(path: str, content: str = None, index=False, store=False) -> (str, str): + nbpath = jp_root_dir.joinpath(path) + # Check that the notebook has the correct file extension. + if nbpath.suffix != ".ipynb": + msg = "File extension for notebook must be .ipynb" + raise Exception(msg) + # If the notebook path has a parent directory, make sure it's created. + parent = nbpath.parent + parent.mkdir(parents=True, exist_ok=True) + + # Create a notebook string and write to file. + if content == None: + nb = nbformat.v4.new_notebook() + content = nbformat.writes(nb, version=4) + + nbpath.write_text(content) + + if index: + fim.index(path) + + if store: + await rtc_add_doc_to_store("json", "notebook", path) + + return path, content + + return _inner + + +@pytest.fixture +def rtc_fetch_session(jp_fetch): + def _inner(format: str, type: str, path: str): + return jp_fetch( + "/api/collaboration/session", + path, + method="PUT", + body=json.dumps({"format": format, "type": type}), + ) + + return _inner + + +@pytest.fixture +def rtc_connect_awareness_client(jp_http_port, jp_base_url): + async def _inner(room_id: str): + return connect( + f"ws://127.0.0.1:{jp_http_port}{jp_base_url}api/collaboration/room/{room_id}" + ) + + return _inner + + +@pytest.fixture +def rtc_connect_doc_client(jp_http_port, jp_base_url, rtc_fetch_session): + async def _inner(format: str, type: str, path: str): + resp = await rtc_fetch_session(format, type, path) + data = json.loads(resp.body.decode("utf-8")) + return connect( + f"ws://127.0.0.1:{jp_http_port}{jp_base_url}api/collaboration/room/{data['format']}:{data['type']}:{data['fileId']}?sessionId={data['sessionId']}" + ) + + return _inner + + +@pytest.fixture +def rtc_add_doc_to_store(rtc_connect_doc_client): + event = Event() + + def _on_document_change(target: str, e: Any) -> None: + if target == "source": + event.set() + + async def _inner(format: str, type: str, path: str): + if type == "notebook": + doc = YNotebook() + else: + doc = YUnicode() + + doc.observe(_on_document_change) + + async with await rtc_connect_doc_client(format, type, path) as ws, WebsocketProvider( + doc.ydoc, ws + ): + await event.wait() + await sleep(0.1) + + return _inner From 41cb9616db73e3e7e92bd5e8733bfc3623bb4a9d Mon Sep 17 00:00:00 2001 From: Carlos Herrero <26092748+hbcarlos@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:51:03 +0200 Subject: [PATCH 2/6] Adds tests for the app and handlers --- tests/test_app.py | 61 ++++++++++++++++++++++++++++++ tests/test_handlers.py | 85 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 tests/test_app.py create mode 100644 tests/test_handlers.py diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 00000000..1e4397c4 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,61 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +from __future__ import annotations + +from jupyter_collaboration.stores import SQLiteYStore, TempFileYStore + + +def test_default_settings(jp_serverapp): + settings = jp_serverapp.web_app.settings["jupyter_collaboration_config"] + + assert settings["disable_rtc"] == False + assert settings["file_poll_interval"] == 1 + assert settings["document_cleanup_delay"] == 60 + assert settings["document_save_delay"] == 1 + assert settings["ystore_class"] == SQLiteYStore + + +def test_settings_should_disable_rtc(jp_configurable_serverapp): + argv = ["--YDocExtension.disable_rtc=True"] + + app = jp_configurable_serverapp(argv=argv) + settings = app.web_app.settings["jupyter_collaboration_config"] + + assert settings["disable_rtc"] == True + + +def test_settings_should_change_file_poll(jp_configurable_serverapp): + argv = ["--YDocExtension.file_poll_interval=2"] + + app = jp_configurable_serverapp(argv=argv) + settings = app.web_app.settings["jupyter_collaboration_config"] + + assert settings["file_poll_interval"] == 2 + + +def test_settings_should_change_document_cleanup(jp_configurable_serverapp): + argv = ["--YDocExtension.document_cleanup_delay=10"] + + app = jp_configurable_serverapp(argv=argv) + settings = app.web_app.settings["jupyter_collaboration_config"] + + assert settings["document_cleanup_delay"] == 10 + + +def test_settings_should_change_save_delay(jp_configurable_serverapp): + argv = ["--YDocExtension.document_save_delay=10"] + + app = jp_configurable_serverapp(argv=argv) + settings = app.web_app.settings["jupyter_collaboration_config"] + + assert settings["document_save_delay"] == 10 + + +def test_settings_should_change_ystore_class(jp_configurable_serverapp): + argv = ["--YDocExtension.ystore_class=jupyter_collaboration.stores.TempFileYStore"] + + app = jp_configurable_serverapp(argv=argv) + settings = app.web_app.settings["jupyter_collaboration_config"] + + assert settings["ystore_class"] == TempFileYStore diff --git a/tests/test_handlers.py b/tests/test_handlers.py new file mode 100644 index 00000000..0a8f7d08 --- /dev/null +++ b/tests/test_handlers.py @@ -0,0 +1,85 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +from __future__ import annotations + +import json +from asyncio import Event, sleep +from typing import Any + +from jupyter_ydoc import YUnicode +from ypy_websocket import WebsocketProvider + + +async def test_session_handler_should_create_session_id( + rtc_create_file, rtc_fetch_session, jp_serverapp +): + file_format = "text" + file_type = "file" + file_path = "sessionID.txt" + + fim = jp_serverapp.web_app.settings["file_id_manager"] + await rtc_create_file(file_path) + + resp = await rtc_fetch_session(file_format, file_type, file_path) + assert resp.code == 201 + + data = json.loads(resp.body.decode("utf-8")) + assert data["format"] == file_format + assert data["type"] == file_type + assert data["fileId"] == fim.get_id(file_path) + assert data["sessionId"] + + +async def test_session_handler_should_respond_with_session_id( + rtc_create_file, rtc_fetch_session, jp_serverapp +): + file_format = "text" + file_type = "file" + file_path = "sessionID_2.txt" + + fim = jp_serverapp.web_app.settings["file_id_manager"] + await rtc_create_file(file_path, None, True) + + resp = await rtc_fetch_session(file_format, file_type, file_path) + assert resp.code == 200 + + data = json.loads(resp.body.decode("utf-8")) + + assert data["format"] == file_format + assert data["type"] == file_type + assert data["fileId"] == fim.get_id(file_path) + assert data["sessionId"] + + +async def test_session_handler_should_respond_with_not_found(rtc_fetch_session): + # TODO: Fix session handler + # File ID manager allays returns an index, even if the file doesn't exist + file_format = "text" + file_type = "file" + file_path = "doesnotexist.txt" + + resp = await rtc_fetch_session(file_format, file_type, file_path) + assert resp + # assert resp.code == 404 + + +async def test_room_handler_doc_client_should_connect(rtc_create_file, rtc_connect_doc_client): + path, content = await rtc_create_file("test.txt", "test") + + event = Event() + + def _on_document_change(target: str, e: Any) -> None: + if target == "source": + event.set() + + doc = YUnicode() + doc.observe(_on_document_change) + + async with await rtc_connect_doc_client("text", "file", path) as ws, WebsocketProvider( + doc.ydoc, ws + ): + await event.wait() + await sleep(0.1) + + assert doc.source == content From ad1432b54eb384c3df106563661c87477536af03 Mon Sep 17 00:00:00 2001 From: Carlos Herrero <26092748+hbcarlos@users.noreply.github.com> Date: Tue, 8 Aug 2023 17:20:39 +0200 Subject: [PATCH 3/6] Test stores --- tests/conftest.py | 93 ++++++++++++++++++++++++++++------ tests/test_app.py | 4 +- tests/test_rooms.py | 118 ++++++++++++++++++-------------------------- tests/utils.py | 11 +++++ 4 files changed, 139 insertions(+), 87 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e91b7cf5..9db6cc3b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations import json from asyncio import Event, sleep -from distutils.dir_util import copy_tree +from datetime import datetime from pathlib import Path from typing import Any @@ -13,6 +14,12 @@ from websockets import connect from ypy_websocket import WebsocketProvider +from jupyter_collaboration.loaders import FileLoader +from jupyter_collaboration.rooms import DocumentRoom +from jupyter_collaboration.stores import SQLiteYStore + +from .utils import FakeContentsManager, FakeEventLogger, FakeFileIDManager + pytest_plugins = ["jupyter_server.pytest_plugin"] HERE = Path(__file__).parent.resolve() @@ -32,23 +39,20 @@ def jp_server_config(jp_root_dir, jp_server_config): } -@pytest.fixture(autouse=True) -def rtc_move_resources(jp_root_dir): - copy_tree(RESOURCES_PATH, str(jp_root_dir)) - - @pytest.fixture def rtc_create_file(jp_root_dir, jp_serverapp, rtc_add_doc_to_store): """Creates a text file in the test's home directory.""" fim = jp_serverapp.web_app.settings["file_id_manager"] - async def _inner(path: str, content: str = None, index=False, store=False) -> (str, str): + async def _inner( + path: str, content: str | None = None, index: bool = False, store: bool = False + ) -> tuple[str, str]: file_path = jp_root_dir.joinpath(path) # If the file path has a parent directory, make sure it's created. parent = file_path.parent parent.mkdir(parents=True, exist_ok=True) - if content == None: + if content is None: content = "" file_path.write_text(content) @@ -65,11 +69,13 @@ async def _inner(path: str, content: str = None, index=False, store=False) -> (s @pytest.fixture -def rtc_create_notebook(jp_root_dir, jp_serverapp): +def rtc_create_notebook(jp_root_dir, jp_serverapp, rtc_add_doc_to_store): """Creates a notebook in the test's home directory.""" fim = jp_serverapp.web_app.settings["file_id_manager"] - async def _inner(path: str, content: str = None, index=False, store=False) -> (str, str): + async def _inner( + path: str, content: str | None = None, index: bool = False, store: bool = False + ) -> tuple[str, str]: nbpath = jp_root_dir.joinpath(path) # Check that the notebook has the correct file extension. if nbpath.suffix != ".ipynb": @@ -80,7 +86,7 @@ async def _inner(path: str, content: str = None, index=False, store=False) -> (s parent.mkdir(parents=True, exist_ok=True) # Create a notebook string and write to file. - if content == None: + if content is None: nb = nbformat.v4.new_notebook() content = nbformat.writes(nb, version=4) @@ -99,7 +105,7 @@ async def _inner(path: str, content: str = None, index=False, store=False) -> (s @pytest.fixture def rtc_fetch_session(jp_fetch): - def _inner(format: str, type: str, path: str): + def _inner(format: str, type: str, path: str) -> Any: return jp_fetch( "/api/collaboration/session", path, @@ -112,7 +118,7 @@ def _inner(format: str, type: str, path: str): @pytest.fixture def rtc_connect_awareness_client(jp_http_port, jp_base_url): - async def _inner(room_id: str): + async def _inner(room_id: str) -> Any: return connect( f"ws://127.0.0.1:{jp_http_port}{jp_base_url}api/collaboration/room/{room_id}" ) @@ -122,7 +128,7 @@ async def _inner(room_id: str): @pytest.fixture def rtc_connect_doc_client(jp_http_port, jp_base_url, rtc_fetch_session): - async def _inner(format: str, type: str, path: str): + async def _inner(format: str, type: str, path: str) -> Any: resp = await rtc_fetch_session(format, type, path) data = json.loads(resp.body.decode("utf-8")) return connect( @@ -140,7 +146,7 @@ def _on_document_change(target: str, e: Any) -> None: if target == "source": event.set() - async def _inner(format: str, type: str, path: str): + async def _inner(format: str, type: str, path: str) -> None: if type == "notebook": doc = YNotebook() else: @@ -155,3 +161,60 @@ async def _inner(format: str, type: str, path: str): await sleep(0.1) return _inner + + +@pytest.fixture +def rtc_create_SQLite_store(jp_serverapp): + for k, v in jp_serverapp.config.get("SQLiteYStore").items(): + setattr(SQLiteYStore, k, v) + + async def _inner(type: str, path: str, content: str) -> DocumentRoom: + db = SQLiteYStore(path=f"{type}:{path}") + await db.start() + + if type == "notebook": + doc = YNotebook() + else: + doc = YUnicode() + + doc.source = content + await db.encode_state_as_update(doc.ydoc) + + return db + + return _inner + + +@pytest.fixture +def rtc_create_mock_document_room(): + def _inner( + id: str, + path: str, + content: str, + last_modified: datetime | None = None, + save_delay: float | None = None, + store: SQLiteYStore | None = None, + ) -> tuple[FakeContentsManager, FileLoader, DocumentRoom]: + paths = {id: path} + + if last_modified is None: + cm = FakeContentsManager({"content": content}) + else: + cm = FakeContentsManager({"last_modified": datetime.now(), "content": content}) + + loader = FileLoader( + id, + FakeFileIDManager(paths), + cm, + poll_interval=0.1, + ) + + return ( + cm, + loader, + DocumentRoom( + "test-room", "text", "file", loader, FakeEventLogger(), store, None, save_delay + ), + ) + + return _inner diff --git a/tests/test_app.py b/tests/test_app.py index 1e4397c4..becc278c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -9,7 +9,7 @@ def test_default_settings(jp_serverapp): settings = jp_serverapp.web_app.settings["jupyter_collaboration_config"] - assert settings["disable_rtc"] == False + assert settings["disable_rtc"] is False assert settings["file_poll_interval"] == 1 assert settings["document_cleanup_delay"] == 60 assert settings["document_save_delay"] == 1 @@ -22,7 +22,7 @@ def test_settings_should_disable_rtc(jp_configurable_serverapp): app = jp_configurable_serverapp(argv=argv) settings = app.web_app.settings["jupyter_collaboration_config"] - assert settings["disable_rtc"] == True + assert settings["disable_rtc"] is True def test_settings_should_change_file_poll(jp_configurable_serverapp): diff --git a/tests/test_rooms.py b/tests/test_rooms.py index 669c6d4e..7ecc5ccd 100644 --- a/tests/test_rooms.py +++ b/tests/test_rooms.py @@ -7,57 +7,59 @@ from datetime import datetime import pytest -from ypy_websocket.yutils import write_var_uint +from jupyter_ydoc import YUnicode -from jupyter_collaboration.loaders import FileLoader -from jupyter_collaboration.rooms import DocumentRoom -from jupyter_collaboration.utils import RoomMessages - -from .utils import FakeContentsManager, FakeEventLogger, FakeFileIDManager +from .utils import overite_msg, reload_msg @pytest.mark.asyncio -async def test_should_initialize_document_room_without_store(): - id = "test-id" +async def test_should_initialize_document_room_without_store(rtc_create_mock_document_room): content = "test" - paths = {id: "test.txt"} - cm = FakeContentsManager({"content": content}) - loader = FileLoader( - id, - FakeFileIDManager(paths), - cm, - poll_interval=0.1, - ) - - room = DocumentRoom("test-room", "text", "file", loader, FakeEventLogger(), None, None) + _, _, room = rtc_create_mock_document_room("test-id", "test.txt", content) await room.initialize() assert room._document.source == content @pytest.mark.asyncio -async def test_should_initialize_document_room_from_store(): - """ - We need to create test files with Y updates to simulate - a store. - """ - pass +async def test_should_initialize_document_room_from_store( + rtc_create_SQLite_store, rtc_create_mock_document_room +): + # TODO: We don't know for sure if it is taking the content from the store. + # If the content from the store is different than the content from disk, + # the room will initialize with the content from disk and overwrite the document + + id = "test-id" + content = "test" + store = await rtc_create_SQLite_store("file", id, content) + _, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, store=store) + + await room.initialize() + assert room._document.source == content @pytest.mark.asyncio -async def test_defined_save_delay_should_save_content_after_document_change(): +async def test_should_overwrite_the_store(rtc_create_SQLite_store, rtc_create_mock_document_room): id = "test-id" content = "test" - paths = {id: "test.txt"} - cm = FakeContentsManager({"content": content}) - loader = FileLoader( - id, - FakeFileIDManager(paths), - cm, - poll_interval=0.1, - ) + store = await rtc_create_SQLite_store("file", id, "whatever") + _, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, store=store) + + await room.initialize() + assert room._document.source == content - room = DocumentRoom("test-room", "text", "file", loader, FakeEventLogger(), None, None, 0.01) + doc = YUnicode() + await store.apply_updates(doc.ydoc) + + assert doc.source == content + + +@pytest.mark.asyncio +async def test_defined_save_delay_should_save_content_after_document_change( + rtc_create_mock_document_room, +): + content = "test" + cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=0.01) await room.initialize() room._document.source = "Test 2" @@ -69,19 +71,11 @@ async def test_defined_save_delay_should_save_content_after_document_change(): @pytest.mark.asyncio -async def test_undefined_save_delay_should_not_save_content_after_document_change(): - id = "test-id" +async def test_undefined_save_delay_should_not_save_content_after_document_change( + rtc_create_mock_document_room, +): content = "test" - paths = {id: "test.txt"} - cm = FakeContentsManager({"content": content}) - loader = FileLoader( - id, - FakeFileIDManager(paths), - cm, - poll_interval=0.1, - ) - - room = DocumentRoom("test-room", "text", "file", loader, FakeEventLogger(), None, None, None) + cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=None) await room.initialize() room._document.source = "Test 2" @@ -93,20 +87,13 @@ async def test_undefined_save_delay_should_not_save_content_after_document_chang @pytest.mark.asyncio -async def test_should_reload_content_from_disk(): - id = "test-id" +async def test_should_reload_content_from_disk(rtc_create_mock_document_room): content = "test" - paths = {id: "test.txt"} last_modified = datetime.now() - cm = FakeContentsManager({"last_modified": last_modified, "content": "whatever"}) - loader = FileLoader( - id, - FakeFileIDManager(paths), - cm, - poll_interval=0.1, - ) - room = DocumentRoom("test-room", "text", "file", loader, FakeEventLogger(), None, None, None) + cm, loader, room = rtc_create_mock_document_room( + "test-id", "test.txt", "whatever", last_modified + ) await room.initialize() @@ -117,26 +104,17 @@ async def test_should_reload_content_from_disk(): await loader.notify() msg_id = next(iter(room._messages)).encode("utf8") - await room.handle_msg(bytes([RoomMessages.RELOAD]) + write_var_uint(len(msg_id)) + msg_id) + await room.handle_msg(reload_msg(msg_id)) assert room._document.source == content @pytest.mark.asyncio -async def test_should_not_reload_content_from_disk(): - id = "test-id" +async def test_should_not_reload_content_from_disk(rtc_create_mock_document_room): content = "test" - paths = {id: "test.txt"} last_modified = datetime.now() - cm = FakeContentsManager({"last_modified": datetime.now(), "content": content}) - loader = FileLoader( - id, - FakeFileIDManager(paths), - cm, - poll_interval=0.1, - ) - room = DocumentRoom("test-room", "text", "file", loader, FakeEventLogger(), None, None, None) + cm, loader, room = rtc_create_mock_document_room("test-id", "test.txt", content, last_modified) await room.initialize() @@ -147,6 +125,6 @@ async def test_should_not_reload_content_from_disk(): await loader.notify() msg_id = list(room._messages.keys())[0].encode("utf8") - await room.handle_msg(bytes([RoomMessages.OVERWRITE]) + write_var_uint(len(msg_id)) + msg_id) + await room.handle_msg(overite_msg(msg_id)) assert room._document.source == content diff --git a/tests/utils.py b/tests/utils.py index 8114b673..199e152c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,6 +7,9 @@ from typing import Any from jupyter_server import _tz as tz +from ypy_websocket.yutils import write_var_uint + +from jupyter_collaboration.utils import RoomMessages class FakeFileIDManager: @@ -52,3 +55,11 @@ def save_content(self, model: dict[str, Any], path: str) -> dict: class FakeEventLogger: def emit(self, schema_id: str, data: dict) -> None: print(data) + + +def reload_msg(msg_id: str) -> bytearray: + return bytes([RoomMessages.RELOAD]) + write_var_uint(len(msg_id)) + msg_id + + +def overite_msg(msg_id: str) -> bytearray: + return bytes([RoomMessages.OVERWRITE]) + write_var_uint(len(msg_id)) + msg_id From a23ebb2a60b0044990ebb06e65d961537f74a10a Mon Sep 17 00:00:00 2001 From: Carlos Herrero <26092748+hbcarlos@users.noreply.github.com> Date: Tue, 8 Aug 2023 18:15:44 +0200 Subject: [PATCH 4/6] Configures file_id_manager in tests --- .github/workflows/test.yml | 39 ++++++++++++++++++++++++++++++++++---- pyproject.toml | 1 + tests/conftest.py | 11 ++++++----- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a181bebb..8b7da4e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,19 +16,26 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Install dependencies run: | - pip install jupyterlab + pip install "jupyterlab>=4.0.0,<5" pip install -e . jlpm + - name: Run pre-commit uses: pre-commit/action@v2.0.0 with: extra_args: --all-files --hook-stage=manual + - name: Help message if pre-commit fail if: ${{ failure() }} run: | @@ -39,6 +46,7 @@ jobs: echo " pre-commit run" echo "or after-the-fact on already committed files with" echo " pre-commit run --all-files --hook-stage=manual" + - name: Lint frontend run: | jlpm run lint:check @@ -49,15 +57,21 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Install dependencies run: | - pip install jupyterlab + pip install "jupyterlab>=4.0.0,<5" pip install -e . jlpm + - name: Run Tests run: | set -eux @@ -78,28 +92,35 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Install the Python dependencies run: | + python -m pip install "jupyterlab>=4.0.0,<5" pip install -e ".[test]" codecov - python -m pip install jupyterlab + - name: List installed packages run: | pip freeze pip check + - name: Run the tests with Coverage if: ${{ !startsWith(matrix.python-version, 'pypy') && !startsWith(runner.os, 'Windows') }} run: | python -m pytest -vv --cov jupyter_collaboration --cov-branch --cov-report term-missing:skip-covered + - name: Run the tests on pypy and Windows if: ${{ startsWith(matrix.python-version, 'pypy') || startsWith(runner.os, 'Windows') }} run: | python -W ignore::ImportWarning -m pytest -vv + - name: Coverage if: ${{ !startsWith(matrix.python-version, 'pypy') && !startsWith(runner.os, 'Windows') }} run: | codecov + - name: Build the extension if: ${{ !startsWith(matrix.python-version, 'pypy') }} shell: bash @@ -118,12 +139,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 with: python_version: "3.11" + - name: Install minimum versions uses: jupyterlab/maintainer-tools/.github/actions/install-minimums@v1 + - name: Run the unit tests run: | pytest -vv -W default || pytest -vv -W default --lf @@ -136,15 +160,19 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Install the Python dependencies run: | pip install --pre -e ".[test]" + - name: List installed packages run: | pip freeze pip check + - name: Run the tests run: | pytest -vv -W default || pytest -vv --lf @@ -166,8 +194,10 @@ jobs: timeout-minutes: 15 steps: - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Download sdist uses: actions/download-artifact@v3 + - name: Install From SDist shell: bash run: | @@ -177,9 +207,10 @@ jobs: mkdir test tar --strip-components=1 -zxvf *.tar.gz -C ./test cd test + python -m pip install "jupyterlab>=4.0.0,<5" python -m pip install ".[test]" - python -m pip install jupyterlab echo "::endgroup::" + - name: Run Test shell: bash run: | diff --git a/pyproject.toml b/pyproject.toml index ded035a9..f501a189 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ dev = [ test = [ "coverage", "jupyter_server[test]>=2.0.0", + "jupyter_server_fileid[test]", "pytest>=7.0", "pytest-cov", "pytest-asyncio", diff --git a/tests/conftest.py b/tests/conftest.py index 9db6cc3b..234dbb3c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,6 @@ import json from asyncio import Event, sleep from datetime import datetime -from pathlib import Path from typing import Any import nbformat @@ -20,10 +19,7 @@ from .utils import FakeContentsManager, FakeEventLogger, FakeFileIDManager -pytest_plugins = ["jupyter_server.pytest_plugin"] - -HERE = Path(__file__).parent.resolve() -RESOURCES_PATH = f"{HERE}/resources" +pytest_plugins = ["jupyter_server.pytest_plugin", "jupyter_server_fileid.pytest_plugin"] @pytest.fixture @@ -36,6 +32,11 @@ def jp_server_config(jp_root_dir, jp_server_config): "disable_check_xsrf": True, }, "SQLiteYStore": {"db_path": str(jp_root_dir.joinpath(".rtc_test.db"))}, + "BaseFileIdManager": { + "root_dir": str(jp_root_dir), + "db_path": str(jp_root_dir.joinpath(".fid_test.db")), + "db_journal_mode": "OFF", + }, } From 0df00723e7474dd14fc656e38779ff627e0ccc67 Mon Sep 17 00:00:00 2001 From: Carlos Herrero <26092748+hbcarlos@users.noreply.github.com> Date: Wed, 9 Aug 2023 11:58:29 +0200 Subject: [PATCH 5/6] clean up test workflow --- .github/workflows/test.yml | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b7da4e4..f2ecada6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,9 +86,11 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.8", "3.11"] - include: - - os: ubuntu-latest - python-version: "pypy-3.8" + # PyPy is not supported because we use the file_id_manager. See: + # https://github.com/jupyter-server/jupyter_server_fileid/issues/44 + #include: + # - os: ubuntu-latest + # python-version: "pypy-3.8" steps: - name: Checkout uses: actions/checkout@v3 @@ -111,15 +113,19 @@ jobs: run: | python -m pytest -vv --cov jupyter_collaboration --cov-branch --cov-report term-missing:skip-covered - - name: Run the tests on pypy and Windows - if: ${{ startsWith(matrix.python-version, 'pypy') || startsWith(runner.os, 'Windows') }} - run: | - python -W ignore::ImportWarning -m pytest -vv + #- name: Run the tests on pypy + # if: ${{ startsWith(matrix.python-version, 'pypy') }} + # run: | + # PyPy is not supported because we use the file_id_manager. See: + # https://github.com/jupyter-server/jupyter_server_fileid/issues/44 + # python -W ignore::ImportWarning -m pytest -vv - - name: Coverage - if: ${{ !startsWith(matrix.python-version, 'pypy') && !startsWith(runner.os, 'Windows') }} + - name: Run the tests on Windows + if: ${{ startsWith(runner.os, 'Windows') }} run: | - codecov + python -W ignore::ImportWarning -m pytest -vv --cov jupyter_collaboration --cov-branch --cov-report term-missing:skip-covered + + - uses: jupyterlab/maintainer-tools/.github/actions/upload-coverage@v1 - name: Build the extension if: ${{ !startsWith(matrix.python-version, 'pypy') }} From fee33e3f2c476455b3c502c044ea2092cd2e9b13 Mon Sep 17 00:00:00 2001 From: Carlos Herrero <26092748+hbcarlos@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:43:33 +0200 Subject: [PATCH 6/6] pre-commit --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2ecada6..0d4807e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - + - name: Set up Python uses: actions/setup-python@v4 with: @@ -57,7 +57,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - + - name: Set up Python uses: actions/setup-python@v4 with: