From 351c97deeadb523179718076f851e88ead1c348f Mon Sep 17 00:00:00 2001 From: Kilari Teja Date: Wed, 4 Apr 2018 07:42:35 +0530 Subject: [PATCH] Update to jsonrpc lib This updates the project to use jsonrpc module from python-language-server. Closes https://github.com/coala/coala-vs-code/issues/27 --- coala_langserver/jsonrpc.py | 148 -------- coala_langserver/langserver.py | 121 ++++--- requirements.txt | 1 + tests/server.features/jsonrpc.feature | 39 +-- tests/server.features/langserver.feature | 47 ++- tests/server.features/steps/jsonrpc_steps.py | 143 ++++---- .../server.features/steps/langserver_steps.py | 323 +++++++++++++++++- 7 files changed, 515 insertions(+), 307 deletions(-) delete mode 100644 coala_langserver/jsonrpc.py diff --git a/coala_langserver/jsonrpc.py b/coala_langserver/jsonrpc.py deleted file mode 100644 index bc11cba..0000000 --- a/coala_langserver/jsonrpc.py +++ /dev/null @@ -1,148 +0,0 @@ -import json -import random -import uuid -from collections import OrderedDict - -from .log import log - - -class JSONRPC2Error(Exception): - pass - - -class ReadWriter: - - def __init__(self, reader, writer): - self.reader = reader - self.writer = writer - - def readline(self, *args): - return self.reader.readline(*args) - - def read(self, *args): - return self.reader.read(*args) - - def write(self, out): - self.writer.write(out) - self.writer.flush() - - -class TCPReadWriter(ReadWriter): - - def readline(self, *args): - data = self.reader.readline(*args) - return data.decode('utf-8') - - def read(self, *args): - return self.reader.read(*args).decode('utf-8') - - def write(self, out): - self.writer.write(out.encode()) - self.writer.flush() - - -class JSONRPC2Connection: - - def __init__(self, conn=None): - self.conn = conn - self._msg_buffer = OrderedDict() - - def handle(self, id, request): - pass - - @staticmethod - def _read_header_content_length(line): - if len(line) < 2 or line[-2:] != '\r\n': - raise JSONRPC2Error('Line endings must be \\r\\n') - if line.startswith('Content-Length: '): - _, value = line.split('Content-Length: ') - value = value.strip() - try: - return int(value) - except ValueError: - raise JSONRPC2Error( - 'Invalid Content-Length header: {}'.format(value)) - - def _receive(self): - line = self.conn.readline() - if line == '': - raise EOFError() - length = self._read_header_content_length(line) - # Keep reading headers until we find the sentinel line for the JSON - # request. - while line != '\r\n': - line = self.conn.readline() - body = self.conn.read(length) - obj = json.loads(body) - # If the next message doesn't have an id, just give it a random key. - self._msg_buffer[obj.get('id') or uuid.uuid4()] = obj - - def read_message(self, _id=None): - """Read a JSON RPC message sent over the current connection. If - id is None, the next available message is returned.""" - if _id is not None: - while self._msg_buffer.get(_id) is None: - self._receive() - return self._msg_buffer.pop(_id) - else: - while len(self._msg_buffer) == 0: - self._receive() - _, msg = self._msg_buffer.popitem(last=False) - return msg - - def write_response(self, _id, result): - body = { - 'jsonrpc': '2.0', - 'id': _id, - 'result': result, - } - body = json.dumps(body, separators=(',', ':')) - content_length = len(body) - response = ( - 'Content-Length: {}\r\n' - 'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n' - '{}'.format(content_length, body)) - self.conn.write(response) - log('RESPONSE: ', response) - - def send_request(self, method: str, params): - _id = random.randint(0, 2 ** 16) # TODO(renfred) guarantee uniqueness. - body = { - 'jsonrpc': '2.0', - 'id': _id, - 'method': method, - 'params': params, - } - body = json.dumps(body, separators=(',', ':')) - content_length = len(body) - request = ( - 'Content-Length: {}\r\n' - 'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n' - '{}'.format(content_length, body)) - log('SENDING REQUEST: ', request) - self.conn.write(request) - return self.read_message(_id) - - def send_notification(self, method: str, params): - body = { - 'jsonrpc': '2.0', - 'method': method, - 'params': params, - } - body = json.dumps(body, separators=(',', ':')) - content_length = len(body) - notification = ( - 'Content-Length: {}\r\n' - 'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n' - '{}'.format(content_length, body)) - log('SENDING notification: ', notification) - self.conn.write(notification) - return None - - def listen(self): - while True: - try: - request = self.read_message() - except EOFError: - break - self.handle(id, request) diff --git a/coala_langserver/langserver.py b/coala_langserver/langserver.py index d1b94a4..5d5bb81 100644 --- a/coala_langserver/langserver.py +++ b/coala_langserver/langserver.py @@ -3,84 +3,76 @@ import socketserver import traceback -from .jsonrpc import JSONRPC2Connection, ReadWriter, TCPReadWriter +from pyls.jsonrpc.endpoint import Endpoint +from pyls.jsonrpc.dispatchers import MethodDispatcher +from pyls.jsonrpc.streams import JsonRpcStreamReader +from pyls.jsonrpc.streams import JsonRpcStreamWriter +from coala_utils.decorators import enforce_signature from .log import log from .coalashim import run_coala_with_specific_file from .uri import path_from_uri from .diagnostic import output_to_diagnostics -class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): - pass +class _StreamHandlerWrapper(socketserver.StreamRequestHandler, object): + """ + A wrapper class that is used to construct a custom handler class. + """ + delegate = None -class LangserverTCPTransport(socketserver.StreamRequestHandler): + def setup(self): + super(_StreamHandlerWrapper, self).setup() + self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile) def handle(self): - s = LangServer(conn=TCPReadWriter(self.rfile, self.wfile)) - try: - s.listen() - except Exception as e: - tb = traceback.format_exc() - log('ERROR: {} {}'.format(e, tb)) + self.delegate.start() -class LangServer(JSONRPC2Connection): +class LangServer(MethodDispatcher): """ Language server for coala base on JSON RPC. """ - def __init__(self, conn=None): - super().__init__(conn=conn) + def __init__(self, rx, tx): self.root_path = None + self._jsonrpc_stream_reader = JsonRpcStreamReader(rx) + self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx) + self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write) + self._dispatchers = [] + self._shutdown = False - def handle(self, _id, request): - """ - Handle the request from language client. - """ - log('REQUEST: ', request) - resp = None - - if request['method'] == 'initialize': - resp = self.serve_initialize(request) - # TODO: Support did_change and did_change_watched_files. - # elif request["method"] == "textDocument/didChange": - # resp = self.serve_change(request) - # elif request["method"] == "workspace/didChangeWatchedFiles": - # resp = self.serve_did_change_watched_files(request) - elif request['method'] == 'textDocument/didSave': - self.serve_did_save(request) - - if resp is not None: - self.write_response(request['id'], resp) - - def serve_initialize(self, request): + def start(self): + self._jsonrpc_stream_reader.listen(self._endpoint.consume) + + def m_initialize(self, rootUri=None, rootPath=None, **kargs): """ Serve for the initialization request. """ - params = request['params'] # Notice that the root_path could be None. - if 'rootUri' in params: - self.root_path = path_from_uri(params['rootUri']) - elif 'rootPath' in params: - self.root_path = path_from_uri(params['rootPath']) + if rootUri is not None: + self.root_path = path_from_uri(rootUri) + elif rootPath is not None: + self.root_path = path_from_uri(rootPath) return { 'capabilities': { 'textDocumentSync': 1 } } - def serve_did_save(self, request): + def m_text_document__did_save(self, textDocument=None, **kargs): """ Serve for did_change request. """ - params = request['params'] - uri = params['textDocument']['uri'] + uri = textDocument['uri'] path = path_from_uri(uri) diagnostics = output_to_diagnostics( run_coala_with_specific_file(self.root_path, path)) self.send_diagnostics(path, diagnostics) + def m_shutdown(self, **_kwargs): + self._shutdown = True + # TODO: Support did_change and did_change_watched_files. # def serve_change(self, request): # '""Serve for the request of documentation changed.""' @@ -110,7 +102,38 @@ def send_diagnostics(self, path, diagnostics): 'uri': 'file://{0}'.format(path), 'diagnostics': _diagnostics, } - self.send_notification('textDocument/publishDiagnostics', params) + self._endpoint.notify('textDocument/publishDiagnostics', params=params) + + +@enforce_signature +def start_tcp_lang_server(handler_class: LangServer, bind_addr, port): + # Construct a custom wrapper class around the user's handler_class + wrapper_class = type( + handler_class.__name__ + 'Handler', + (_StreamHandlerWrapper,), + {'DELEGATE_CLASS': handler_class}, + ) + + try: + server = socketserver.TCPServer((bind_addr, port), wrapper_class) + except Exception as e: + log('Fatal Exception: {}'.format(e)) + sys.exit(1) + + log('Serving {} on ({}, {})'.format( + handler_class.__name__, bind_addr, port)) + try: + server.serve_forever() + finally: + log('Shutting down') + server.server_close() + + +@enforce_signature +def start_io_lang_server(handler_class: LangServer, rstream, wstream): + log('Starting {} IO language server'.format(handler_class.__name__)) + server = handler_class(rstream, wstream) + server.start() def main(): @@ -123,18 +146,10 @@ def main(): args = parser.parse_args() if args.mode == 'stdio': - log('Reading on stdin, writing on stdout') - s = LangServer(conn=ReadWriter(sys.stdin, sys.stdout)) - s.listen() + start_io_lang_server(LangServer, sys.stdin.buffer, sys.stdout.buffer) elif args.mode == 'tcp': host, addr = '0.0.0.0', args.addr - log('Accepting TCP connections on {}:{}'.format(host, addr)) - ThreadingTCPServer.allow_reuse_address = True - s = ThreadingTCPServer((host, addr), LangserverTCPTransport) - try: - s.serve_forever() - finally: - s.shutdown() + start_tcp_lang_server(LangServer, host, addr) if __name__ == '__main__': diff --git a/requirements.txt b/requirements.txt index 676d7fd..03205ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ coala>=0.10.0.dev20170213201648 typing>=3.5.3.0 coala-bears>=0.10.0.dev20170215041744 +python-language-server~=0.18.0 diff --git a/tests/server.features/jsonrpc.feature b/tests/server.features/jsonrpc.feature index 047f604..24ae1ad 100644 --- a/tests/server.features/jsonrpc.feature +++ b/tests/server.features/jsonrpc.feature @@ -1,35 +1,20 @@ Feature: jsonrpc module jsonrpc is a module of language-server. - Scenario: Test ReadWriter - Given the string - When I write it to ReadWriter - Then it should read from ReadWriter + Scenario: Test JsonRpcStreamWriter and JsonRpcStreamReader + Given the message + When I write it to JsonRpcStreamWriter + Then it should read from JsonRpcStreamReader - Scenario: Test ReadWriter - Given the string - When I write it to ReadWriter - Then it should readline from ReadWriter + Scenario: Test notification and disptacher + Given a notification type rpc request + When I send rpc request using JsonRpcStreamWriter + Then it should invoke the notification consumer with args - Scenario: Test TCPReadWriter - Given the string - When I write it to TCPReadWriter - Then it should read from TCPReadWriter - - Scenario: Test TCPReadWriter - Given the string - When I write it to TCPReadWriter - Then it should readline from TCPReadWriter - - Scenario: Test send_notification and read_message - Given the JSONRPC2Connection instance - When I write a notification to the JSONRPC2Connection - Then it should return the notification from JSONRPC2Connection - - Scenario: Test write_response - Given the JSONRPC2Connection instance - When I write a response to the JSONRPC2Connection - Then it should return the response from JSONRPC2Connection + Scenario: Test rpc request and response + Given a request type rpc request + When I send rpc request using JsonRpcStreamWriter + Then it should invoke consumer and return response # TODO: block until we have generantee the unique request. # Scenario: Test send_request diff --git a/tests/server.features/langserver.feature b/tests/server.features/langserver.feature index d8b2c5e..23e2c6e 100644 --- a/tests/server.features/langserver.feature +++ b/tests/server.features/langserver.feature @@ -1,13 +1,52 @@ Feature: langserver module langserver is the main program of language-server. - Scenario: Test serve_initialize + Scenario: Test serve_initialize with rootPath Given the LangServer instance - When I send a initialize request to the server + When I send a initialize request with rootPath to the server Then it should return the response with textDocumentSync - # TODO: Add positive test case. - Scenario: Test serve_did_save + Scenario: Test serve_initialize with rootUri + Given the LangServer instance + When I send a initialize request with rootUri to the server + Then it should return the response with textDocumentSync + + Scenario: Test send_diagnostics + Given the LangServer instance + When I invoke send_diagnostics message + Then it should send a publishDiagnostics request + + Scenario: Test negative m_text_document__did_save Given the LangServer instance When I send a did_save request about a non-existed file to the server Then it should send a publishDiagnostics request + + Scenario: Test positive m_text_document__did_save + Given the LangServer instance + When I send a did_save request about a existing file to the server + Then it should send a publishDiagnostics request + + Scenario: Test when coafile is missing + Given the LangServer instance + When I send a did_save request on a file with no coafile to server + Then it should send a publishDiagnostics request + + Scenario: Test didChange + Given the LangServer instance + When I send a did_change request about a file to the server + Then it should ignore the request + + Scenario: Test langserver shutdown + Given the LangServer instance + When I send a shutdown request to the server + Then it should shutdown + + Scenario: Test language server in stdio mode + Given I send a initialize request via stdio stream + When the server is started in stdio mode + Then it should return the response with textDocumentSync via stdio + + Scenario: Test language server in tcp mode + Given the server started in TCP mode + When I send a initialize request via TCP stream + Then it should return the response with textDocumentSync via TCP diff --git a/tests/server.features/steps/jsonrpc_steps.py b/tests/server.features/steps/jsonrpc_steps.py index 43db5a5..3164aff 100644 --- a/tests/server.features/steps/jsonrpc_steps.py +++ b/tests/server.features/steps/jsonrpc_steps.py @@ -7,101 +7,122 @@ import tempfile import json from behave import given, when, then -from coala_langserver.jsonrpc import ReadWriter, TCPReadWriter, JSONRPC2Connection +from pyls.jsonrpc import streams +from pyls.jsonrpc import endpoint +from pyls.jsonrpc import dispatchers -@given('the string') -def step_impl(context): - context.str = 'test-cases' +def issimilar(dicta, dictb): + """ + Return bool indicating if dicta is deeply similar to dictb. + """ + # slow but safe for deeper evaluation + return json.dumps(dicta) == json.dumps(dictb) -@when('I write it to ReadWriter') +@given('the message') def step_impl(context): - context.f = tempfile.TemporaryFile(mode='w+') - context.readWriter = ReadWriter(context.f, context.f) - context.readWriter.write(context.str) + context.message = { + 'simple': 'test', + } -@then('it should read from ReadWriter') +@when('I write it to JsonRpcStreamWriter') def step_impl(context): - context.f.seek(0) - assert context.readWriter.read(len(context.str)) is not '' - context.f.close() + context.f = tempfile.TemporaryFile(mode='w+b') + context.writer = streams.JsonRpcStreamWriter(context.f) + context.writer.write(context.message) -@then('it should readline from ReadWriter') +@then('it should read from JsonRpcStreamReader') def step_impl(context): context.f.seek(0) - assert context.readWriter.readline() is not '' - context.f.close() + context._passed = False + def consumer(message): + assert issimilar(context.message, message) + context._passed = True + context.writer.close() -@when('I write it to TCPReadWriter') -def step_impl(context): - context.f = tempfile.TemporaryFile() - context.readWriter = TCPReadWriter(context.f, context.f) - context.readWriter.write(context.str) + reader = streams.JsonRpcStreamReader(context.f) + reader.listen(consumer) + reader.close() - -@then('it should read from TCPReadWriter') -def step_impl(context): - context.f.seek(0) - assert context.readWriter.read(len(context.str)) is not '' - context.f.close() + if not context._passed: + assert False -@then('it should readline from TCPReadWriter') +@given('a notification type rpc request') def step_impl(context): - context.f.seek(0) - assert context.readWriter.readline() is not '' - context.f.close() + context.request = { + 'jsonrpc': '2.0', + 'method': 'math/add', + 'params': { + 'a': 1, + 'b': 2, + }, + } -@given('the JSONRPC2Connection instance') +@when('I send rpc request using JsonRpcStreamWriter') def step_impl(context): context.f = tempfile.TemporaryFile() - context.jsonConn = JSONRPC2Connection(conn=TCPReadWriter(context.f, context.f)) + context.writer = streams.JsonRpcStreamWriter(context.f) + context.writer.write(context.request) -@when('I write a request to the JSONRPC2Connection with id') +@then('it should invoke the notification consumer with args') def step_impl(context): - context.jsonConn.send_request('mockMethod', { - 'mock': 'mock' - }) + context.f.seek(0) + context._passed = False + class Example(dispatchers.MethodDispatcher): -@then('it should return the request from JSONRPC2Connection with id') -def step_impl(context): - context.f.seek(0) - assert context.jsonConn.read_message() is not None - context.f.close() + def m_math__add(self, a, b): + context.writer.close() + context._passed = True + epoint = endpoint.Endpoint(Example(), None) + reader = streams.JsonRpcStreamReader(context.f) + reader.listen(epoint.consume) + reader.close() -@when('I write a notification to the JSONRPC2Connection') -def step_impl(context): - context.jsonConn.send_notification('mockMethod', { - 'mock': 'mock' - }) + if not context._passed: + assert False -@then('it should return the notification from JSONRPC2Connection') +@given('a request type rpc request') +def step_impl(context): + context.request = { + 'jsonrpc': '2.0', + 'id': 2148, + 'method': 'math/add', + 'params': { + 'a': 1, + 'b': 2, + }, + } + + +@then('it should invoke consumer and return response') def step_impl(context): context.f.seek(0) - assert context.jsonConn.read_message() is not None - context.f.close() + context._passed = False + class Example(dispatchers.MethodDispatcher): -@when('I write a response to the JSONRPC2Connection') -def step_impl(context): - # BUG: when id = 0 - context.ID = 1 - context.jsonConn.write_response(context.ID, { - 'mock': 'mock' - }) + def m_math__add(self, a, b): + return a + b + def consumer(message): + assert message['result'] == sum(context.request['params'].values()) + context.writer.close() + context._passed = True -@then('it should return the response from JSONRPC2Connection') -def step_impl(context): - context.f.seek(0) - assert context.jsonConn.read_message(context.ID) is not None - context.f.close() + epoint = endpoint.Endpoint(Example(), consumer) + reader = streams.JsonRpcStreamReader(context.f) + reader.listen(epoint.consume) + reader.close() + + if not context._passed: + assert False diff --git a/tests/server.features/steps/langserver_steps.py b/tests/server.features/steps/langserver_steps.py index 565b913..ee4706e 100644 --- a/tests/server.features/steps/langserver_steps.py +++ b/tests/server.features/steps/langserver_steps.py @@ -4,19 +4,28 @@ # ---------------------------------------------------------------------------- # STEPS: # ---------------------------------------------------------------------------- +import io +import os +import sys +import time +import socket import tempfile +from threading import Thread + from behave import given, when, then -from coala_langserver.jsonrpc import TCPReadWriter -from coala_langserver.langserver import LangServer +from unittest import mock + +from pyls.jsonrpc import streams +from coala_langserver.langserver import LangServer, start_io_lang_server, main @given('the LangServer instance') def step_impl(context): context.f = tempfile.TemporaryFile() - context.langServer = LangServer(conn=TCPReadWriter(context.f, context.f)) + context.langServer = LangServer(context.f, context.f) -@when('I send a initialize request to the server') +@when('I send a initialize request with rootPath to the server') def step_impl(context): request = { 'method': 'initialize', @@ -27,16 +36,46 @@ def step_impl(context): 'id': 1, 'jsonrpc': '2.0' } - context.langServer.handle(1, request) + context.langServer._endpoint.consume(request) + + +@when('I send a initialize request with rootUri to the server') +def step_impl(context): + request = { + 'method': 'initialize', + 'params': { + 'rootUri': '/Users/mock-user/mock-dir', + 'capabilities': {}, + }, + 'id': 1, + 'jsonrpc': '2.0', + } + context.langServer._endpoint.consume(request) @then('it should return the response with textDocumentSync') def step_impl(context): context.f.seek(0) - response = context.langServer.read_message(1) - assert response is not None - assert response['result']['capabilities']['textDocumentSync'] is 1 - context.f.close() + context._passed = False + + def consumer(response): + assert response is not None + assert response['result']['capabilities']['textDocumentSync'] == 1 + context.f.close() + context._passed = True + + reader = streams.JsonRpcStreamReader(context.f) + reader.listen(consumer) + reader.close() + + if not context._passed: + assert False + + +@when('I invoke send_diagnostics message') +def step_impl(context): + context.langServer.send_diagnostics('/sample', []) + context._diagCount = 0 @when('I send a did_save request about a non-existed file to the server') @@ -48,16 +87,272 @@ def step_impl(context): 'uri': 'file:///Users/mock-user/non-exist.py' } }, + 'jsonrpc': '2.0', + } + context.langServer._endpoint.consume(request) + context._diagCount = 0 + + +@when('I send a did_save request about a existing file to the server') +def step_impl(context): + thisfile = os.path.realpath(__file__) + thisdir = os.path.dirname(thisfile) + parturi = os.path.join(thisdir, '../../resources', 'unqualified.py') + absparturi = os.path.abspath(parturi) + + request = { + 'method': 'textDocument/didSave', + 'params': { + 'textDocument': { + 'uri': 'file://{}'.format(absparturi), + }, + }, + 'jsonrpc': '2.0', + } + context.langServer._endpoint.consume(request) + context._diagCount = 4 + + +@when('I send a did_save request on a file with no coafile to server') +def step_impl(context): + somefile = tempfile.NamedTemporaryFile(delete=False) + somefilename = somefile.name + somefile.close() + + request = { + 'method': 'textDocument/didSave', + 'params': { + 'textDocument': { + 'uri': 'file://{}'.format(somefilename), + }, + }, 'jsonrpc': '2.0' } - context.langServer.handle(None, request) + context.langServer._endpoint.consume(request) + context._diagCount = 0 @then('it should send a publishDiagnostics request') def step_impl(context): context.f.seek(0) - response = context.langServer.read_message() - assert response is not None - assert response['method'] == 'textDocument/publishDiagnostics' - assert len(response['params']['diagnostics']) is 0 + context._passed = False + + def consumer(response): + assert response is not None + assert response['method'] == 'textDocument/publishDiagnostics' + assert len(response['params']['diagnostics']) is context._diagCount + + context.f.close() + context._passed = True + + reader = streams.JsonRpcStreamReader(context.f) + reader.listen(consumer) + reader.close() + + if not context._passed: + assert False + + +@when('I send a did_change request about a file to the server') +def step_impl(context): + thisfile = os.path.realpath(__file__) + thisdir = os.path.dirname(thisfile) + parturi = os.path.join(thisdir, '../../resources', 'unqualified.py') + + request = { + 'method': 'textDocument/didChange', + 'params': { + 'textDocument': { + 'uri': 'file://{}'.format(parturi), + }, + 'contentChanges': [ + { + 'text': 'def test():\n a = 1\n', + }, + ], + }, + 'jsonrpc': '2.0', + } + context.langServer._endpoint.consume(request) + + +@then('it should ignore the request') +def step_impl(context): + length = context.f.seek(0, os.SEEK_END) + assert length == 0 context.f.close() + + +@when('I send a shutdown request to the server') +def step_impl(context): + request = { + 'method': 'shutdown', + 'params': None, + 'id': 1, + 'jsonrpc': '2.0', + } + context.langServer._endpoint.consume(request) + + +@then('it should shutdown') +def step_impl(context): + context.f.seek(0) + context._passed = False + + def consumer(response): + assert response is not None + assert response['result'] is None + + context.f.close() + context._passed = True + + reader = streams.JsonRpcStreamReader(context.f) + reader.listen(consumer) + reader.close() + + assert context._passed + assert context.langServer._shutdown + + +def gen_alt_log(context, mode='tcp'): + if mode == 'tcp': + check = 'Serving LangServer on (0.0.0.0, 20801)\n' + elif mode == 'stdio': + check = 'Starting LangServer IO language server\n' + else: + assert False + + def alt_log(*args, **kargs): + result = io.StringIO() + print(*args, file=result, **kargs) + + value = result.getvalue() + if value == check: + context._server_alive = True + + return alt_log + + +@given('the server started in TCP mode') +def step_impl(context): + context._server_alive = False + host, port = ('0.0.0.0', 20801) + + with mock.patch('coala_langserver.langserver.log') as mock_log: + mock_log.side_effect = gen_alt_log(context) + + sys.argv = ['', '--mode', 'tcp', '--addr', str(port)] + context.thread = Thread(target=main) + context.thread.daemon = True + context.thread.start() + + for _ in range(20): + if context._server_alive: + break + else: + time.sleep(1) + else: + assert False + + context.sock = socket.create_connection( + address=(host, port), timeout=10) + context.f = context.sock.makefile('rwb') + + context.reader = streams.JsonRpcStreamReader(context.f) + context.writer = streams.JsonRpcStreamWriter(context.f) + + +@when('I send a initialize request via TCP stream') +def step_impl(context): + request = { + 'method': 'initialize', + 'params': { + 'rootUri': '/Users/mock-user/mock-dir', + 'capabilities': {}, + }, + 'id': 1, + 'jsonrpc': '2.0', + } + context.writer.write(request) + + +@then('it should return the response with textDocumentSync via TCP') +def step_impl(context): + context._passed = False + + def consumer(response): + assert response is not None + assert response['result']['capabilities']['textDocumentSync'] == 1 + context.f.close() + context._passed = True + + context.reader.listen(consumer) + context.reader.close() + context.sock.close() + + if not context._passed: + assert False + + +@given('I send a initialize request via stdio stream') +def step_impl(context): + context.f = tempfile.TemporaryFile() + context.writer = streams.JsonRpcStreamWriter(context.f) + + request = { + 'method': 'initialize', + 'params': { + 'rootUri': '/Users/mock-user/mock-dir', + 'capabilities': {}, + }, + 'id': 1, + 'jsonrpc': '2.0', + } + context.writer.write(request) + context.f.seek(0) + + +@when('the server is started in stdio mode') +def step_impl(context): + context._server_alive = False + context.o = tempfile.TemporaryFile() + + with mock.patch('coala_langserver.langserver.log') as mock_log: + mock_log.side_effect = gen_alt_log(context, 'stdio') + + context.thread = Thread(target=start_io_lang_server, args=( + LangServer, context.f, context.o)) + context.thread.daemon = True + context.thread.start() + + for _ in range(10): + if context._server_alive: + break + else: + time.sleep(1) + else: + assert False + + +@then('it should return the response with textDocumentSync via stdio') +def step_impl(context): + context._passed = False + + def consumer(response): + assert response is not None + assert response['result']['capabilities']['textDocumentSync'] == 1 + context.f.close() + context._passed = True + + last = -9999 + while context.o.tell() != last: + last = context.o.tell() + time.sleep(1) + + context.o.seek(0) + context.reader = streams.JsonRpcStreamReader(context.o) + context.reader.listen(consumer) + context.reader.close() + + if not context._passed: + assert False