From 2255675ca8a94ca1132508f2637f023a3ac53f6d Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Fri, 8 Sep 2023 16:24:57 +0100 Subject: [PATCH] pytest-lsp: Document generic RPC server testing --- docs/conf.py | 1 + docs/pytest-lsp/guide.rst | 1 + .../guide/testing-json-rpc-servers.rst | 60 +++++++++++++++++++ docs/pytest-lsp/reference.rst | 8 +-- .../tests/examples/generic-rpc/server.py | 30 ++++++++++ .../tests/examples/generic-rpc/t_server.py | 50 ++++++++++++++++ lib/pytest-lsp/tests/test_examples.py | 12 ++++ 7 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 docs/pytest-lsp/guide/testing-json-rpc-servers.rst create mode 100644 lib/pytest-lsp/tests/examples/generic-rpc/server.py create mode 100644 lib/pytest-lsp/tests/examples/generic-rpc/t_server.py diff --git a/docs/conf.py b/docs/conf.py index b2672f0..538e610 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,6 +37,7 @@ autodoc_typehints = "description" intersphinx_mapping = { + "pygls": ("https://pygls.readthedocs.io/en/latest/", None), "python": ("https://docs.python.org/3/", None), "pytest": ("https://docs.pytest.org/en/stable/", None), } diff --git a/docs/pytest-lsp/guide.rst b/docs/pytest-lsp/guide.rst index 6bdfb6a..b8b4f4d 100644 --- a/docs/pytest-lsp/guide.rst +++ b/docs/pytest-lsp/guide.rst @@ -9,3 +9,4 @@ User Guide guide/client-capabilities guide/fixtures guide/troubleshooting + guide/testing-json-rpc-servers diff --git a/docs/pytest-lsp/guide/testing-json-rpc-servers.rst b/docs/pytest-lsp/guide/testing-json-rpc-servers.rst new file mode 100644 index 0000000..1005c59 --- /dev/null +++ b/docs/pytest-lsp/guide/testing-json-rpc-servers.rst @@ -0,0 +1,60 @@ +Testing JSON-RPC Servers +======================== + +While ``pytest-lsp`` is primarily focused on writing tests for LSP servers it is possible to reuse some of the machinery to test other JSON-RPC servers. + +A Simple JSON-RPC Server +------------------------ + +As an example we'll reuse some of the `pygls`_ internals to write a simple JSON-RPC server that implements the following protocol. + +- client to server request ``math/add``, returns the sum of two numbers ``a`` and ``b`` +- client to server request ``math/sub``, returns the difference of two numbers ``a`` and ``b`` +- server to client notification ``log/message``, allows the server to send debug messages to the client. + +.. note:: + + The details of the implementation below don't really matter as we just need *something* to help us illustrate how to use ``pytest-lsp`` in this way. + + Remember you can write your servers in whatever language/framework you prefer! + +.. literalinclude:: ../../../lib/pytest-lsp/tests/examples/generic-rpc/server.py + :language: python + +Constructing a Client +--------------------- + +While ``pytest-lsp`` can manage the connection between client and server, it needs to be given a client that understands the protocol that the server implements. +This is done with a factory function + +.. literalinclude:: ../../../lib/pytest-lsp/tests/examples/generic-rpc/t_server.py + :language: python + :start-at: def client_factory(): + :end-at: return client + +The Client Fixture +------------------ + +Once you have your factory function defined you can pass it to the :class:`~pytest_lsp.ClientServerConfig` when defining your client fixture + +.. literalinclude:: ../../../lib/pytest-lsp/tests/examples/generic-rpc/t_server.py + :language: python + :start-at: @pytest_lsp.fixture( + :end-at: # Teardown code + +Writing Test Cases +------------------ + +With the client fixuture defined, test cases are written almost identically as they would be for your LSP servers. +The only difference is that the generic :meth:`~pygls:pygls.protocol.JsonRPCProtocol.send_request_async` and :meth:`~pygls:pygls.protocol.JsonRPCProtocol.notify` methods are used to communicate with the server. + +.. literalinclude:: ../../../lib/pytest-lsp/tests/examples/generic-rpc/t_server.py + :language: python + :start-at: @pytest.mark.asyncio + +However, it is also possible to extend the base :class:`~pygls:pygls.client.JsonRPCClient` to provide a higher level interface to your server. +See the `SubprocessSphinxClient`_ from the `esbonio`_ project for such an example. + +.. _esbonio: https://github.com/swyddfa/esbonio +.. _pygls: https://github.com/openlawlibrary/pygls +.. _SubprocessSphinxClient: https://github.com/swyddfa/esbonio/blob/develop/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py diff --git a/docs/pytest-lsp/reference.rst b/docs/pytest-lsp/reference.rst index 8768a30..d6daa55 100644 --- a/docs/pytest-lsp/reference.rst +++ b/docs/pytest-lsp/reference.rst @@ -8,7 +8,7 @@ LanguageClient .. autoclass:: LanguageClient :members: - :inherited-members: + :show-inheritance: Test Setup @@ -21,12 +21,8 @@ Test Setup .. autoclass:: ClientServerConfig :members: -.. autofunction:: make_client_server +.. autofunction:: make_test_lsp_client -.. autofunction:: make_test_client - -.. autoclass:: ClientServer - :members: Checks ------ diff --git a/lib/pytest-lsp/tests/examples/generic-rpc/server.py b/lib/pytest-lsp/tests/examples/generic-rpc/server.py new file mode 100644 index 0000000..3381860 --- /dev/null +++ b/lib/pytest-lsp/tests/examples/generic-rpc/server.py @@ -0,0 +1,30 @@ +from pygls.protocol import JsonRPCProtocol, default_converter +from pygls.server import Server + +server = Server(protocol_cls=JsonRPCProtocol, converter_factory=default_converter) + + +@server.lsp.fm.feature("math/add") +def addition(ls: Server, params): + a = params.a + b = params.b + + ls.lsp.notify("log/message", dict(message=f"{a=}")) + ls.lsp.notify("log/message", dict(message=f"{b=}")) + + return dict(total=a + b) + + +@server.lsp.fm.feature("math/sub") +def subtraction(ls: Server, params): + a = params.a + b = params.b + + ls.lsp.notify("log/message", dict(message=f"{a=}")) + ls.lsp.notify("log/message", dict(message=f"{b=}")) + + return dict(total=b - a) + + +if __name__ == "__main__": + server.start_io() diff --git a/lib/pytest-lsp/tests/examples/generic-rpc/t_server.py b/lib/pytest-lsp/tests/examples/generic-rpc/t_server.py new file mode 100644 index 0000000..38b6e3c --- /dev/null +++ b/lib/pytest-lsp/tests/examples/generic-rpc/t_server.py @@ -0,0 +1,50 @@ +import logging +import sys + +import pytest +import pytest_lsp +from pygls.client import JsonRPCClient +from pytest_lsp import ClientServerConfig + + +def client_factory(): + client = JsonRPCClient() + + @client.feature("log/message") + def _on_message(params): + logging.info("LOG: %s", params.message) + + return client + + +@pytest_lsp.fixture( + config=ClientServerConfig( + client_factory=client_factory, server_command=[sys.executable, "server.py"] + ), +) +async def client(rpc_client: JsonRPCClient): + # Setup code here (if any) + + yield + + # Teardown code here (if any) + + +@pytest.mark.asyncio +async def test_add(client: JsonRPCClient): + """Ensure that the server implements addition correctly.""" + + result = await client.protocol.send_request_async( + "math/add", params={"a": 1, "b": 2} + ) + assert result.total == 3 + + +@pytest.mark.asyncio +async def test_sub(client: JsonRPCClient): + """Ensure that the server implements addition correctly.""" + + result = await client.protocol.send_request_async( + "math/sub", params={"a": 1, "b": 2} + ) + assert result.total == -1 diff --git a/lib/pytest-lsp/tests/test_examples.py b/lib/pytest-lsp/tests/test_examples.py index e900f12..d493e3d 100644 --- a/lib/pytest-lsp/tests/test_examples.py +++ b/lib/pytest-lsp/tests/test_examples.py @@ -101,6 +101,18 @@ def test_getting_started_fail(pytester: pytest.Pytester): results.stdout.fnmatch_lines(message) +def test_generic_rpc(pytester: pytest.Pytester): + """Ensure that the generic rpc example works as expected""" + + setup_test(pytester, "generic-rpc") + + results = pytester.runpytest("--log-cli-level", "info") + results.assert_outcomes(passed=1, failed=1) + + results.stdout.fnmatch_lines(" *LOG: a=1") + results.stdout.fnmatch_lines(" *LOG: b=2") + + def test_window_log_message_fail(pytester: pytest.Pytester): """Ensure that the initial getting started example fails as expected."""