Skip to content

Commit

Permalink
pytest-lsp: Document generic RPC server testing
Browse files Browse the repository at this point in the history
  • Loading branch information
alcarney committed Sep 8, 2023
1 parent 4516219 commit 4ece492
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
1 change: 1 addition & 0 deletions docs/pytest-lsp/guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ User Guide
guide/client-capabilities
guide/fixtures
guide/troubleshooting
guide/testing-json-rpc-servers
60 changes: 60 additions & 0 deletions docs/pytest-lsp/guide/testing-json-rpc-servers.rst
Original file line number Diff line number Diff line change
@@ -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
8 changes: 2 additions & 6 deletions docs/pytest-lsp/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ LanguageClient

.. autoclass:: LanguageClient
:members:
:inherited-members:
:show-inheritance:


Test Setup
Expand All @@ -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
------
Expand Down
30 changes: 30 additions & 0 deletions lib/pytest-lsp/tests/examples/generic-rpc/server.py
Original file line number Diff line number Diff line change
@@ -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()
50 changes: 50 additions & 0 deletions lib/pytest-lsp/tests/examples/generic-rpc/t_server.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions lib/pytest-lsp/tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down

0 comments on commit 4ece492

Please sign in to comment.