Skip to content

Commit

Permalink
pytest-lsp: Cancel pendining notification futures on server exit
Browse files Browse the repository at this point in the history
  • Loading branch information
alcarney committed Nov 4, 2024
1 parent 42403fb commit fbe8d71
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/pytest-lsp/changes/186.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`pytest-lsp` is now able to detect the situation where the server process exits while the client is waiting on a notification message and fail the test accordingly
8 changes: 8 additions & 0 deletions lib/pytest-lsp/pytest_lsp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ async def server_exit(self, server: asyncio.subprocess.Process):
if self._stop_event.is_set():
return

reason = (
f"Server process {server.pid} exited with return code: {server.returncode}"
)
for id_, fut in self.protocol._notification_futures.items():
if not fut.done():
fut.set_exception(RuntimeError(reason))
logger.debug("Cancelled pending request '%s': %s", id_, reason)

def report_server_error(
self, error: Exception, source: PyglsError | JsonRpcException
):
Expand Down
30 changes: 30 additions & 0 deletions lib/pytest-lsp/tests/servers/notify_exit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# A server that exits mid request.
import sys

from lsprotocol import types
from pygls.lsp.server import LanguageServer


class CountingLanguageServer(LanguageServer):
count: int = 0


server = CountingLanguageServer(name="completion-exit-server", version="v1.0")


@server.feature("server/exit")
def server_exit(*args):
sys.exit(0)


@server.feature(types.TEXT_DOCUMENT_COMPLETION)
def on_complete(server: CountingLanguageServer, params: types.CompletionParams):
server.count += 1
if server.count == 5:
sys.exit(0)

return [types.CompletionItem(label=f"{server.count}")]


if __name__ == "__main__":
server.start_io()
37 changes: 37 additions & 0 deletions lib/pytest-lsp/tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,43 @@ async def test_capabilities(client):
results.stdout.fnmatch_lines("E*RuntimeError: Client has been stopped.")


def test_detect_server_exit_pending_notification(pytester: pytest.Pytester):
"""Ensure that the plugin can detect when the server process exits while the client
is waiting for a notification to arrive."""

test_code = """\
import pytest
from lsprotocol.types import CompletionParams
from lsprotocol.types import Position
from lsprotocol.types import TextDocumentIdentifier
@pytest.mark.asyncio
async def test_capabilities(client):
expected = {str(i) for i in range(10)}
for i in range(10):
client.protocol.notify("server/exit")
await client.wait_for_notification("never/happening")
params = CompletionParams(
text_document=TextDocumentIdentifier(uri="file:///test.txt"),
position=Position(line=0, character=0)
)
items = await client.text_document_completion_async(params)
assert len({i.label for i in items} & expected) == len(items)
"""

setup_test(pytester, "notify_exit.py", test_code)
results = pytester.runpytest("-vv")

results.assert_outcomes(failed=1, errors=1)

message = r"E\s+RuntimeError: Server process \d+ exited with return code: 0"
results.stdout.re_match_lines(message)
results.stdout.fnmatch_lines("E*RuntimeError: Client has been stopped.")


def test_detect_server_crash(pytester: pytest.Pytester):
"""Ensure the plugin can detect when the server process crashes on boot."""

Expand Down

0 comments on commit fbe8d71

Please sign in to comment.