Skip to content

Commit

Permalink
pytest-lsp: Enable testing of non lsp servers
Browse files Browse the repository at this point in the history
This further simplifies the architecture by removing the
`ClientServer` object.

Also by basing most of the plugin of the base `Client` from `pygls`
this should enable non LSP, JSON-RPC servers to "just work" within the
same framework
  • Loading branch information
alcarney committed Aug 27, 2023
1 parent 5a18119 commit d0fc9c4
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 76 deletions.
2 changes: 2 additions & 0 deletions lib/pytest-lsp/changes/73.enhancement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
It is now possible to test any JSON-RPC based server with ``pytest-lsp``.
Note however, this support will only ever extend to managing the client-server connection.
1 change: 1 addition & 0 deletions lib/pytest-lsp/changes/73.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``make_test_client`` has been renamed to ``make_test_lsp_client``
10 changes: 2 additions & 8 deletions lib/pytest-lsp/pytest_lsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,20 @@
from .client import LanguageClient
from .client import __version__
from .client import client_capabilities
from .client import make_test_client
from .plugin import ClientServer
from .client import make_test_lsp_client
from .plugin import ClientServerConfig
from .plugin import fixture
from .plugin import make_client_server
from .plugin import pytest_runtest_makereport
from .plugin import pytest_runtest_setup
from .protocol import LanguageClientProtocol

__all__ = [
"__version__",
"ClientServer",
"ClientServerConfig",
"LanguageClient",
"LanguageClientProtocol",
"LspSpecificationWarning",
"client_capabilities",
"fixture",
"make_client_server",
"make_test_client",
"make_test_lsp_client",
"pytest_runtest_makereport",
"pytest_runtest_setup",
]
2 changes: 1 addition & 1 deletion lib/pytest-lsp/pytest_lsp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def cancel_all_tasks(message: str):
task.cancel(message)


def make_test_client() -> LanguageClient:
def make_test_lsp_client() -> LanguageClient:
"""Construct a new test client instance with the handlers needed to capture
additional responses from the server."""

Expand Down
93 changes: 26 additions & 67 deletions lib/pytest-lsp/pytest_lsp/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,82 +4,42 @@
import textwrap
import typing
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional

import attrs
import pytest
import pytest_asyncio
from pygls.client import Client

from pytest_lsp.client import LanguageClient
from pytest_lsp.client import make_test_client
from pytest_lsp.client import make_test_lsp_client

logger = logging.getLogger("client")


class ClientServer:
"""A client server pair used to drive test cases."""

def __init__(self, *, client: LanguageClient, server_command: List[str]):
self.server_command = server_command
"""The command to use when starting the server."""

self.client = client
"""The client used to drive the test."""

async def start(self):
await self.client.start_io(*self.server_command)

async def stop(self):
await self.client.stop()


@attrs.define
class ClientServerConfig:
"""Configuration for a LSP Client-Server pair."""

def __init__(
self,
server_command: List[str],
*,
client_factory: Callable[[], LanguageClient] = make_test_client,
) -> None:
"""
Parameters
----------
server_command
The command to use to start the language server.
client_factory
Factory function to use when constructing the language client instance.
Defaults to :func:`pytest_lsp.make_test_client`
"""

self.server_command = server_command
self.client_factory = client_factory


def make_client_server(config: ClientServerConfig) -> ClientServer:
"""Construct a new ``ClientServer`` instance."""

return ClientServer(
server_command=config.server_command,
client=config.client_factory(),
)
"""Configuration for a Client-Server connection."""

server_command: List[str]
"""The command to use to start the language server."""

@pytest.hookimpl(trylast=True)
def pytest_runtest_setup(item: pytest.Item):
"""Ensure that that client has not errored before running a test."""
client_factory: Callable[[], Client] = attrs.field(
default=make_test_lsp_client,
)
"""Factory function to use when constructing the test client instance."""

client: Optional[LanguageClient] = None
for arg in item.funcargs.values(): # type: ignore[attr-defined]
if isinstance(arg, LanguageClient):
client = arg
break
server_env: Optional[Dict[str, str]] = attrs.field(default=None)
"""Environment variables to set when starting the server."""

if not client or client.error is None:
return
async def start(self) -> Client:
"""Return the client instance to use for the test."""
client = self.client_factory()

raise client.error
await client.start_io(*self.server_command, env=self.server_env)
return client


def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo):
Expand Down Expand Up @@ -123,7 +83,7 @@ async def anext(it):

def get_fixture_arguments(
fn: Callable,
client: LanguageClient,
client: Client,
request: pytest.FixtureRequest,
) -> dict:
"""Return the arguments to pass to the user's fixture function.
Expand All @@ -134,7 +94,7 @@ def get_fixture_arguments(
The user's fixture function
client
The language client instance to inject
The test client instance to inject
request
pytest's request fixture
Expand All @@ -154,7 +114,7 @@ def get_fixture_arguments(

# Inject the language client
for name, cls in typing.get_type_hints(fn).items():
if issubclass(cls, LanguageClient):
if issubclass(cls, Client):
kwargs[name] = client
required_parameters.remove(name)

Expand Down Expand Up @@ -183,26 +143,25 @@ def fixture(
def wrapper(fn):
@pytest_asyncio.fixture(**kwargs)
async def the_fixture(request):
client_server = make_client_server(config)
await client_server.start()
client = await config.start()

kwargs = get_fixture_arguments(fn, client_server.client, request)
kwargs = get_fixture_arguments(fn, client, request)
result = fn(**kwargs)
if inspect.isasyncgen(result):
try:
await anext(result)
except StopAsyncIteration:
pass

yield client_server.client
yield client

if inspect.isasyncgen(result):
try:
await anext(result)
except StopAsyncIteration:
pass

await client_server.stop()
await client.stop()

return the_fixture

Expand Down

0 comments on commit d0fc9c4

Please sign in to comment.