diff --git a/src/cmake_language_server/server.py b/src/cmake_language_server/server.py index 1fd0786..bfb69ae 100644 --- a/src/cmake_language_server/server.py +++ b/src/cmake_language_server/server.py @@ -23,8 +23,8 @@ class CMakeLanguageServer(LanguageServer): _parser: ListParser _api: API - def __init__(self): - super().__init__() + def __init__(self, *args): + super().__init__(*args) self._parser = ListParser() self._api = None diff --git a/tests/conftest.py b/tests/conftest.py index 236ced0..aafea51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,19 @@ +import asyncio +import logging +import os +import pprint +from subprocess import PIPE, run +from threading import Thread + import pytest +from pygls import features +from pygls.server import LanguageServer + +from cmake_language_server.server import CMakeLanguageServer @pytest.fixture() def cmake_build(shared_datadir): - from subprocess import run, PIPE source = shared_datadir / 'cmake' build = source / 'build' build.mkdir() @@ -13,11 +23,37 @@ def cmake_build(shared_datadir): stderr=PIPE, universal_newlines=True) if p.returncode != 0: - import logging - import os - import pprint logging.error('env:\n' + pprint.pformat(os.environ)) logging.error('stdout:\n' + p.stdout) logging.error('stderr:\n' + p.stderr) raise RuntimeError("CMake failed") yield build + + +@pytest.fixture() +def client_server(): + c2s_r, c2s_w = os.pipe() + s2c_r, s2c_w = os.pipe() + + def start(ls: LanguageServer, fdr, fdw): + # TODO: better patch is needed + # disable `close()` to avoid error messages + close = ls.loop.close + ls.loop.close = lambda: None + ls.start_io(os.fdopen(fdr, 'rb'), os.fdopen(fdw, 'wb')) + ls.loop.close = close + + server = CMakeLanguageServer(asyncio.new_event_loop()) + server_thread = Thread(target=start, args=(server, c2s_r, s2c_w)) + server_thread.start() + + client = LanguageServer(asyncio.new_event_loop()) + client_thread = Thread(target=start, args=(client, s2c_r, c2s_w)) + client_thread.start() + + yield client, server + + client.send_notification(features.EXIT) + server.send_notification(features.EXIT) + server_thread.join() + client_thread.join() diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..22863df --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,130 @@ +from concurrent import futures +from pathlib import Path +from typing import Optional + +from pygls.features import (COMPLETION, FORMATTING, HOVER, INITIALIZE, + TEXT_DOCUMENT_DID_OPEN) +from pygls.server import LanguageServer +from pygls.types import (CompletionContext, CompletionParams, + CompletionTriggerKind, DidOpenTextDocumentParams, + DocumentFormattingParams, FormattingOptions, + InitializeParams, Position, TextDocumentIdentifier, + TextDocumentItem, TextDocumentPositionParams) + +CALL_TIMEOUT = 2 + + +def _init(client: LanguageServer, root: Path): + retry = 3 + while retry > 0: + try: + client.lsp.send_request( + INITIALIZE, + InitializeParams( + process_id=1234, root_uri=root.as_uri(), + capabilities=None)).result(timeout=CALL_TIMEOUT) + except futures.TimeoutError: + retry -= 1 + else: + break + + +def _open(client: LanguageServer, path: Path, text: Optional[str] = None): + if text is None: + with open(path) as fp: + text = fp.read() + + client.lsp.notify( + TEXT_DOCUMENT_DID_OPEN, + DidOpenTextDocumentParams( + TextDocumentItem(path.as_uri(), 'cmake', 1, text))) + + +def test_initialize(client_server, datadir): + client, server = client_server + + assert server._api is None + _init(client, datadir) + assert server._api is not None + + +def test_completions_invoked(client_server, datadir): + client, server = client_server + _init(client, datadir) + path = datadir / 'CMakeLists.txt' + _open(client, path, 'projec') + response = client.lsp.send_request( + COMPLETION, + CompletionParams(TextDocumentIdentifier(path.as_uri()), Position( + 0, 6), CompletionContext( + CompletionTriggerKind.Invoked))).result(timeout=CALL_TIMEOUT) + item = next(filter(lambda x: x.label == 'project', response.items), None) + assert item is not None + assert '' in item.documentation + + +def test_completions_triggercharacter_variable(client_server, datadir): + client, server = client_server + _init(client, datadir) + path = datadir / 'CMakeLists.txt' + _open(client, path, '${') + response = client.lsp.send_request( + COMPLETION, + CompletionParams( + TextDocumentIdentifier(path.as_uri()), Position(0, 2), + CompletionContext(CompletionTriggerKind.TriggerCharacter, + '{'))).result(timeout=CALL_TIMEOUT) + assert 'PROJECT_VERSION' in [x.label for x in response.items] + + +def test_completions_triggercharacter_module(client_server, datadir): + client, server = client_server + _init(client, datadir) + path = datadir / 'CMakeLists.txt' + _open(client, path, 'include(') + response = client.lsp.send_request( + COMPLETION, + CompletionParams( + TextDocumentIdentifier(path.as_uri()), Position(0, 8), + CompletionContext(CompletionTriggerKind.TriggerCharacter, + '('))).result(timeout=CALL_TIMEOUT) + assert 'GoogleTest' in [x.label for x in response.items] + + +def test_completions_triggercharacter_package(client_server, datadir): + client, server = client_server + _init(client, datadir) + path = datadir / 'CMakeLists.txt' + _open(client, path, 'find_package(') + response = client.lsp.send_request( + COMPLETION, + CompletionParams( + TextDocumentIdentifier(path.as_uri()), Position(0, 13), + CompletionContext(CompletionTriggerKind.TriggerCharacter, + '('))).result(timeout=CALL_TIMEOUT) + assert 'Boost' in [x.label for x in response.items] + + +def test_formatting(client_server, datadir): + client, server = client_server + _init(client, datadir) + path = datadir / 'CMakeLists.txt' + _open(client, path, 'a ( b c ) ') + response = client.lsp.send_request( + FORMATTING, + DocumentFormattingParams(TextDocumentIdentifier(path.as_uri()), + FormattingOptions( + 2, True))).result(timeout=CALL_TIMEOUT) + assert response[0].newText == 'a(b c)\n' + + +def test_hover(client_server, datadir): + client, server = client_server + _init(client, datadir) + path = datadir / 'CMakeLists.txt' + _open(client, path, 'project()') + response = client.lsp.send_request( + HOVER, + TextDocumentPositionParams(TextDocumentIdentifier(path.as_uri()), + Position())).result(timeout=CALL_TIMEOUT) + assert '' in response.contents.value diff --git a/tox.ini b/tox.ini index 027a718..ea9eb0a 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ passenv = INCLUDE LIB LIBPATH Platform VCTools* VSCMD_* WindowsSDK* commands_pre = poetry install commands = - poetry run pytest --cov-report=xml --cov=src -sv tests + poetry run pytest --cov-report=term --cov-report=xml --cov=src -sv tests [testenv:lint] commands =