diff --git a/.codecov.yml b/.codecov.yml index 9d955c5..889797c 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,4 +6,4 @@ coverage: project: default: enabled: true - target: 95% + target: 100% diff --git a/.coveragerc b/.coveragerc index 9e73f79..444e6f2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,7 @@ branch = True source = . omit = - coala-langserver.py + coalals/__main__.py tests/* [report] diff --git a/.gitignore b/.gitignore index f098d4d..7927b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -281,6 +281,14 @@ tags .idea/**/gradle.xml .idea/**/libraries +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + # CMake cmake-build-*/ diff --git a/.travis.yml b/.travis.yml index cde3b2b..87eaba0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,12 +26,6 @@ before_install: sleep 3; fi - - cd ./vscode-client - - npm install - - mkdir ./out - - npm run vscode:prepublish - - cd - > /dev/null - - printf '%s\n' "$(cat test-requirements.txt requirements.txt)" > requirements.txt @@ -50,12 +44,7 @@ script: # https://github.com/coala/coala-bears/issues/1037 - sed -i.bak '/bears = GitCommitBear/d' .coafile # Server side tests. - - coverage run $(which behave) ./tests/server.features - - coverage run -a -m unittest discover -s tests - # Frontend tests. -# - cd ./vscode-client -# - npm test -# - cd - > /dev/null + - py.test --cov-report term --cov=coalals notifications: email: false diff --git a/README.md b/README.md index c7afc71..4d1b49c 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,20 @@ -# coala-vs-code +# coala-ls -[![Build Status](https://travis-ci.org/coala/coala-vs-code.svg?branch=master)](https://travis-ci.org/coala/coala-vs-code) -[![codecov](https://codecov.io/gh/coala/coala-vs-code/branch/master/graph/badge.svg)](https://codecov.io/gh/coala/coala-vs-code) +[![Build Status](https://travis-ci.org/coala/coala-ls.svg?branch=master)](https://travis-ci.org/coala/coala-ls) +[![codecov](https://codecov.io/gh/coala/coala-ls/branch/master/graph/badge.svg)](https://codecov.io/gh/coala/coala-ls) -A visual studio code plugin working via [Language Server Protocol (LSP)](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md).Python versions 3.x is supported. +A coala language server based on [Language Server Protocol (LSP)](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md).Python versions 3.x is supported. ## Feature preview -![](./docs/images/demo.gif) +![coala-ls demo](./docs/images/demo.gif) +Watch full video on [YouTube](https://www.youtube.com/watch?v=MeybdlCB96U) ## Setting up your dev environment, coding, and debugging -You'll need python version 3.5 or greater, run `pip3 install -r requirements.txt` to install the requirements, and run `python3 langserver-python.py --mode=tcp --addr=2087` to start a local languager server listening at port 2087. - -Then you should update the `./vscode-client/src/extension.ts` to make client in TCP mode. - -```diff -export function activate(context: ExtensionContext) { -- context.subscriptions.push(startLangServer -- (require("path").resolve(__dirname, '../coala-langserver.sh'), ["python"])); -+ context.subscriptions.push(startLangServerTCP(2087, ["python"])); - console.log("coala language server is running."); -} -``` - -To try it in [Visual Studio Code](https://code.visualstudio.com), open ./vscode-client in VS Code and turn to debug view, launch the extension. - -## Known bugs - -* [Language server restarts when `didSave` requests come](https://github.com/coala/coala-vs-code/issues/7) +You'll need python version 3.5 or greater, run `pip3 install -r requirements.txt` to install the requirements, and run `python3 -m coalals --mode=tcp --addr=2087` to start a local language server listening at port 2087. Currently `stdio` mode is also supported and can be used by invoking coalals with `--mode=stdio`. ## Reference * [python-langserver](https://github.com/sourcegraph/python-langserver) +* [python-language-server](http://github.com/palantir/python-language-server) diff --git a/coala-langserver.py b/coala-langserver.py deleted file mode 100644 index 7e77ec6..0000000 --- a/coala-langserver.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/local/bin/python3 - -import traceback -from coala_langserver import langserver - -while True: - try: - langserver.main() - except Exception as e: - tb = traceback.format_exc() - print('FATAL ERROR: {} {}'.format(e, tb)) diff --git a/coala-langserver.sh b/coala-ls.sh similarity index 62% rename from coala-langserver.sh rename to coala-ls.sh index a88d46d..3e05ab7 100755 --- a/coala-langserver.sh +++ b/coala-ls.sh @@ -1,5 +1,5 @@ #!/bin/sh cd "$(dirname "$0")" > /dev/null -exec python3 ./coala-langserver.py +exec python3 -m coalals cd - /dev/null diff --git a/coala_langserver/coalashim.py b/coala_langserver/coalashim.py deleted file mode 100644 index f04f2a3..0000000 --- a/coala_langserver/coalashim.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys -import os -import io -from contextlib import redirect_stdout - -from coalib import coala - -from .log import log - - -def run_coala_with_specific_file(working_dir, file): - sys.argv = ['', '--json', '--find-config', '--limit-files', file] - if working_dir is None: - working_dir = '.' - os.chdir(working_dir) - f = io.StringIO() - with redirect_stdout(f): - retval = coala.main() - output = None - if retval == 1: - output = f.getvalue() - if output: - log('Output =', output) - else: - log('No results for the file') - elif retval == 0: - log('No issues found') - else: - log('Exited with:', retval) - return output diff --git a/coala_langserver/diagnostic.py b/coala_langserver/diagnostic.py deleted file mode 100644 index 1e6150e..0000000 --- a/coala_langserver/diagnostic.py +++ /dev/null @@ -1,56 +0,0 @@ -import json - - -def output_to_diagnostics(output): - """ - Turn output to diagnstics. - """ - if output is None: - return None - output_json = json.loads(output)['results'] - res = [] - for key, problems in output_json.items(): - section = key - for problem in problems: - """ - Transform RESULT_SEVERITY of coala into DiagnosticSeverity of LSP - coala: INFO = 0, NORMAL = 1, MAJOR = 2 - LSP: Error = 1, Warning = 2, Information = 3, Hint = 4 - """ - severity = 3 - problem['severity'] - message = problem['message'] - origin = problem['origin'] - real_message = '[{}] {}: {}'.format(section, origin, message) - for code in problem['affected_code']: - """ - Line position and character offset should be zero-based - according to LSP, but row and column positions of coala - are None or one-based number. - coala uses None for convenience. None for column means the - whole line while None for line means the whole file. - """ - def convert_offset(x): return x - 1 if x else x - start_line = convert_offset(code['start']['line']) - start_char = convert_offset(code['start']['column']) - end_line = convert_offset(code['end']['line']) - end_char = convert_offset(code['end']['column']) - if start_char is None or end_char is None: - start_char = 0 - end_line = start_line + 1 - end_char = 0 - res.append({ - 'severity': severity, - 'range': { - 'start': { - 'line': start_line, - 'character': start_char - }, - 'end': { - 'line': end_line, - 'character': end_char - } - }, - 'source': 'coala', - 'message': real_message - }) - return res diff --git a/coala_langserver/langserver.py b/coala_langserver/langserver.py deleted file mode 100644 index 7ea9bd4..0000000 --- a/coala_langserver/langserver.py +++ /dev/null @@ -1,156 +0,0 @@ -import sys -import argparse -import socketserver -import traceback - -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 _StreamHandlerWrapper(socketserver.StreamRequestHandler, object): - """ - A wrapper class that is used to construct a custom handler class. - """ - - delegate = None - - def setup(self): - super(_StreamHandlerWrapper, self).setup() - self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile) - - def handle(self): - self.delegate.start() - - -class LangServer(MethodDispatcher): - """ - Language server for coala base on JSON RPC. - """ - - 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 start(self): - self._jsonrpc_stream_reader.listen(self._endpoint.consume) - - def m_initialize(self, **params): - """ - Serve for the initialization request. - """ - # 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']) - return { - 'capabilities': { - 'textDocumentSync': 1 - } - } - - def m_text_document__did_save(self, **params): - """ - Serve for did_change request. - """ - uri = params['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.""' - # params = request['params'] - # uri = params['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) - # return None - # - # def serve_did_change_watched_files(self, request): - # '""Serve for thr workspace/didChangeWatchedFiles request.""' - # changes = request['changes'] - # for fileEvent in changes: - # uri = fileEvent['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 send_diagnostics(self, path, diagnostics): - _diagnostics = [] - if diagnostics is not None: - _diagnostics = diagnostics - params = { - 'uri': 'file://{0}'.format(path), - 'diagnostics': _diagnostics, - } - 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(): - parser = argparse.ArgumentParser(description='') - parser.add_argument('--mode', default='stdio', - help='communication (stdio|tcp)') - parser.add_argument('--addr', default=2087, - help='server listen (tcp)', type=int) - - args = parser.parse_args() - - if args.mode == 'stdio': - start_io_lang_server(LangServer, sys.stdin.buffer, sys.stdout.buffer) - elif args.mode == 'tcp': - host, addr = '0.0.0.0', args.addr - start_tcp_lang_server(LangServer, host, addr) - - -if __name__ == '__main__': - main() diff --git a/coala_langserver/log.py b/coala_langserver/log.py deleted file mode 100644 index 924f0a5..0000000 --- a/coala_langserver/log.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys - - -def log(*args, **kwargs): - print(*args, file=sys.stderr, **kwargs) - sys.stderr.flush() diff --git a/coala_langserver/uri.py b/coala_langserver/uri.py deleted file mode 100644 index 977135e..0000000 --- a/coala_langserver/uri.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - - -def path_from_uri(uri): - """ - Get the path from JSON RPC initialization request. - """ - if not uri.startswith('file://'): - return uri - _, path = uri.split('file://', 1) - return path - - -def dir_from_uri(uri): - """ - Get the directory name from the path. - """ - return os.path.dirname(path_from_uri(uri)) diff --git a/coala_langserver/__init__.py b/coalals/__init__.py similarity index 100% rename from coala_langserver/__init__.py rename to coalals/__init__.py diff --git a/coalals/__main__.py b/coalals/__main__.py new file mode 100644 index 0000000..8b9a74a --- /dev/null +++ b/coalals/__main__.py @@ -0,0 +1,9 @@ +from sys import argv, stdout +from .main import main +from .utils.log import configure_logger + +import logging +configure_logger() + +if __name__ == '__main__': + main() diff --git a/coalals/concurrency.py b/coalals/concurrency.py new file mode 100644 index 0000000..e980e54 --- /dev/null +++ b/coalals/concurrency.py @@ -0,0 +1,219 @@ +from concurrent.futures import ProcessPoolExecutor + +from coala_utils.decorators import enforce_signature +from .utils.wrappers import func_wrapper + +import logging +logger = logging.getLogger(__name__) + + +class JobTracker: + """ + JobTracker helps to keep track of running jobs and + function as an advanced counter. It is lazy and will + only update its internal state when requested for + information about it. JobTracker instances should always + live on the main thread. Hence should never go out of sync. + """ + + @staticmethod + def kill_job(job): + """ + Abstract method that should kill a job instance. This + makes JobTracker independent of the job type and its + state tracking mechanism. + + :param job: + Job to be killed. + :return: + A boolean value representing the result of a killing. + True if preemption was successful or false. + """ + # job is a Future, cancel() only requests and + # does not currently guarantee cancellation. + # TODO Add a reliable cancellation mechanism + # (https://kutt.it/MyF1AZ) + return job.cancel() + + @staticmethod + def is_active(job): + """ + Abstract method to find the running state of a given job. + + :param job: + The job to find the running state of. + :return: + The running state of a job. + """ + # A typical job can be any convenient object + # by default it is considered as a future. + return not job.done() + + def __init__(self, max_jobs=1): + """ + :param max_jobs: + The maximum number of concurrent jobs to permit. + """ + if max_jobs < 1: + raise ValueError() + + self._max_jobs = max_jobs + # actually contains the Future + # instances from scheduled jobs + self._jobs = [] + + def refresh_jobs(self): + """ + Refresh the internal state of the tracker. + """ + self._jobs = list(filter( + lambda job: JobTracker.is_active(job), self._jobs)) + + def __len__(self): + """ + :return: + Returns the number of active jobs after + refreshing the job list. + """ + self.refresh_jobs() + return len(self._jobs) + + def has_slots(self): + """ + :return: + Returns a boolean value representing if there + are free slots available to add more jobs. + """ + self.refresh_jobs() + return len(self._jobs) < self._max_jobs + + def force_free_slots(self): + """ + Attempt to empty job slots by trying to kill + older processes. All the excessive jobs + that are above limit are also killed off. + force_free_slots only attempts to make slots, + it does not assure the availability of a slot. + + :return: + A boolean value indicating if a slot was + freed for use. + """ + if self.has_slots(): + return True + + count = 1 + len(self._jobs) - self._max_jobs + for job in self._jobs[:count]: + if not JobTracker.kill_job(job): + return False + + return True + + def prepare_slot(self, force=False): + """ + Jobtracker is not responsible to schedule, + monitor or manage the jobs in any manner, although + it can send a pre-empt signal to running jobs. + It only keeps a running count and indicates if + resources with respect to max allocation are + available to accept incoming request. Hence the + prepare_slot() should be called before actual + job allocation happens to check for resources. + + :param force: + Boolean value indicating if force freeing of + slots should be used if no slots are empty. + :return: + Returns a boolean value indicating if a job + should be scheduled and added. + """ + if not self.has_slots(): + if force is True: + return self.force_free_slots() + else: + return False + + return True + + def add(self, job): + """ + Add a job to the list of jobs but does not + check for slots or the active status as It is + assumed that proper checks have been done + before scheduling. Hence before calling this + use prepare_slot(). + + :param job: + The job instance. + """ + # Evaluates lazily and does not complain about + # the overflow. prepare_slot() should be used. + + # TODO self._jobs should be a dict and should + # map from filename to job object. That way we + # can prevent multiple requests fighting to run + # on the same source file. + # take caution before adding + self._jobs.append(job) + + +class TrackedProcessPool: + """ + Abstracts the integration of ProcessPoolExec, + JobTracker and func_wrapper. + """ + + def __init__(self, max_jobs=1, max_workers=1): + """ + :param max_jobs: + The maximum number of jobs to allow concurrently, + the parameter is passed to JobTracker as is. + :param max_workers: + The number of processes to use with the pool. + """ + self._job_tracker = JobTracker(max_jobs) + self._process_pool = ProcessPoolExecutor(max_workers=max_workers) + + @enforce_signature + def exec_func(self, + func, + params: (list, tuple) = [], + kparams: dict = {}, + force=False): + """ + Handle preparing slot on the tracker, scheduling the + job on the pool and adding the job to the tracker. + + :param func: + The callable that should be invoked on a new process. + :param params: + A list of non-keyword arguments to be passed to the + callable for execution. + :param kparams: + A dict of keyword arguments to be passed to the callable + for execution. + :param force: + The force flag to use while preparing a slot. + :return: + Returns a job added to the tracker or a False. + """ + # TODO Add a meta information carrying mechanism + # so the callbacks can get some context. + if not self._job_tracker.prepare_slot(force): + return False + + future = self._process_pool.submit( + func_wrapper, func, *params, **kparams) + + self._job_tracker.add(future) + return future + + def shutdown(self, wait=True): + """ + Shutdown the process pool and all associated resources. + + :param wait: + Boolean indicating if the pool should wait until all + the processes close. + """ + self._process_pool.shutdown(wait) diff --git a/coalals/interface.py b/coalals/interface.py new file mode 100644 index 0000000..66d4dc8 --- /dev/null +++ b/coalals/interface.py @@ -0,0 +1,103 @@ +import sys +from os import chdir +from json import dumps +from io import StringIO +from contextlib import redirect_stdout + +from coalib import coala +from .concurrency import TrackedProcessPool +from .utils.log import configure_logger, reset_logger + +import logging +logger = logging.getLogger(__name__) + + +class coalaWrapper: + """ + Provide an abstract interaction layer to coala + to perform the actual analysis. + """ + + def __init__(self, max_jobs=1, max_workers=1): + """ + coalaWrapper uses a tracked process pool to run + concurrent cycles of code analysis. + + :param max_jobs: + The maximum number of concurrent jobs to permit. + :param max_workers: + The number of threads to maintain in process pool. + """ + self._tracked_pool = TrackedProcessPool( + max_jobs=max_jobs, max_workers=max_workers) + + @staticmethod + def _run_coala(): + stream = StringIO() + with redirect_stdout(stream): + return (stream, coala.main()) + + @staticmethod + def _get_op_from_coala(stream, retval): + output = None + if retval == 1: + output = stream.getvalue() + if output: + logger.debug('Output: %s', output) + else: + logger.debug('No results for the file') + elif retval == 0: + logger.debug('No issues found') + else: + logger.debug('Exited with: %s', retval) + + return output or dumps({'results': {}}) + + @staticmethod + def analyse_file(file_proxy): + """ + Invoke and performs the actual coala analysis. + + :param file_proxy: + The proxy of the file coala analysis is to be + performed on. + :return: + A valid json string containing results. + """ + sys.argv = ['', '--json', '--find-config', + '--limit-files', file_proxy.filename] + + workspace = file_proxy.workspace + if workspace is None: + workspace = '.' + chdir(workspace) + + stream, retval = coalaWrapper._run_coala() + return coalaWrapper._get_op_from_coala(stream, retval) + + def p_analyse_file(self, file_proxy, force=False, **kargs): + """ + It is a concurrent version of coalaWrapper.analyse_file(). + force indicates whether the request should pre-empt running + cycles. + + :param file_proxy: + The proxy of the file coala analysis is to be + performed on. + :param force: + The force flag to use while preparing a slot using + JobTracker. + """ + result = self._tracked_pool.exec_func( + coalaWrapper.analyse_file, (file_proxy,), kargs, force=force) + + if result is False: + logging.debug('Failed p_analysis_file() on %s', file_proxy) + + return result + + def close(self): + """ + Perform resource clean up. + """ + self._tracked_pool.shutdown(True) diff --git a/coalals/langserver.py b/coalals/langserver.py new file mode 100644 index 0000000..e5df0aa --- /dev/null +++ b/coalals/langserver.py @@ -0,0 +1,334 @@ +import sys + +from jsonrpc.endpoint import Endpoint +from jsonrpc.dispatchers import MethodDispatcher +from jsonrpc.streams import JsonRpcStreamWriter, JsonRpcStreamReader +from .results.diagnostics import Diagnostics +from .results.fixes import coalaPatch, TextEdits +from .interface import coalaWrapper +from .utils.files import UriUtils, FileProxy, FileProxyMap + +import logging +logger = logging.getLogger(__name__) + + +class LangServer(MethodDispatcher): + """ + LangServer class handles various kinds of + notifications and requests. + """ + + _config_max_jobs = 2 + _config_max_workers = 2 + + @classmethod + def set_concurrency_params(cls, max_jobs=2, max_workers=2): + """ + Allow for setting concurrency parameters to be used with + tracker and process pool. The settings are sticky and will + also be used with later instances of language server unless + explicitly reset. + + :param max_jobs: + The max_jobs parameter to be used with coalaWrapper. + Indicates the maximum number of concurrent jobs to permit. + :param max_workers: + The max_workers parameter to be used with coalaWrapper. + This indicates the maximum number of processes to maintain + in the pool. max_workers >= max_jobs. + :return: + True if parameters were set successfully else False. + """ + if max_jobs < 1 or max_workers < max_jobs: + return False + + cls._config_max_jobs = max_jobs + cls._config_max_workers = max_workers + + return True + + def __init__(self, rx, tx): + """ + :param rx: + An input stream. + :param tx: + An output stream. + """ + self.root_path = None + self._dispatchers = [] + self._shutdown = False + + self._jsonrpc_stream_reader = JsonRpcStreamReader(rx) + self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx) + self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write) + self._proxy_map = FileProxyMap() + + # max_jobs is strict and a new job can only be submitted by pre-empting + # an older job or submitting later. No queuing is supported. + self._coala = coalaWrapper(max_jobs=self._config_max_jobs, + max_workers=self._config_max_workers) + + self._capabilities = { + 'capabilities': { + 'textDocumentSync': 1, + 'documentFormattingProvider': 1, + } + } + + def start(self): # pragma: no cover + """ + Start listening on the stream and dispatches events to + callbacks. + """ + self._jsonrpc_stream_reader.listen(self._endpoint.consume) + + def m_initialize(self, **params): + """ + initialize request is sent from a client to server and it + expects the list of capabilities as a response. + + :param params: + Parameters passed to the callback method from a dispatcher, + follows InitializeParams structure according to LSP. + """ + logger.info('Serving initialize request') + + # Notice that the root_path could be None. + if 'rootUri' in params: + self.root_path = UriUtils.path_from_uri(params['rootUri']) + elif 'rootPath' in params: # pragma: no cover + self.root_path = UriUtils.path_from_uri(params['rootPath']) + + return self._capabilities + + def local_p_analyse_file(self, fileproxy, force=False): + """ + Schedule concurrent analysis cycle and handles + the resolved future. The diagnostics are published + from the callback. + + :param fileproxy: + The proxy of the file to perform coala analysis on. + :param force: + The force flag to use when perparing a slot. + """ + filename = fileproxy.filename + logger.info('Running analysis on %s', filename) + + result = self._coala.p_analyse_file(fileproxy, force=force) + if result is False: + logging.info('Failed analysis on %s', fileproxy.filename) + return + + # Always called on this thread + def _handle_diagnostics(future): + # TODO Find a better method to deal with + # failure cases. + if future.cancelled(): + logger.debug('Cancelled diagnostics on %s', filename) + return + + if future.exception(): + logger.debug('Exception during analysis: %s', + future.exception()) + return + + coala_json = future.result() + diagnostics = Diagnostics.from_coala_json(coala_json) + + self.send_diagnostics(filename, diagnostics) + + result.add_done_callback(_handle_diagnostics) + + def _text_document_to_name(self, text_document): + uri = text_document['uri'] + return UriUtils.path_from_uri(uri) + + def m_text_document__did_open(self, **params): + """ + textDocument/didOpen request is dispatched by the client + to the server whenever a new file is opened in the editor. + LangServer here builds a new FileProxy object and replaces + or adds it to the map. It also has access to file content + via the params. It also performs initial coala analysis on + the file and published the diagnostics. + + :param params: + The params passed by the client. The structure remains + consistent with the LSP protocol definition. + """ + logger.info('Reacting to didOpen notification') + + text_document = params['textDocument'] + filename = self._text_document_to_name(text_document) + + # didOpen can create the file proxy from + # the params passed to it + contents = text_document['text'] + version = text_document['version'] + + proxy = FileProxy(filename, workspace=self.root_path) + proxy.replace(contents, version) + self._proxy_map.add(proxy, replace=True) + + self.local_p_analyse_file(proxy, True) + + def m_text_document__did_save(self, **params): + """ + textDocument/didSave is dispatched by the client to the + server on saving a file. LangServer performs a coala + analysis of the file if it already exists in its file + map. + + :param params: + The parameters passed during the notification. + """ + logger.info('Reacting to didSave notification') + + text_document = params['textDocument'] + filename = self._text_document_to_name(text_document) + + # If the file does not exist in the proxy map + # discard the request, it should didOpen first + proxy = self._proxy_map.get(filename) + if proxy is None: + return + + text_document = params['textDocument'] + self.local_p_analyse_file(proxy, True) + + def m_text_document__did_change(self, **params): + """ + textDocument/didChange is a notification from client to + server when the text document is changed by adding or + removing content. This callback current updates the + associated proxy's content. + + :param params: + The parameters passed during the notification. + """ + logger.info('Reacting to didChange notification') + text_document = params['textDocument'] + content_changes = params['contentChanges'] + + filename = self._text_document_to_name(text_document) + proxy = self._proxy_map.get(filename) + + # Send a didOpen first + if proxy is None: + return + + # update if range and rangeLength are present + # in the contentChanges dict else replace + if ('range' not in content_changes[0] and + 'rangeLength' not in content_changes[0]): + + version = text_document['version'] + content = content_changes[0]['text'] + + if proxy.replace(content, version): + logger.info( + 'Replaced proxy content to version %s', version) + + # FIXME Add a way to handle the range updates mechanism + # i.e resolve diffs and construct the text, the diff + # handling mechanism should be handled in FileProxy's + # update method + + def m_text_document__formatting(self, **params): + """ + textDocument/formatting is a request. A formatting + request is raised from the editor. The server should + intend to fix all the indentation and spacing like + issues. + + :param params: + The parameters passed during the request. + """ + logger.info('Responding to formatting request') + + text_document = params['textDocument'] + filename = self._text_document_to_name(text_document) + + # If the file does not exist in the proxy map + # discard the request, it should didOpen first + proxy = self._proxy_map.get(filename) + if proxy is None: + return + + def _internal_formatter(): + result = self._coala.p_analyse_file(proxy, force=False) + if result is False: # pragma: no cover + logging.info('Failed analysis on %s', proxy.filename) + return + + # wait for returns + coala_json = result.result() + fixes = Diagnostics.from_coala_json(coala_json) + + # send diagnostic warnings found during analysis + self.send_diagnostics(proxy.filename, fixes) + + text_edits = fixes.fixes_to_text_edits(proxy) + return list(text_edits.get()) + + return _internal_formatter + + def m_text_document__did_close(self, **params): + """ + textDocument/didClose is dispatched by the client to the + server when a file is closed in the text editor. This + callback updates the state of the proxy map. + + :param params: + The parameters passed during the notification. + """ + logger.info('Reacting to didClose notification') + + text_document = params['textDocument'] + filename = self._text_document_to_name(text_document) + + # just remove the proxy object + self._proxy_map.remove(filename) + + # TODO Add a mechanism to send a pre-empt signal to running + # analysis cycles on the file being closed. + + def m_shutdown(self, **params): + """ + shutdown request is sent from client to the server. + """ + self._shutdown = True + + def m_exit(self, **params): + """ + exit notification expects that server will close while + freeing all its resources. + """ + logger.info('Reacting to exit notification') + + self._coala.close() + sys.exit(not self._shutdown) + + def send_diagnostics(self, path, diagnostics): + """ + Dispatche diagnostic messages to the editor. + + :param path: + The path of the file corresponding to the diagnostic + messages as a string. + :param diagnostics: + An instance of Diagnostics class with holding the + associated diagnostic messages. + """ + warnings = diagnostics.warnings() + logger.info('Publishing %s diagnostic messages', len(warnings)) + + params = { + 'uri': UriUtils.file_to_uri(path), + 'diagnostics': warnings, + } + + self._endpoint.notify( + 'textDocument/publishDiagnostics', + params=params) diff --git a/coalals/main.py b/coalals/main.py new file mode 100644 index 0000000..9293e7b --- /dev/null +++ b/coalals/main.py @@ -0,0 +1,94 @@ +import sys +from socketserver import TCPServer +from argparse import ArgumentParser + +from coala_utils.decorators import enforce_signature +from .utils.wrappers import StreamHandlerWrapper +from .langserver import LangServer + +import logging +logger = logging.getLogger(__name__) + + +@enforce_signature +def start_tcp_lang_server(handler_class: LangServer, bind_addr, port): + """ + Start a coala language server in TCP mode. Exits on exception. + + :param LangServer: + The handles class that will be wrapped on the StreamHandler and + invoked by TCP Server. + :param bind_addr: + A string representing the address to bind the TCP Server to. + :param port: + The port address to bind the server to. + """ + wrapper_class = type( + handler_class.__name__ + 'Handler', + (StreamHandlerWrapper,), + {'DELEGATE_CLASS': handler_class, }, + ) + + try: + server = TCPServer((bind_addr, port), wrapper_class) + except Exception as e: + logger.fatal('Fatal Exception: %s', e) + sys.exit(1) + + logger.info('Serving %s on (%s, %s)', + handler_class.__name__, bind_addr, port) + + try: + server.serve_forever() + except KeyboardInterrupt: + sys.exit('Killed by keyboard interrupt') + finally: + logger.info('Shutting down') + server.server_close() + + +@enforce_signature +def start_io_lang_server(handler_class: LangServer, rstream, wstream): + """ + Start a coala Language Server in stdio mode. + + :param handler_class: + The class capable of stream processing and representing + the Language Server. + :param rstream: + An input stream. + :param wstream: + An output stream. + """ + logger.info('Starting %s IO language server', handler_class.__name__) + server = handler_class(rstream, wstream) + server.start() + + +def main(): + """ + Main entry point into coala-ls. Parses arguments and starts + the server in respective mode. + """ + parser = ArgumentParser(description='') + parser.add_argument('--mode', default='stdio', + help='communication (stdio|tcp)') + parser.add_argument('--addr', default=2087, + help='server listen (tcp)', type=int) + parser.add_argument('--max-jobs', default=2, + help='maximum number of concurrent jobs', type=int) + parser.add_argument('--max-workers', default=2, + help='maximum number of processes', type=int) + args = parser.parse_args() + + # Since here LangServer only ever requires one instance + if not LangServer.set_concurrency_params(args.max_jobs, + args.max_workers): + logger.fatal('Invalid concurrency parameters') + sys.exit(1) + + if args.mode == 'stdio': + start_io_lang_server(LangServer, sys.stdin.buffer, sys.stdout.buffer) + elif args.mode == 'tcp': + host, addr = '0.0.0.0', args.addr + start_tcp_lang_server(LangServer, host, addr) diff --git a/coalals/results/diagnostics.py b/coalals/results/diagnostics.py new file mode 100644 index 0000000..09ae846 --- /dev/null +++ b/coalals/results/diagnostics.py @@ -0,0 +1,153 @@ +from json import loads +from coalals.results.fixes import (coalaPatch, + TextEdit, + TextEdits) + +import logging +logger = logging.getLogger(__name__) + + +class Diagnostics: + """ + Handle the diagnostics format transformation + and processing. + """ + + @classmethod + def from_coala_json(cls, json_op): + """ + Transform coala json output into valid + diagnostic messages following LSP protocol + structure. + + :param json_op: + coala json output as string. + :return: + Instance of Diagnostics class. + """ + coala_op = loads(json_op)['results'] + warnings, fixes = [], [] + + def convert_offset(x): + return x - 1 if x else x + + for section, coala_warnings in coala_op.items(): + for warning in coala_warnings: + """ + Transform RESULT_SEVERITY of coala to DiagnosticSeverity of LSP + coala: INFO = 0, NORMAL = 1, MAJOR = 2 + LSP: Error = 1, Warning = 2, Information = 3, Hint = 4 + """ + severity = 3 - warning['severity'] + message = warning['message'] + origin = warning['origin'] + full_message = '[{}] {}: {}'.format(section, origin, message) + + # TODO Handle results for multiple files + for code in warning['affected_code']: + start_line = convert_offset(code['start']['line']) + start_char = convert_offset(code['start']['column']) + end_line = convert_offset(code['end']['line']) + end_char = convert_offset(code['end']['column']) + + if start_char is None or end_char is None: + start_char, end_char = 0, 0 + end_line = start_line + 1 + + warnings.append({ + 'severity': severity, + 'range': { + 'start': { + 'line': start_line, + 'character': start_char, + }, + 'end': { + 'line': end_line, + 'character': end_char, + }, + }, + 'source': 'coala', + 'message': full_message, + }) + + # TODO Handle results for multiple files + # and also figure out a way to resolve + # overlapping patches. + + if warning['diffs'] is not None: + for file, diff in warning['diffs'].items(): + fixes.append((file, coalaPatch(diff))) + + logger.debug(warnings) + return cls(warnings, fixes=fixes) + + def __init__(self, warnings=[], fixes=[]): + """ + :param warnings: + A list of initial warnings to initialize + instance with. + :param fixes: + A list of initial code fixes to initialize + instance with. + """ + self._warnings = warnings + self._fixes = fixes + + def _filter_fixes(self, fixes): + """ + Filter and sort the fixes in some way so + the overlapping patch issues can be reduced. + + :param fixes: + The list of fixes, instances of coalaPatch. + """ + return reversed(fixes) + + def fixes_to_text_edits(self, proxy): + """ + Apply all the possible changes to the file, + then creates massive diff and converts it into + TextEdits compatible with Language Server. + + :param proxy: + The proxy of the file that needs to be worked + upon. + """ + # TODO Update to use the in-memory copy of + # the file if and when coalaWrapper is + # updated to use it too. + old = content = proxy.get_disk_contents() + sel_file = proxy.filename + + passed, failed = 0, 0 + for ths_file, fix in self._filter_fixes(self._fixes): + if sel_file is None or ths_file == sel_file: + try: + content = fix.apply(content) + passed += 1 + except AssertionError: + failed += 1 + continue + + logger.info('Applied %s patches on %s', + passed, sel_file or ths_file) + + logger.info('Skipped %s patches on %s', + failed, sel_file or ths_file) + + full_repl = TextEdit.replace_all(old, content) + return TextEdits([full_repl]) + + def warnings(self): + """ + :return: + Returns a list of warnings. + """ + return self._warnings + + def fixes(self): + """ + :return: + Returns a list of fixes. + """ + return self._fixes diff --git a/coalals/results/fixes.py b/coalals/results/fixes.py new file mode 100644 index 0000000..53dbc65 --- /dev/null +++ b/coalals/results/fixes.py @@ -0,0 +1,164 @@ +import whatthepatch as wp + + +class coalaPatch: + """ + coalaPatch processes the diff fixes + generated by coala core. It provides + an abstract way to use those patches. + """ + + @staticmethod + def join_parts(parts): + """ + Join lines to form text. + + :param parts: + The array of lines to join. + :return: + The joined text. + """ + # TODO Support joining on all platforms. + # i.e support \r, \n, \r\n. + return '\n'.join(list(parts) + ['']) + + def __init__(self, raw_patch): + """ + :param raw_patch: + The patch as text. + """ + self._raw = raw_patch + + def wp_parsed(self): + """ + :return: + Returns a whatthepatch parsed patch. + """ + return wp.parse_patch(self._raw) + + def apply(self, content, parsed_patch=None): + """ + Apply the wp parsed patch on content. + + :param content: + The original content of the file. + :param parsed_patch: + The wp parsed patch object. + :return: + The resulting file contents. + """ + if parsed_patch is None: + parsed_patch = self.wp_parsed() + + # assumes all the individual diffs can + # work incrementally over the same text. + # diffs from coala core support this. + for diff in parsed_patch: + content = coalaPatch.apply_diff(diff, content) + content = coalaPatch.join_parts(content) + + return content + + @staticmethod + def apply_diff(diff, content): + """ + :param content: + The original content of the file. + :return: + The resulting file contents. + """ + return wp.apply_diff(diff, content, False) + + +class TextEdit: + """ + TextEdit is an Language Server protocol + entity that represents a diff to be + performed on the contents of the file in + the editor. + """ + + @staticmethod + def replace_all(old, new): + """ + Change the contents in the editor + by replacing all the content. + + :param old: + The content in the editor. + :param new: + The content to be replaced with + in the editor. + :return: + Returns the TextEdit instance. + """ + old_lines = old.splitlines() + old_lines_len = len(old_lines) + old_end_char = len(old_lines[-1]) + + replace_range = { + 'start': { + 'line': 0, + 'character': 0, + }, + + 'end': { + 'line': old_lines_len, + 'character': old_end_char, + } + } + + return TextEdit(replace_range, new) + + def __init__(self, sel_range, new_text): + """ + :param sel_range: + The range of the text to modify. + :param new_text: + The new text to change the selected + range with. + """ + self._text_edit = { + 'range': sel_range, + 'newText': new_text, + } + + def get(self): + """ + :return: + Returns the raw TextEdit map. + """ + return self._text_edit + + +class TextEdits: + """ + Equivalent to TextEdit[] of the Language + Server protocol. It handles a collection + of TextEdits. + """ + + def __init__(self, edits=[]): + """ + :param edits: + The collection of edits. They + should be instances of TextEdit. + """ + self._edits = edits + + def add(self, edit): + """ + Add a TextEdit instance to the collection. + + :param edit: + The TextEdit instance to add. + """ + self._edits.append(edit) + + def get(self): + """ + :return: + Returns the list of raw TextEdits ready + to be jsonified. + """ + return map(lambda l: l.get(), self._edits) diff --git a/coalals/utils/__init__.py b/coalals/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coalals/utils/files.py b/coalals/utils/files.py new file mode 100644 index 0000000..3d2a130 --- /dev/null +++ b/coalals/utils/files.py @@ -0,0 +1,314 @@ +from pathlib import Path +from os.path import sep, isabs, dirname + +import logging +logger = logging.getLogger(__name__) + + +class UriUtils: + """ + UriUtils helps in performing various transformations + to file paths and URIs. It works independently from + the operating system. + """ + + @staticmethod + def path_from_uri(uri): + """ + :param uri: + The URI to decode path from. This method + fallsback and considers a invalid URI as + a valid path in itself. + :return: + Returns a string path encoded in the URI. + """ + if not uri.startswith('file://'): + return uri + + _, path = uri.split('file://', 1) + return path + + @classmethod + def dir_from_uri(cls, uri): + """ + Find and returns the parent directory of the + path encoded in the URI. + + :param uri: + The subject URI string containing the path + to find the parent of. + :return: + A string path to the parent directory. + """ + return dirname(cls.path_from_uri(uri)) + + @staticmethod + def file_to_uri(filename): + """ + Transform a given file name into a URI. It + works independent of the file name format. + + :param filename: + The path of the file to transform into URI. + :return: + Returns transformed URI as a string. + """ + return Path(filename).as_uri() + + +class FileProxy: + """ + coala requires the files to be flushed to perform + analysis on. This provides an alternative by providing + an always updated proxy of the said file by watching + for events from the client such as didChange. + """ + + @classmethod + def from_name(cls, file, workspace): + """ + Construct a FileProxy instance from an existing + file on the drive. + + :param file: + The name of the file to be represented by + the proxy instance. + :param workspace: + The workspace the file belongs to. This can + be none representing that the the directory + server is currently serving from is the workspace. + :return: + Returns a FileProxy instance of the file with + the content synced from a disk copy. + """ + with open(file, 'r') as reader: + return cls(file, workspace, reader.read()) + + def __init__(self, filename, workspace=None, contents=''): + """ + Initialize the FileProxy instance with the passed + parameters. A FileProxy instance always starts at + a fresh state with a negative version indicating + that no updating operation has been performed on it. + + :param filename: + The name of the file to create a FileProxy of. + :param workspace: + The workspace this file belongs to. Can be None. + :param contents: + The contents of the file to initialize the + instance with. Integrity of the content or the + sync state is never checked during initialization. + """ + logger.debug('File proxy for %s created', filename) + + # The file may not exist yet, hence there is no + # reliable way of knowing if it is a file on the + # disk or a directory. + if not isabs(filename) or filename.endswith(sep): + raise Exception('filename needs to be absolute') + + self._version = -1 + self._filename = filename + self._contents = contents + self._workspace = workspace + self._changes_history = [] + + def __str__(self): + """ + :return: + Return a string representation of a file proxy + with information about its version and filename. + """ + return ''.format( + self._filename, self._version) + + def update(self, diffs): + """ + The method updates the contents of the file proxy + instance by applying patches to the content and + changing the version number along. It also maintains + the update history of the proxy. + + :param diffs: + The list of patches in exact order to be applied + to the content. + """ + logger.debug('Updated file proxy %s', self._filename) + + if not isinstance(diffs, list): + diffs = [diffs] + + # TODO Handle the diff applying + + self._changes_history.extend(diffs) + + def replace(self, contents, version): + """ + The method replaces the content of the proxy + entirely and does not push the change to the + history. It is similar to updating the proxy + with the range spanning to the entire content. + + :param contents: + The new contents of the proxy. + :param version: + The version number proxy upgrades to after + the update. This needs to be greater than + the current version number. + :return: + Returns a boolean indicating the status of + the update. + """ + if version > self._version: + self._contents = contents + self._version = version + return True + + return False + + def get_disk_contents(self): + """ + :return: + Returns the contents of a copy of the file + on the disk. It might not be in sync with + the editor version of the file. + """ + with open(self.filename) as disk: + return disk.read() + + def contents(self): + """ + :return: + Returns the current contents of the proxy. + """ + return self._contents + + def close(self): + """ + Closing a proxy essentially means emptying the contents + of the proxy instance. + """ + self._contents = '' + + @property + def filename(self): + """ + :return: + Returns the complete file name. + """ + return self._filename + + @property + def workspace(self): + """ + :return: + Returns the workspace of the file. + """ + return self._workspace + + @property + def version(self): + """ + :return: + Returns the current edit version of the file. + """ + return self._version + + +class FileProxyMap: + """ + Proxy map handles a collection of proxies + and provides a mechanism to handles duplicate + proxies and resolving them. + """ + + def __init__(self, file_proxies=[]): + """ + :param file_proxies: + A list of FileProxy instances to initialize + the ProxyMap with. + """ + self._map = {proxy.filename: proxy for proxy in file_proxies} + + def add(self, proxy, replace=False): + """ + Add a proxy instance to the proxy map. + + :param proxy: + The proxy instance to register in the map. + :param replace: + A boolean flag indicating if the proxy should + replace an existing proxy of the same file. + :return: + Boolean true if registering of the proxy was + successful else false. + """ + if not isinstance(proxy, FileProxy): + return False + + if self._map.get(proxy.filename) is not None: + if replace: + self._map[proxy.filename] = proxy + return True + return False + + self._map[proxy.filename] = proxy + return True + + def remove(self, filename): + """ + Remove the proxy associated with a file from the + proxy map. + + :param filename: + The name of the file to remove the proxy + associated with. + """ + if self.get(filename): + del self._map[filename] + + def get(self, filename): + """ + :param filename: + The name of the file to get the associated proxy of. + :return: + A file proxy instance or None if not available. + """ + return self._map.get(filename) + + def resolve(self, filename, workspace=None, hard_sync=True): + """ + Resolve tries to find an available proxy or creates one + if there is no available proxy for the said file. + + :param filename: + The filename to search for in the map or to create + a proxy instance using. + :param workspace: + Used in case the lookup fails and a new instance is + being initialized. + :hard_sync: + Boolean flag indicating if the file should be initialized + from the file on disk or fail otherwise. + :return: + Returns a proxy instance or a boolean indicating the + failure of the resolution. + """ + proxy = self.get(filename) + if proxy is not None: + return proxy + + try: + proxy = FileProxy.from_name(filename, workspace) + except FileNotFoundError: + if hard_sync: + return False + + try: + proxy = FileProxy(filename, workspace) + except Exception: + return False + + self.add(proxy) + return proxy diff --git a/coalals/utils/log.py b/coalals/utils/log.py new file mode 100644 index 0000000..09767b0 --- /dev/null +++ b/coalals/utils/log.py @@ -0,0 +1,26 @@ +import logging +from sys import stderr + + +def configure_logger(): # pragma: no cover + """ + Configure logging to stream to stderr to not + interfere with the stdio mode of the server. + """ + logging.basicConfig(stream=stderr, level=logging.INFO) + + +def reset_logger(logger=None): # pragma: no cover + """ + Reset the logger while removing all the handlers. + This can reset the logger when it has been interfered + with some other logging configuration. + + :param logger: + The logger to reset configuration. + """ + logger = logging.getLogger() if logger is None else logger + for handler in logger.handlers[:]: + handler.removeHandler() + + configure_logger() diff --git a/coalals/utils/wrappers.py b/coalals/utils/wrappers.py new file mode 100644 index 0000000..3ac3d84 --- /dev/null +++ b/coalals/utils/wrappers.py @@ -0,0 +1,40 @@ +from socketserver import StreamRequestHandler + + +def func_wrapper(func, *args, **kargs): + """ + Minimal function wrapper to be used with process + pool. ProcessPool requires function to be picklable. + func_wrapper simplifies passing an callable to + executor by wrapping it. + + :param func: + The callable to wrap. + :param args: + The args to be passed to func callable. + :return: + The result of execution of func with args and kargs. + """ + return func(*args, **kargs) + + +class StreamHandlerWrapper(StreamRequestHandler): + """ + Wraps a stream processing class and abstracts setup and + handle methods. + """ + delegate = None + + def setup(self): + """ + Initialize delegate class instance with read and write + streams. + """ + super(StreamHandlerWrapper, self).setup() + self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile) + + def handle(self): + """ + Start the delegate class on handle being called. + """ + self.delegate.start() diff --git a/docs/images/demo.gif b/docs/images/demo.gif index 0422985..1af536e 100644 Binary files a/docs/images/demo.gif and b/docs/images/demo.gif differ diff --git a/docs/images/demo.png b/docs/images/demo.png index 04dcc55..c6f4465 100644 Binary files a/docs/images/demo.png and b/docs/images/demo.png differ diff --git a/requirements.txt b/requirements.txt index 03205ec..ba98a71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -coala>=0.10.0.dev20170213201648 -typing>=3.5.3.0 -coala-bears>=0.10.0.dev20170215041744 -python-language-server~=0.18.0 +coala>=0.11.0 +coala-bears>=0.11.1 +python-jsonrpc-server>=0.0.1 +whatthepatch>=0.0.5 diff --git a/test-requirements.txt b/test-requirements.txt index fcffd11..f55ef10 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,3 @@ -six>=0.11 -behave +pytest~=3.6.0 +pytest-cov codecov diff --git a/tests/resources/diagnostic/output_char_none.json b/tests/resources/diagnostic/output_char_none.json deleted file mode 100644 index 1147433..0000000 --- a/tests/resources/diagnostic/output_char_none.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "results": { - "autopep8": [ - { - "affected_code": [ - { - "end": { - "column": null, - "line": 1 - }, - "start": { - "column": null, - "line": 1 - } - } - ], - "message": "The code does not comply to PEP8.", - "origin": "PEP8Bear", - "severity": 0 - } - ] - } -} diff --git a/tests/resources/diagnostic/output_multiple_problems.json b/tests/resources/diagnostic/output_multiple_problems.json deleted file mode 100644 index 8ab8236..0000000 --- a/tests/resources/diagnostic/output_multiple_problems.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "results": { - "autopep8": [ - { - "affected_code": [ - { - "end": { - "column": null, - "line": 1 - }, - "start": { - "column": null, - "line": 1 - } - } - ], - "message": "The code does not comply to PEP8.", - "origin": "PEP8Bear", - "severity": 1 - }, - { - "affected_code": [ - { - "end": { - "column": 6, - "line": 1 - }, - "start": { - "column": 6, - "line": 1 - } - } - ], - "message": "W291 trailing whitespace", - "origin": "PycodestyleBear (W291)", - "severity": 1 - } - ], - "cli": [], - "commit": [], - "linelength": [], - "python": [ - { - "affected_code": [ - { - "end": { - "column": null, - "line": 1 - }, - "start": { - "column": null, - "line": 1 - } - } - ], - "message": "Line contains following spacing inconsistencies:\n- Trailing whitespaces.", - "origin": "SpaceConsistencyBear", - "severity": 1 - } - ], - "yml": [] - } -} diff --git a/tests/resources/diagnostic/output_normal_offset.json b/tests/resources/diagnostic/output_normal_offset.json deleted file mode 100644 index 74bb43a..0000000 --- a/tests/resources/diagnostic/output_normal_offset.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "results": { - "autopep8": [ - { - "affected_code": [ - { - "end": { - "column": 2, - "line": 1 - }, - "start": { - "column": 1, - "line": 1 - } - } - ], - "message": "The code does not comply to PEP8.", - "origin": "PEP8Bear", - "severity": 0 - } - ] - } -} diff --git a/tests/resources/diagnostic/output_severity_info.json b/tests/resources/diagnostic/output_severity_info.json deleted file mode 100644 index 1d7421e..0000000 --- a/tests/resources/diagnostic/output_severity_info.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "results": { - "autopep8": [ - { - "affected_code": [ - { - "end": { - "column": 1, - "line": 1 - }, - "start": { - "column": 1, - "line": 1 - } - } - ], - "message": "The code does not comply to PEP8.", - "origin": "PEP8Bear", - "severity": 0 - } - ] - } -} diff --git a/tests/resources/diagnostic/output_severity_major.json b/tests/resources/diagnostic/output_severity_major.json deleted file mode 100644 index 3a3c9fa..0000000 --- a/tests/resources/diagnostic/output_severity_major.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "results": { - "autopep8": [ - { - "affected_code": [ - { - "end": { - "column": 1, - "line": 1 - }, - "start": { - "column": 1, - "line": 1 - } - } - ], - "message": "The code does not comply to PEP8.", - "origin": "PEP8Bear", - "severity": 2 - } - ] - } -} diff --git a/tests/resources/diagnostic/output_severity_normal.json b/tests/resources/diagnostic/output_severity_normal.json deleted file mode 100644 index 4155134..0000000 --- a/tests/resources/diagnostic/output_severity_normal.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "results": { - "autopep8": [ - { - "affected_code": [ - { - "end": { - "column": 1, - "line": 1 - }, - "start": { - "column": 1, - "line": 1 - } - } - ], - "message": "The code does not comply to PEP8.", - "origin": "PEP8Bear", - "severity": 1 - } - ] - } -} diff --git a/tests/resources/diagnostics.json b/tests/resources/diagnostics.json new file mode 100644 index 0000000..1f5caa0 --- /dev/null +++ b/tests/resources/diagnostics.json @@ -0,0 +1,421 @@ +{ + "samples": [ + { + "coala": { + "results": { + "all": [], + "all.autopep8": [ + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": 5, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "line": 2 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "start": { + "column": 5, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "line": 2 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": null, + "id": 281466453796115535461439422208028175619, + "message": "E128 continuation line under-indented for visual indent", + "message_arguments": {}, + "message_base": "E128 continuation line under-indented for visual indent", + "origin": "PycodestyleBear (E128)", + "severity": 1 + }, + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": 1, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "line": 3 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "start": { + "column": 1, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "line": 3 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": null, + "id": 86549142496323668488131828852516756744, + "message": "E901 TokenError: EOF in multi-line statement", + "message_arguments": {}, + "message_base": "E901 TokenError: EOF in multi-line statement", + "origin": "PycodestyleBear (E901)", + "severity": 1 + } + ], + "all.linelength": [], + "all.python": [], + "all.yml": [], + "cli": [] + } + }, + "langserver": [ + { + "severity": 2, + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 1, + "character": 4 + } + }, + "source": "coala", + "message": "[all.autopep8] PycodestyleBear (E128): E128 continuation line under-indented for visual indent" + }, + { + "severity": 2, + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "source": "coala", + "message": "[all.autopep8] PycodestyleBear (E901): E901 TokenError: EOF in multi-line statement" + } + ] + }, + { + "coala": { + "results": { + "all": [], + "all.autopep8": [ + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": null, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "line": 2 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "start": { + "column": null, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "line": 2 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": null, + "id": 281466453796115535461439422208028175619, + "message": "E128 continuation line under-indented for visual indent", + "message_arguments": {}, + "message_base": "E128 continuation line under-indented for visual indent", + "origin": "PycodestyleBear (E128)", + "severity": 1 + }, + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": 1, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "line": 3 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "start": { + "column": 1, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure2.py", + "line": 3 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": null, + "id": 86549142496323668488131828852516756744, + "message": "E901 TokenError: EOF in multi-line statement", + "message_arguments": {}, + "message_base": "E901 TokenError: EOF in multi-line statement", + "origin": "PycodestyleBear (E901)", + "severity": 1 + } + ], + "all.linelength": [], + "all.python": [], + "all.yml": [], + "cli": [] + } + }, + "langserver": [ + { + "severity": 2, + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "source": "coala", + "message": "[all.autopep8] PycodestyleBear (E128): E128 continuation line under-indented for visual indent" + }, + { + "severity": 2, + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "source": "coala", + "message": "[all.autopep8] PycodestyleBear (E901): E901 TokenError: EOF in multi-line statement" + } + ] + } + ], + "fixes": [ + { + "results": { + "autopep8": [ + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": null, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 2 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "start": { + "column": null, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 2 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": { + "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py": "--- \n+++ \n@@ -1,5 +1,6 @@\n def test_function():\n- hello = \"Hey\"\n+ hello = \"Hey\"\n+\n \n if True:\n pass\n" + }, + "id": 114043415368839830363592685540474798853, + "message": "The code does not comply to PEP8.", + "message_arguments": {}, + "message_base": "The code does not comply to PEP8.", + "origin": "PEP8Bear", + "severity": 1 + }, + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": null, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 6 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "start": { + "column": null, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 5 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": { + "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py": "--- \n+++ \n@@ -2,5 +2,4 @@\n hello = \"Hey\"\n \n if True:\n- pass\n-\n+ pass\n" + }, + "id": 248965969153426220505454526877482618366, + "message": "The code does not comply to PEP8.", + "message_arguments": {}, + "message_base": "The code does not comply to PEP8.", + "origin": "PEP8Bear", + "severity": 1 + }, + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": 6, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 2 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "start": { + "column": 6, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 2 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": null, + "id": 254772311010356611066691872403954759470, + "message": "E111 indentation is not a multiple of four", + "message_arguments": {}, + "message_base": "E111 indentation is not a multiple of four", + "origin": "PycodestyleBear (E111)", + "severity": 1 + }, + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": 1, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 4 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "start": { + "column": 1, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 4 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": null, + "id": 178811740477814076025362794491165347773, + "message": "E305 expected 2 blank lines after class or function definition, found 1", + "message_arguments": {}, + "message_base": "E305 expected 2 blank lines after class or function definition, found 1", + "origin": "PycodestyleBear (E305)", + "severity": 1 + }, + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": 6, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 5 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "start": { + "column": 6, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 5 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": null, + "id": 61531538508211120440011129259809842309, + "message": "E111 indentation is not a multiple of four", + "message_arguments": {}, + "message_base": "E111 indentation is not a multiple of four", + "origin": "PycodestyleBear (E111)", + "severity": 1 + }, + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": 1, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 6 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "start": { + "column": 1, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 6 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": null, + "id": 87251029903777560447759498262381685728, + "message": "W391 blank line at end of file", + "message_arguments": {}, + "message_base": "W391 blank line at end of file", + "origin": "PycodestyleBear (W391)", + "severity": 1 + } + ], + "cli": [], + "linelength": [], + "python": [ + { + "additional_info": "", + "affected_code": [ + { + "end": { + "column": null, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 2 + }, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "start": { + "column": null, + "file": "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py", + "line": 2 + } + } + ], + "aspect": "NoneType", + "confidence": 100, + "debug_msg": "", + "diffs": { + "/home/ksdme/Work/gsoc/coala-ls/tests/resources/failure3.py": "--- \n+++ \n@@ -1,5 +1,5 @@\n def test_function():\n- hello = \"Hey\"\n+ hello = 'Hey'\n \n if True:\n pass\n" + }, + "id": 279859262526669763135858265098078150424, + "message": "You do not use the preferred quotation marks.", + "message_arguments": {}, + "message_base": "You do not use the preferred quotation marks.", + "origin": "QuotesBear", + "severity": 1 + } + ], + "yml": [] + } + } + ] +} diff --git a/tests/resources/failure.py b/tests/resources/failure.py new file mode 100644 index 0000000..7a46da7 --- /dev/null +++ b/tests/resources/failure.py @@ -0,0 +1,2 @@ +def failure(): + print('"'") diff --git a/tests/resources/failure2.py b/tests/resources/failure2.py new file mode 100644 index 0000000..4a990b1 --- /dev/null +++ b/tests/resources/failure2.py @@ -0,0 +1,2 @@ +def sample_failure(x: + pass diff --git a/tests/resources/failure3.py b/tests/resources/failure3.py new file mode 100644 index 0000000..7ab75f2 --- /dev/null +++ b/tests/resources/failure3.py @@ -0,0 +1,5 @@ +def test_function(): + hello = "Hey" + +if True: + pass diff --git a/tests/resources/fixes.json b/tests/resources/fixes.json new file mode 100644 index 0000000..960b35a --- /dev/null +++ b/tests/resources/fixes.json @@ -0,0 +1,14 @@ +{ + "diffs": [ + { + "diff": "--- \n+++ \n@@ -1,5 +1,5 @@\n def test_function():\n- hello = \"Hey\"\n+ hello = 'Hey'\n \n if True:\n pass\n", + "original": "def test_function():\n hello = \"Hey\"\n\nif True:\n pass\n\n", + "patched": "def test_function():\n hello = 'Hey'\n\nif True:\n pass\n\n" + }, + { + "diff": "--- \n+++ \n@@ -1,5 +1,5 @@\n def test_function():\n- hello = \"Hey\"\n+ hello = 'Hey!'\n \n if True:\n pass\n", + "original": "def test_function():\n hello = \"Hey\"\n\nif True:\n pass\n\n", + "patched": "def test_function():\n hello = 'Hey!'\n\nif True:\n pass\n\n" + } + ] +} diff --git a/tests/resources/qualified.py b/tests/resources/qualified.py deleted file mode 100644 index f5efd16..0000000 --- a/tests/resources/qualified.py +++ /dev/null @@ -1 +0,0 @@ -print('Hello, World.') diff --git a/tests/resources/unqualified.py b/tests/resources/unqualified.py deleted file mode 100644 index 2ce46a0..0000000 --- a/tests/resources/unqualified.py +++ /dev/null @@ -1,2 +0,0 @@ -def test(): - a = 1 \ No newline at end of file diff --git a/tests/server.features/coalashim.feature b/tests/server.features/coalashim.feature deleted file mode 100644 index 1cb3c9e..0000000 --- a/tests/server.features/coalashim.feature +++ /dev/null @@ -1,14 +0,0 @@ -Feature: coalashim module - coalashim is a module of language-server, it interacts with coala core. - - Scenario: Test run_coala_with_specific_file - Given the current directory and path of qualified.py - When I pass the qualified.py to run_coala_with_specific_file - Then it should return output in json format - And with no error in the output - - Scenario: Test run_coala_with_specific_file - Given the current directory and path of unqualified.py - When I pass the unqualified.py to run_coala_with_specific_file - Then it should return output in json format - And with autopep8 errors in the output diff --git a/tests/server.features/diagnostic.feature b/tests/server.features/diagnostic.feature deleted file mode 100644 index 8677e5c..0000000 --- a/tests/server.features/diagnostic.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: diagnostic module - diagnostic is a module of language-server. - - Scenario: Test output_to_diagnostics - Given the output with errors by coala - When I pass the parameters to output_to_diagnostics - Then it should return output in vscode format diff --git a/tests/server.features/jsonrpc.feature b/tests/server.features/jsonrpc.feature deleted file mode 100644 index 24ae1ad..0000000 --- a/tests/server.features/jsonrpc.feature +++ /dev/null @@ -1,23 +0,0 @@ -Feature: jsonrpc module - jsonrpc is a module of language-server. - - Scenario: Test JsonRpcStreamWriter and JsonRpcStreamReader - Given the message - When I write it to JsonRpcStreamWriter - Then it should read from JsonRpcStreamReader - - 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 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 -# Given the JSONRPC2Connection instance -# When I write a request to the JSONRPC2Connection with id -# Then it should return the request from JSONRPC2Connection with id diff --git a/tests/server.features/langserver.feature b/tests/server.features/langserver.feature deleted file mode 100644 index 4a5791a..0000000 --- a/tests/server.features/langserver.feature +++ /dev/null @@ -1,52 +0,0 @@ -Feature: langserver module - langserver is the main program of language-server. - - Scenario: Test serve_initialize with rootPath - Given the LangServer instance - When I send a initialize request with rootPath to the server - Then it should return the response with textDocumentSync - - 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 I should receive a publishDiagnostics type response - - 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 I should receive a publishDiagnostics type response - - 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 I should receive a publishDiagnostics type response - - 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 I should receive a publishDiagnostics type response - - 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/log.feature b/tests/server.features/log.feature deleted file mode 100644 index 1d7021a..0000000 --- a/tests/server.features/log.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: log module - log is a module of language-server. - - Scenario: Test log - Given There is a string - When I pass the string to log - Then it should return normally diff --git a/tests/server.features/steps/coalashim_steps.py b/tests/server.features/steps/coalashim_steps.py deleted file mode 100644 index 0062513..0000000 --- a/tests/server.features/steps/coalashim_steps.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: UTF-8 -*- - -# @mark.steps -# ---------------------------------------------------------------------------- -# STEPS: -# ---------------------------------------------------------------------------- -import os -import json -from behave import given, when, then -from coala_langserver.coalashim import run_coala_with_specific_file - - -@given('the current directory and path of qualified.py') -def step_impl(context): - context.dir = os.path.abspath( - os.path.join( - os.path.dirname(os.path.abspath(__file__)), - os.pardir, - os.pardir, - os.pardir - ) - ) - context.path = os.path.join(context.dir, 'tests', 'resources', 'qualified.py') - - -@when('I pass the qualified.py to run_coala_with_specific_file') -def step_impl(context): - context.output = run_coala_with_specific_file(context.dir, context.path) - - -@then('it should return output in json format') -def step_impl(context): - assert context.failed is False - - -@then('with no error in the output') -def step_impl(context): - assert context.output is None - - -@given('the current directory and path of unqualified.py') -def step_impl(context): - context.dir = os.path.abspath( - os.path.join( - os.path.dirname(os.path.abspath(__file__)), - os.pardir, - os.pardir, - os.pardir - ) - ) - context.path = os.path.join(context.dir, 'tests', 'resources', 'unqualified.py') - - -@when('I pass the unqualified.py to run_coala_with_specific_file') -def step_impl(context): - context.output = run_coala_with_specific_file(context.dir, context.path) - - -@then('with autopep8 errors in the output') -def step_impl(context): - assert json.loads(context.output)['results']['autopep8'] is not None diff --git a/tests/server.features/steps/diagnostic_steps.py b/tests/server.features/steps/diagnostic_steps.py deleted file mode 100644 index 4baf371..0000000 --- a/tests/server.features/steps/diagnostic_steps.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: UTF-8 -*- - -# @mark.steps -# ---------------------------------------------------------------------------- -# STEPS: -# ---------------------------------------------------------------------------- -import os -from behave import given, when, then -from coala_langserver.diagnostic import output_to_diagnostics -from coala_langserver.coalashim import run_coala_with_specific_file - - -@given('the output with errors by coala') -def step_impl(context): - context.dir = os.path.abspath( - os.path.join( - os.path.dirname(os.path.abspath(__file__)), - os.pardir, - os.pardir, - 'resources' - ) - ) - context.path = os.path.join(context.dir, 'unqualified.py') - - context.output = run_coala_with_specific_file(context.dir, context.path) - - -@when('I pass the parameters to output_to_diagnostics') -def step_impl(context): - context.message = output_to_diagnostics(context.output) - - -@then('it should return output in vscode format') -def step_impl(context): - assert len(context.message) is not 0 diff --git a/tests/server.features/steps/jsonrpc_steps.py b/tests/server.features/steps/jsonrpc_steps.py deleted file mode 100644 index 3164aff..0000000 --- a/tests/server.features/steps/jsonrpc_steps.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: UTF-8 -*- - -# @mark.steps -# ---------------------------------------------------------------------------- -# STEPS: -# ---------------------------------------------------------------------------- -import tempfile -import json -from behave import given, when, then -from pyls.jsonrpc import streams -from pyls.jsonrpc import endpoint -from pyls.jsonrpc import dispatchers - - -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) - - -@given('the message') -def step_impl(context): - context.message = { - 'simple': 'test', - } - - -@when('I write it to JsonRpcStreamWriter') -def step_impl(context): - context.f = tempfile.TemporaryFile(mode='w+b') - context.writer = streams.JsonRpcStreamWriter(context.f) - context.writer.write(context.message) - - -@then('it should read from JsonRpcStreamReader') -def step_impl(context): - context.f.seek(0) - context._passed = False - - def consumer(message): - assert issimilar(context.message, message) - context._passed = True - context.writer.close() - - reader = streams.JsonRpcStreamReader(context.f) - reader.listen(consumer) - reader.close() - - if not context._passed: - assert False - - -@given('a notification type rpc request') -def step_impl(context): - context.request = { - 'jsonrpc': '2.0', - 'method': 'math/add', - 'params': { - 'a': 1, - 'b': 2, - }, - } - - -@when('I send rpc request using JsonRpcStreamWriter') -def step_impl(context): - context.f = tempfile.TemporaryFile() - context.writer = streams.JsonRpcStreamWriter(context.f) - context.writer.write(context.request) - - -@then('it should invoke the notification consumer with args') -def step_impl(context): - context.f.seek(0) - context._passed = False - - class Example(dispatchers.MethodDispatcher): - - 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() - - if not context._passed: - assert False - - -@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) - context._passed = False - - class Example(dispatchers.MethodDispatcher): - - 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 - - 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 deleted file mode 100644 index 2c8b367..0000000 --- a/tests/server.features/steps/langserver_steps.py +++ /dev/null @@ -1,358 +0,0 @@ -# -*- coding: UTF-8 -*- - -# @mark.steps -# ---------------------------------------------------------------------------- -# STEPS: -# ---------------------------------------------------------------------------- -import io -import os -import sys -import time -import socket -import tempfile -from threading import Thread - -from behave import given, when, then -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(context.f, context.f) - - -@when('I send a initialize request with rootPath to the server') -def step_impl(context): - request = { - 'method': 'initialize', - 'params': { - 'rootPath': '/Users/mock-user/mock-dir', - 'capabilities': {}, - }, - 'id': 1, - 'jsonrpc': '2.0' - } - 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) - 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') -def step_impl(context): - request = { - 'method': 'textDocument/didSave', - 'params': { - 'textDocument': { - '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._endpoint.consume(request) - context._diagCount = 0 - - -@then('I should receive a publishDiagnostics type response') -def step_impl(context): - context.f.seek(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 diff --git a/tests/server.features/steps/log_steps.py b/tests/server.features/steps/log_steps.py deleted file mode 100644 index af9287e..0000000 --- a/tests/server.features/steps/log_steps.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: UTF-8 -*- - -# @mark.steps -# ---------------------------------------------------------------------------- -# STEPS: -# ---------------------------------------------------------------------------- -from behave import given, when, then -from coala_langserver.log import log - - -@given('There is a string') -def step_impl(context): - context.str = 'file://Users' - - -@when('I pass the string to log') -def step_impl(context): - log(context.str) - - -@then('it should return normally') -def step_impl(context): - assert context.failed is False diff --git a/tests/server.features/steps/uri_steps.py b/tests/server.features/steps/uri_steps.py deleted file mode 100644 index fdb2d74..0000000 --- a/tests/server.features/steps/uri_steps.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: UTF-8 -*- - -# @mark.steps -# ---------------------------------------------------------------------------- -# STEPS: -# ---------------------------------------------------------------------------- -from behave import given, when, then -from coala_langserver.uri import path_from_uri, dir_from_uri - - -@given('There is a string with "file://"') -def step_impl(context): - context.str = 'file://Users' - - -@when('I pass the string with the prefix to path_from_uri') -def step_impl(context): - context.path = path_from_uri(context.str) - - -@given('There is a string without "file://"') -def step_impl(context): - context.str = '/Users' - - -@when('I pass the string without the prefix to path_from_uri') -def step_impl(context): - context.path = path_from_uri(context.str) - - -@then('it should return a string without "file://"') -def step_impl(context): - assert context.failed is False - assert 'file://' not in context.path - - -@when('I pass the string to dir_from_uri') -def step_impl(context): - context.path = dir_from_uri(context.str) - - -@then('it should return the directory of the path') -def step_impl(context): - assert context.failed is False - assert context.path is '/' diff --git a/tests/server.features/uri.feature b/tests/server.features/uri.feature deleted file mode 100644 index fc2d2c2..0000000 --- a/tests/server.features/uri.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: uri module - uri is a module of language-server. - - Scenario: Test path_from_uri - Given There is a string with "file://" - When I pass the string with the prefix to path_from_uri - Then it should return a string without "file://" - - Scenario: Test path_from_uri - Given There is a string without "file://" - When I pass the string without the prefix to path_from_uri - Then it should return a string without "file://" - - Scenario: Test dir_from_uri - Given There is a string without "file://" - When I pass the string to dir_from_uri - Then it should return the directory of the path diff --git a/tests/test_coalashim.py b/tests/test_coalashim.py deleted file mode 100644 index af0ac01..0000000 --- a/tests/test_coalashim.py +++ /dev/null @@ -1,75 +0,0 @@ -import unittest -from unittest import mock - -from coala_langserver.coalashim import run_coala_with_specific_file - - -def generate_side_effect(message, ret): - def side_effect(): - print(message, end=''), - return ret - return side_effect - - -@mock.patch('coala_langserver.coalashim.log') -@mock.patch('coala_langserver.coalashim.coala') -class ShimTestCase(unittest.TestCase): - - def test_call(self, mock_coala, mock_log): - mock_coala.return_value = 1 - run_coala_with_specific_file(None, None) - - # coala is called without arguments - mock_coala.main.assert_called_with() - - def test_issue_with_result(self, mock_coala, mock_log): - message = 'issue found' - mock_coala.main.side_effect = generate_side_effect(message, 1) - output = run_coala_with_specific_file(None, None) - - # log is message information - mock_log.assert_called_with('Output =', message) - # return value is issue message - self.assertEqual(message, output) - - def test_issue_no_result(self, mock_coala, mock_log): - message = '' - mock_coala.main.side_effect = generate_side_effect(message, 1) - output = run_coala_with_specific_file(None, None) - - # log is `no results` reminder - mock_log.assert_called_with('No results for the file') - # return value is empty string - self.assertEqual(message, output) - - def test_no_issue(self, mock_coala, mock_log): - message = 'no issue' - mock_coala.main.side_effect = generate_side_effect(message, 0) - output = run_coala_with_specific_file(None, None) - - # log is `no issue` reminder - mock_log.assert_called_with('No issues found') - # return value is None - self.assertEqual(None, output) - - def test_coala_error(self, mock_coala, mock_log): - message = 'fatal error' - mock_coala.main.side_effect = generate_side_effect(message, -1) - output = run_coala_with_specific_file(None, None) - - # log is `exit` reminder - mock_log.assert_called_with('Exited with:', -1) - # return value is None - self.assertEqual(None, output) - - @mock.patch('coala_langserver.coalashim.os') - def test_working_dir_normal(self, mock_os, mock_coala, mock_log): - working_dir = '/' - run_coala_with_specific_file(working_dir, None) - mock_os.chdir.assert_called_with(working_dir) - - @mock.patch('coala_langserver.coalashim.os') - def test_working_dir_none(self, mock_os, mock_coala, mock_log): - working_dir = None - run_coala_with_specific_file(working_dir, None) - mock_os.chdir.assert_called_with('.') diff --git a/tests/test_diagnostic.py b/tests/test_diagnostic.py deleted file mode 100644 index 62d1949..0000000 --- a/tests/test_diagnostic.py +++ /dev/null @@ -1,75 +0,0 @@ -import os -import unittest - -from coala_langserver.diagnostic import output_to_diagnostics - - -def get_output(filename): - file_path = os.path.join(os.path.dirname(__file__), - 'resources/diagnostic', - filename) - with open(file_path, 'r') as file: - output = file.read() - return output - - -class DiagnosticTestCase(unittest.TestCase): - - def test_none_output(self): - result = output_to_diagnostics(None) - self.assertEqual(result, None) - - def test_severity_info(self): - output = get_output('output_severity_info.json') - result = output_to_diagnostics(output) - - # INFO: 0 (coala) -> Information: 3 (LSP) - self.assertEqual(result[0]['severity'], 3) - - def test_severity_normal(self): - output = get_output('output_severity_normal.json') - result = output_to_diagnostics(output) - - # NORMAL: 1 (coala) -> Warning: 2 (LSP) - self.assertEqual(result[0]['severity'], 2) - - def test_severity_major(self): - output = get_output('output_severity_major.json') - result = output_to_diagnostics(output) - - # MAJOR: 2 (coala) -> Error: 1 (LSP) - self.assertEqual(result[0]['severity'], 1) - - def test_char_none(self): - output = get_output('output_char_none.json') - result = output_to_diagnostics(output) - - # None column should be regarded as the whole line - start_line = result[0]['range']['start']['line'] - start_char = result[0]['range']['start']['character'] - end_line = result[0]['range']['end']['line'] - end_char = result[0]['range']['end']['character'] - self.assertEqual(start_char, 0) - self.assertEqual(end_char, 0) - self.assertEqual(start_line + 1, end_line) - - def test_normal_offset(self): - output = get_output('output_normal_offset.json') - result = output_to_diagnostics(output) - - # normal offset, one-based -> zero-based - start_line = result[0]['range']['start']['line'] - start_char = result[0]['range']['start']['character'] - end_line = result[0]['range']['end']['line'] - end_char = result[0]['range']['end']['character'] - self.assertEqual(start_char, 0) - self.assertEqual(end_char, 1) - self.assertEqual(start_line, 0) - self.assertEqual(end_line, 0) - - def test_multiple_problems(self): - output = get_output('output_multiple_problems.json') - result = output_to_diagnostics(output) - - # should be able to handle multiple bears & problems - self.assertEqual(len(result), 3) diff --git a/tests/tests_coalals/conftest.py b/tests/tests_coalals/conftest.py new file mode 100644 index 0000000..b22ef98 --- /dev/null +++ b/tests/tests_coalals/conftest.py @@ -0,0 +1,6 @@ +import sys +from os.path import join, dirname, abspath + +current = dirname(__file__) +sys.path.append(join(current, 'helpers')) +sys.path.append(abspath(join(current, '../../'))) diff --git a/tests/tests_coalals/helpers/__init__.py b/tests/tests_coalals/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_coalals/helpers/dummies.py b/tests/tests_coalals/helpers/dummies.py new file mode 100644 index 0000000..bd34538 --- /dev/null +++ b/tests/tests_coalals/helpers/dummies.py @@ -0,0 +1,123 @@ +class DummyFileProxy: + + def __init__(self, filename, workspace='.', content=''): + self.filename = filename + self.workspace = workspace + self.content = content + self.version = -1 + + +class DummyFileProxyMap: + + def __init__(self, file_map={}): + self.file_map = file_map + + def resolve(self, name): + return self.file_map.get(name) + + +class DummyDiagnostics: + + def __init__(self, warnings=[], fixes=[]): + self.f_warnings = warnings + self.f_fixes = fixes + + def warnings(self): + return self.f_warnings + + +class DummyFuture: + + def __init__(self, active=True, on_cancel=True, result=None): + self.f_active = active + self.f_result = result + self.f_cancel = on_cancel + self._cancelled = False + + def done(self): + return not self.f_active + + def cancel(self): + if self.f_cancel: + self.f_active = False + + self._cancelled = True + return self.f_cancel + + def cancelled(self): + return self._cancelled + + def exception(self): + return None + + def result(self): + return self.f_result + + def add_done_callback(self, func): + func(self) + + +class DummyAlwaysCancelledFuture(DummyFuture): + + def __init__(self, active=True, on_cancel=True, result=None): + DummyFuture.__init__(self, active, on_cancel, result) + self._cancelled = True + + +class DummyAlwaysExceptionFuture(DummyFuture): + + def exception(self): + return Exception() + + +class DummyProcessPoolExecutor: + + FutureClass = DummyFuture + on_submit = None + + def __init__(self, max_workers=1, *args, **kargs): + self._max_workers = max_workers + + def submit(self, func, *args, **kargs): + self._func = lambda: func(*args, **kargs) + result = self._func() + + if DummyProcessPoolExecutor.on_submit is None: + return DummyProcessPoolExecutor.FutureClass(result=result) + else: + return DummyProcessPoolExecutor.on_submit + + def shutdown(self, *args, **kargs): + self._closed = True + return True + + +class DummyLangServer: + + def __init__(self, rfile, wfile, *args, **kargs): + self.f_rfile = rfile + self.f_wfile = wfile + self.started = False + + def start(self): + self.started = True + + +class DummyTCPServer: + + panic = False + served = False + closed = False + keyboard_interrupt = False + + def __init__(self, *args, **kargs): + if DummyTCPServer.panic: + raise Exception() + + def serve_forever(self): + DummyTCPServer.served = True + if DummyTCPServer.keyboard_interrupt: + raise KeyboardInterrupt() + + def server_close(self): + DummyTCPServer.closed = True diff --git a/tests/tests_coalals/helpers/resources.py b/tests/tests_coalals/helpers/resources.py new file mode 100644 index 0000000..a184e2b --- /dev/null +++ b/tests/tests_coalals/helpers/resources.py @@ -0,0 +1,34 @@ +from pathlib import Path + +base_relative_url = Path.cwd().joinpath( + 'tests', 'resources') + + +def url(val, as_obj=False): + names = val.split('|') + path = base_relative_url.joinpath(*names) + + return path if as_obj else str(path) + + +sample_diagnostics = url('diagnostics.json') + +sample_code_files = { + url('failure.py'): { + 'diagnostics': 1, + }, + + url('failure2.py'): { + 'diagnostics': 2, + }, +} + +sample_fixes_file = url('fixes.json') + + +def count_diagnostics(diagnostics): + diagnostics_count = 0 + for section, diags in diagnostics.items(): + diagnostics_count += len(diags) + + return diagnostics_count diff --git a/tests/tests_coalals/helpers/utils.py b/tests/tests_coalals/helpers/utils.py new file mode 100644 index 0000000..4066fac --- /dev/null +++ b/tests/tests_coalals/helpers/utils.py @@ -0,0 +1,9 @@ +from pathlib import Path + + +def get_random_path(suffix, as_obj=False, py=True): + ext = 'py' if py else 'txt' + filename = 'coala-rox-{}.{}'.format(suffix, ext) + path = Path.cwd().joinpath('coala-x-miss', filename) + + return path if as_obj else str(path) diff --git a/tests/tests_coalals/test_concurrency__JobTracker.py b/tests/tests_coalals/test_concurrency__JobTracker.py new file mode 100644 index 0000000..6e3a576 --- /dev/null +++ b/tests/tests_coalals/test_concurrency__JobTracker.py @@ -0,0 +1,142 @@ +import pytest + +from helpers.dummies import DummyFuture +from coalals.concurrency import JobTracker + + +@pytest.fixture +def completed_future(): + return DummyFuture(False, True) + + +@pytest.fixture +def running_future(): + return DummyFuture(True, True) + + +@pytest.fixture +def jobtracker(): + return JobTracker() + + +def test_jobtracker_init(): + # Mostly to ensure that the internal API + # is consistent for following tests. + tracker = JobTracker() + assert tracker._jobs == [] + assert tracker._max_jobs == 1 + + tracker = JobTracker(max_jobs=3) + assert tracker._jobs == [] + assert tracker._max_jobs == 3 + + with pytest.raises(ValueError): + tracker = JobTracker(max_jobs=0) + + +def test_jobtracker_kill(): + dummy = DummyFuture(False, True) + assert JobTracker.kill_job(dummy) is True + + dummy.f_cancel = False + assert JobTracker.kill_job(dummy) is False + + +def test_jobtracker_is_active(): + dummy = DummyFuture(True, True) + assert JobTracker.is_active(dummy) is True + + dummy.f_active = False + assert JobTracker.is_active(dummy) is False + + +def test_jobtracker_refresh(jobtracker, completed_future, running_future): + jobtracker._jobs = [completed_future, running_future] + assert len(jobtracker._jobs) == 2 + + jobtracker.refresh_jobs() + assert len(jobtracker._jobs) == 1 + assert completed_future not in jobtracker._jobs + + jobtracker._jobs[0].f_active = False + jobtracker.refresh_jobs() + assert len(jobtracker._jobs) == 0 + assert running_future not in jobtracker._jobs + + +def test_jobtracker_has_slots(completed_future, running_future): + jobtracker = JobTracker(max_jobs=2) + assert jobtracker.has_slots() is True + + jobtracker._jobs = [running_future] + assert jobtracker.has_slots() is True + + jobtracker._jobs = [running_future, running_future] + assert jobtracker.has_slots() is False + + jobtracker._jobs = [completed_future, running_future] + assert jobtracker.has_slots() is True + + +def test_force_free_slots(): + jobtracker = JobTracker(max_jobs=2) + one, two = DummyFuture(), DummyFuture() + + jobtracker._jobs = [one] + assert jobtracker.force_free_slots() is True + + one.f_cancel = False + jobtracker._jobs = [one, two] + assert jobtracker.force_free_slots() is False + + one.f_cancel = True + assert jobtracker.force_free_slots() is True + assert len(jobtracker) == 1 + assert one not in jobtracker._jobs + + three = DummyFuture() + two.f_cancel = False + jobtracker._jobs = [one, two, three] + assert jobtracker.force_free_slots() is False + + two.f_cancel = True + assert jobtracker.force_free_slots() is True + assert len(jobtracker) == 1 + assert one not in jobtracker._jobs + assert two not in jobtracker._jobs + + +def test_jobtracker_prepare_slot(): + jobtracker = JobTracker(max_jobs=2) + one, two = DummyFuture(), DummyFuture() + + jobtracker._jobs = [one] + assert jobtracker.prepare_slot(force=False) is True + + jobtracker._jobs = [one, two] + assert jobtracker.prepare_slot(force=False) is False + + one.f_cancel = False + assert jobtracker.prepare_slot(force=True) is False + + one.f_cancel = True + assert jobtracker.prepare_slot(force=True) is True + jobtracker.refresh_jobs() + assert one not in jobtracker._jobs + + +def test_jobtracker_add(jobtracker, running_future): + jobtracker.add(running_future) + assert jobtracker._jobs == [running_future] + + jobtracker.add(running_future) + assert jobtracker._jobs == [running_future, running_future] + + +def test_jobtracker_len(jobtracker, running_future, completed_future): + jobtracker.add(running_future) + assert len(jobtracker) == 1 + + jobtracker.add(completed_future) + assert len(jobtracker) == 1 + assert completed_future not in jobtracker._jobs diff --git a/tests/tests_coalals/test_concurrency__TrackedProcessPool.py b/tests/tests_coalals/test_concurrency__TrackedProcessPool.py new file mode 100644 index 0000000..0e46255 --- /dev/null +++ b/tests/tests_coalals/test_concurrency__TrackedProcessPool.py @@ -0,0 +1,54 @@ +import pytest + +from coalals.concurrency import JobTracker, TrackedProcessPool +from helpers.dummies import DummyFuture, DummyProcessPoolExecutor + + +@pytest.fixture +def trackedpool(monkeypatch): + def _trackedpool(max_jobs=1, max_workers=1): + with monkeypatch.context() as patch: + patch.setattr('coalals.concurrency.ProcessPoolExecutor', + DummyProcessPoolExecutor) + + return TrackedProcessPool(max_jobs, max_workers) + return _trackedpool + + +def test_trackedpool_init(trackedpool): + trackedpool = trackedpool() + assert isinstance(trackedpool._job_tracker, JobTracker) + assert isinstance(trackedpool._process_pool, DummyProcessPoolExecutor) + + +def test_shutdown(trackedpool): + trackedpool = trackedpool() + trackedpool.shutdown() + assert trackedpool._process_pool._closed + + +def test_exec_func(trackedpool): + trackedpool = trackedpool(2) + one, two = DummyFuture(), DummyFuture() + + def _internal_func(*args, **kargs): + return args + + with pytest.raises(TypeError): + trackedpool.exec_func(_internal_func, True) + + trackedpool._job_tracker._jobs = [one, two] + assert trackedpool.exec_func(_internal_func, (True,)) is False + + one.f_cancel = False + assert trackedpool.exec_func(_internal_func, (True,)) is False + + one.f_cancel = True + future = trackedpool.exec_func(_internal_func, ('coala',), force=True) + trackedpool._job_tracker.refresh_jobs() + + assert isinstance(future, DummyFuture) + assert trackedpool._job_tracker._jobs == [two, future] + # unpacking is done by func_wrapper + # which is not used here, hence... + assert future.result() == ('coala',) diff --git a/tests/tests_coalals/test_interface.py b/tests/tests_coalals/test_interface.py new file mode 100644 index 0000000..1cb485a --- /dev/null +++ b/tests/tests_coalals/test_interface.py @@ -0,0 +1,139 @@ +import pytest +from io import StringIO +from json import loads, dumps + +from coalals.interface import coalaWrapper +from helpers.utils import get_random_path +from helpers.resources import url, sample_code_files, count_diagnostics +from helpers.dummies import (DummyFuture, DummyFileProxy, DummyFileProxyMap, + DummyProcessPoolExecutor) + + +@pytest.fixture +def sample_proxymap(): + file_map = {} + + for filename in sample_code_files.keys(): + file_map[filename] = DummyFileProxy(filename) + + return DummyFileProxyMap(file_map) + + +@pytest.fixture +def coala(monkeypatch): + def _internal(patch_run_coala=False, patched_ret_val=1): + with monkeypatch.context() as patch: + if patch_run_coala: + def _patch(*args): + return (StringIO(), patched_ret_val) + + monkeypatch.setattr('test_interface.coalaWrapper._run_coala', + _patch) + + patch.setattr('coalals.concurrency.ProcessPoolExecutor', + DummyProcessPoolExecutor) + + return coalaWrapper() + return _internal + + +def get_gen_diag_count(result): + if hasattr(result, 'result'): + result = result.result() + + gen_diagnostics = loads(result)['results'] + return count_diagnostics(gen_diagnostics) + + +def test_coalawrapper_analyse_file(coala, sample_proxymap): + coala = coala() + + filename = url('failure.py') + proxy = sample_proxymap.resolve(filename) + proxy.workspace = None + + gen_diagnostics = coala.analyse_file(proxy) + gen_diag_count = get_gen_diag_count(gen_diagnostics) + + sample_code = sample_code_files[filename] + exp_diag_count = sample_code['diagnostics'] + + assert gen_diag_count == exp_diag_count + + +def test_coalawrapper_analyse_missing_file(coala): + coala = coala() + + random_path = get_random_path('1') + proxy = DummyFileProxy(random_path) + + gen_diagnostics = coala.analyse_file(proxy) + gen_diag_count = get_gen_diag_count(gen_diagnostics) + + assert gen_diag_count == 0 + + +def test_coalawrapper_p_analyse_file(coala, sample_proxymap): + coala = coala() + + filename = url('failure2.py') + proxy = sample_proxymap.resolve(filename) + + result = coala.p_analyse_file(proxy) + assert isinstance(result, DummyFuture) + gen_diag_count = get_gen_diag_count(result) + + sample_code = sample_code_files[filename] + exp_diag_count = sample_code['diagnostics'] + + assert gen_diag_count == exp_diag_count + + +def test_coalawrapper_p_analyse_missing_file(coala): + coala = coala() + + random_path = get_random_path('2') + proxy = DummyFileProxy(random_path) + + result = coala.p_analyse_file(proxy) + assert isinstance(result, DummyFuture) + gen_diag_count = get_gen_diag_count(result) + + assert gen_diag_count == 0 + + +def test_coalawrapper_p_analyse_file_fail_job(coala): + coala = coala() + one, two = DummyFuture(), DummyFuture() + + random_path = get_random_path('3') + proxy = DummyFileProxy(random_path) + + one.f_cancel = False + two.f_cancel = False + coala._tracked_pool._job_tracker._jobs = [one, two] + + result = coala.p_analyse_file(proxy) + assert result is False + + +@pytest.mark.parametrize('retval', (1, -1)) +def test_coalawrapper_run_coala_patched_op(coala, retval): + coala = coala(True, retval) + + random_path = get_random_path('4') + proxy = DummyFileProxy(random_path) + + result = coala.p_analyse_file(proxy) + assert isinstance(result, DummyFuture) + gen_diag_count = get_gen_diag_count(result) + + assert gen_diag_count == 0 + + +def test_coalawrapper_close(coala): + coala = coala() + coala.close() + + # mocked shutdown property + assert coala._tracked_pool._process_pool._closed diff --git a/tests/tests_coalals/test_langserver.py b/tests/tests_coalals/test_langserver.py new file mode 100644 index 0000000..47ab8bd --- /dev/null +++ b/tests/tests_coalals/test_langserver.py @@ -0,0 +1,569 @@ +import pytest +from os.path import dirname +from jsonrpc.streams import JsonRpcStreamReader +from tempfile import TemporaryFile, NamedTemporaryFile + +from coalals.langserver import LangServer +from coalals.utils.files import FileProxy, UriUtils +from helpers.utils import get_random_path +from helpers.resources import url, sample_code_files +from helpers.dummies import (DummyDiagnostics, DummyProcessPoolExecutor, + DummyAlwaysCancelledFuture, + DummyAlwaysExceptionFuture) + + +@pytest.fixture +def file_langserver(monkeypatch): + DummyProcessPoolExecutor.on_submit = None + + with monkeypatch.context() as patch: + patch.setattr('coalals.concurrency.ProcessPoolExecutor', + DummyProcessPoolExecutor) + + file = TemporaryFile() + langserver = LangServer(file, file) + + return (file, langserver) + + +@pytest.fixture +def file_langserver_deep(monkeypatch): + DummyProcessPoolExecutor.on_submit = None + + with monkeypatch.context() as patch: + patch.setattr('coalals.concurrency.ProcessPoolExecutor', + DummyProcessPoolExecutor) + # concurrent.futures maintains the same API for PoolExecutor's + # hence, reusing DummyProcessPoolExecutor for ThreadPoolExecutor + patch.setattr('jsonrpc.endpoint.futures.ThreadPoolExecutor', + DummyProcessPoolExecutor) + + file = TemporaryFile() + langserver = LangServer(file, file) + + return (file, langserver) + + +@pytest.fixture +def verify_response(): + def _internal(file, langserver, consumer, **kargs): + passed = [False] + file.seek(0) + + def _consumer(response): + consumer(file, response, passed, **kargs) + + reader = JsonRpcStreamReader(file) + reader.listen(_consumer) + reader.close() + + assert passed == [True] + return _internal + + +@pytest.fixture +def verify_docsync_respone(verify_response): + def _internal(file, langserver): + def consumer(file, response, passed): + assert response is not None + capabilities = response['result']['capabilities'] + assert capabilities['textDocumentSync'] == 1 + assert capabilities['documentFormattingProvider'] == 1 + + file.close() + passed[0] = True + + verify_response(file, langserver, consumer) + return _internal + + +@pytest.fixture +def verify_publish_respone(verify_response): + def _internal(file, langserver, diag_count): + def consumer(file, response, passed, diag_count): + assert response is not None + assert response['method'] == 'textDocument/publishDiagnostics' + assert len(response['params']['diagnostics']) is diag_count + + file.close() + passed[0] = True + + verify_response(file, langserver, consumer, diag_count=diag_count) + return _internal + + +def test_server_init_with_rootPath(file_langserver, verify_docsync_respone): + file, langserver = file_langserver + random = get_random_path('1', True) + uri = random.as_uri() + + request = { + 'method': 'initialize', + 'params': { + 'rootPath': UriUtils.dir_from_uri(uri), + 'capabilities': {}, + }, + 'id': 1, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + verify_docsync_respone(file, langserver) + + +def test_server_init_with_rootUri(file_langserver, verify_docsync_respone): + file, langserver = file_langserver + random = get_random_path('1', True) + uri = random.as_uri() + + request = { + 'method': 'initialize', + 'params': { + 'rootUri': UriUtils.dir_from_uri(uri), + 'capabilities': {}, + }, + 'id': 1, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + verify_docsync_respone(file, langserver) + + +def test_send_diagnostics(file_langserver, verify_publish_respone): + file, langserver = file_langserver + langserver.send_diagnostics('/sample', DummyDiagnostics()) + verify_publish_respone(file, langserver, 0) + + +def test_lanserver_shutdown(file_langserver): + file, langserver = file_langserver + langserver.m_shutdown() + + assert langserver._shutdown is True + + +def test_langserver_exit_no_shutdown(file_langserver): + _, langserver = file_langserver + + with pytest.raises(SystemExit) as excp: + langserver.m_exit() + + assert excp.value.code == 1 + + +def test_langserver_exit_shutdown(file_langserver): + _, langserver = file_langserver + langserver.m_shutdown() + + with pytest.raises(SystemExit) as excp: + langserver.m_exit() + + assert excp.value.code == 0 + + +def assert_missing(langserver, filename): + proxymap = langserver._proxy_map + proxy = proxymap.get(filename) + + assert proxy is None + + +def assert_changed_file(langserver, filename, version, content): + proxymap = langserver._proxy_map + proxy = proxymap.get(filename) + + assert proxy is not None + assert proxy.version == version + assert proxy.contents() == content + + +def test_did_change_proxy_replace_new_file(file_langserver): + file, langserver = file_langserver + random_path = get_random_path('1', True) + random_uri = random_path.as_uri() + + # tests the replace mode of the didChange + request = { + 'method': 'textDocument/didChange', + 'params': { + 'textDocument': { + 'uri': random_uri, + 'version': 1, + }, + 'contentChanges': [ + { + 'text': 'print("coala-rocks!")', + } + ], + }, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + assert_missing(langserver, str(random_path)) + + +def test_did_change_proxy_replace_open_file(file_langserver): + file, langserver = file_langserver + + source = NamedTemporaryFile(delete=False) + source.write('coala'.encode('utf-8')) + source.close() + + proxy = FileProxy(source.name) + langserver._proxy_map.add(proxy) + + request = { + 'method': 'textDocument/didChange', + 'params': { + 'textDocument': { + 'uri': UriUtils.file_to_uri(source.name), + 'version': 2, + }, + 'contentChanges': [ + { + 'text': 'print("coala-bears!")', + } + ], + }, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + assert_changed_file(langserver, source.name, 2, 'print("coala-bears!")') + + # not greater version + request = { + 'method': 'textDocument/didChange', + 'params': { + 'textDocument': { + 'uri': UriUtils.file_to_uri(source.name), + 'version': 1, + }, + 'contentChanges': [ + { + 'text': 'print("coala-bears-old!")', + } + ], + }, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + assert_changed_file(langserver, source.name, 2, 'print("coala-bears!")') + + +def test_did_change_proxy_replace_missing_file(file_langserver): + file, langserver = file_langserver + random_path = get_random_path('2', True) + file_uri = random_path.as_uri() + + request = { + 'method': 'textDocument/didChange', + 'params': { + 'textDocument': { + 'uri': file_uri, + 'version': 2, + }, + 'contentChanges': [ + { + 'text': 'print("coala-bears!")', + } + ], + }, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + proxymap = langserver._proxy_map + proxy = proxymap.get(str(random_path)) + + assert proxy is None + + +def test_did_change_proxy_update(file_langserver): + file, langserver = file_langserver + filename = url('failure2.py', True) + + proxy = FileProxy(str(filename)) + langserver._proxy_map.add(proxy) + + request = { + 'method': 'textDocument/didChange', + 'params': { + 'textDocument': { + 'uri': filename.as_uri(), + 'version': 2, + }, + 'contentChanges': [ + { + 'range': { + 'start': { + 'line': 0, + 'character': 0, + }, + 'end': { + 'line': 0, + 'character': 5, + }, + }, + 'rangeLength': 5, + 'text': 'print("coala-bears!")', + } + ], + }, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + # FIXME Update to test the updates + + +def test_langserver_did_open(file_langserver, verify_publish_respone): + file, langserver = file_langserver + filename = url('failure2.py') + + code = None + with open(filename) as code_file: + code = code_file.read() + + request = { + 'method': 'textDocument/didOpen', + 'params': { + 'textDocument': { + 'uri': UriUtils.file_to_uri(filename), + 'languageId': 'python', + 'version': 1, + 'text': code, + }, + }, + 'jsonrpc': '2.0', + } + + code_file = sample_code_files[filename] + exp_diag_count = code_file['diagnostics'] + + langserver._endpoint.consume(request) + verify_publish_respone(file, langserver, exp_diag_count) + + +def test_langserver_did_save(file_langserver, verify_publish_respone): + file, langserver = file_langserver + + code_sample_path = url('failure.py', True) + code_sample_name = str(code_sample_path) + proxy = FileProxy(code_sample_name) + langserver._proxy_map.add(proxy) + + request = { + 'method': 'textDocument/didSave', + 'params': { + 'textDocument': { + 'uri': code_sample_path.as_uri(), + }, + }, + 'jsonrpc': '2.0', + } + + code_desc = sample_code_files[code_sample_name] + exp_diag_count = code_desc['diagnostics'] + + langserver._endpoint.consume(request) + verify_publish_respone(file, langserver, exp_diag_count) + + +def assert_callback_not_called(file): + passed = [False] + file.seek(0) + + def _consumer(response): + passed[0] = True + + reader = JsonRpcStreamReader(file) + reader.listen(_consumer) + reader.close() + + assert passed[0] is False + + +def test_langserver_did_save_missing_proxy(file_langserver, + verify_publish_respone): + file, langserver = file_langserver + random_path = get_random_path('3', True) + random_uri = random_path.as_uri() + + request = { + 'method': 'textDocument/didSave', + 'params': { + 'textDocument': { + 'uri': random_uri, + }, + }, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + assert_callback_not_called(file) + + +@pytest.mark.parametrize('future_class', [ + DummyAlwaysCancelledFuture, + DummyAlwaysExceptionFuture]) +def test_langserver_did_open_future_cancelled(future_class, + monkeypatch, + verify_publish_respone): + with monkeypatch.context() as patch: + patch.setattr('coalals.concurrency.ProcessPoolExecutor', + DummyProcessPoolExecutor) + DummyProcessPoolExecutor.FutureClass = future_class + + file = TemporaryFile() + langserver = LangServer(file, file) + + code = None + code_path = url('failure2.py', True) + with open(str(code_path)) as code_file: + code = code_file.read() + + request = { + 'method': 'textDocument/didOpen', + 'params': { + 'textDocument': { + 'uri': code_path.as_uri(), + 'languageId': 'python', + 'version': 1, + 'text': code, + }, + }, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + assert_callback_not_called(file) + + +def test_langserver_did_save_failed_job(monkeypatch): + with monkeypatch.context() as patch: + patch.setattr('coalals.concurrency.ProcessPoolExecutor', + DummyProcessPoolExecutor) + DummyProcessPoolExecutor.on_submit = False + + file = TemporaryFile() + langserver = LangServer(file, file) + + code_sample_name = url('failure.py', True) + proxy = FileProxy(str(code_sample_name)) + langserver._proxy_map.add(proxy) + + request = { + 'method': 'textDocument/didSave', + 'params': { + 'textDocument': { + 'uri': code_sample_name.as_uri(), + }, + }, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + assert_callback_not_called(file) + + +@pytest.mark.parametrize('will_respond', [True, False]) +def test_langserver_document_formatting(will_respond, + file_langserver_deep, + verify_response): + file, langserver = file_langserver_deep + + code_sample_path = url('failure3.py', True) + code_sample_name = str(code_sample_path) + proxy = FileProxy(code_sample_name) + + # it should fail if the file is not open + # in the editor by extension is not in the + # proxy map. + if will_respond is True: + langserver._proxy_map.add(proxy) + + request = { + 'method': 'textDocument/formatting', + 'params': { + 'textDocument': { + 'uri': code_sample_path.as_uri(), + }, + + 'options': { + 'tabSize': 4, + 'insertSpaces': True, + }, + }, + 'jsonrpc': '2.0', + 'id': 1244, + } + + langserver._endpoint.consume(request) + + def consumer(file, response, passed): + if will_respond is False: + passed[0] = True + return + + elif 'id' in response: + for text_edit in response['result']: + assert text_edit['newText'] + passed[0] = True + + verify_response(file, langserver, consumer) + + +@pytest.mark.parametrize('name,add', [ + ('one', True), + ('two', False)]) +def test_langserver_did_close(file_langserver, name, add): + _, langserver = file_langserver + random_path = get_random_path(name, True) + random_uri = random_path.as_uri() + random_name = str(random_path) + + if add: + proxy = FileProxy(random_name) + langserver._proxy_map.add(proxy) + + request = { + 'method': 'textDocument/didClose', + 'params': { + 'textDocument': { + 'uri': random_uri, + }, + }, + 'jsonrpc': '2.0', + } + + langserver._endpoint.consume(request) + assert langserver._proxy_map.get(random_name) is None + + +def test_langserver_set_concurrency_params(monkeypatch): + with monkeypatch.context() as patch: + patch.setattr('coalals.concurrency.ProcessPoolExecutor', + DummyProcessPoolExecutor) + + file = TemporaryFile() + + assert LangServer.set_concurrency_params(3, 4) + langserver = LangServer(file, file) + + pool = langserver._coala._tracked_pool + assert pool._job_tracker._max_jobs == 3 + assert pool._process_pool._max_workers == 4 + + # Sticky concurrency configuration + langserver = LangServer(file, file) + + pool = langserver._coala._tracked_pool + assert pool._job_tracker._max_jobs == 3 + assert pool._process_pool._max_workers == 4 + + assert not LangServer.set_concurrency_params(0, 2) + assert not LangServer.set_concurrency_params(2, 0) diff --git a/tests/tests_coalals/test_main.py b/tests/tests_coalals/test_main.py new file mode 100644 index 0000000..ed4ca37 --- /dev/null +++ b/tests/tests_coalals/test_main.py @@ -0,0 +1,34 @@ +import pytest + +from coalals.langserver import LangServer +from coalals.main import (TCPServer, start_tcp_lang_server, + start_io_lang_server) +from helpers.dummies import DummyTCPServer + + +def test_start_tcp_server(monkeypatch): + with monkeypatch.context() as patch: + patch.setattr('coalals.main.TCPServer', DummyTCPServer) + + DummyTCPServer.panic = True + with pytest.raises(SystemExit): + start_tcp_lang_server(LangServer, '127.0.0.1', 4008) + + DummyTCPServer.panic = False + DummyTCPServer.keyboard_interrupt = True + with pytest.raises(SystemExit): + start_tcp_lang_server(LangServer, '127.0.0.1', 4074) + + DummyTCPServer.panic = False + DummyTCPServer.keyboard_interrupt = False + start_tcp_lang_server(LangServer, '127.0.0.1', 4008) + + assert DummyTCPServer.served is True + assert DummyTCPServer.closed is True + + +def test_start_io_server(): + del LangServer.start + + with pytest.raises(AttributeError): + start_io_lang_server(LangServer, None, None) diff --git a/tests/tests_coalals/tests_results/test_diagnostics.py b/tests/tests_coalals/tests_results/test_diagnostics.py new file mode 100644 index 0000000..7c34bf1 --- /dev/null +++ b/tests/tests_coalals/tests_results/test_diagnostics.py @@ -0,0 +1,71 @@ +import pytest +from json import load, dumps + +from coalals.utils.files import FileProxy +from coalals.results.diagnostics import Diagnostics +from coalals.results.fixes import coalaPatch, TextEdits +from helpers.resources import sample_diagnostics, url + + +class DummyDiff: + pass + + +def get_all_samples(): + with open(sample_diagnostics) as samples: + return load(samples)['samples'] + + +def get_all_fixes_samples(): + with open(sample_diagnostics) as samples: + return load(samples)['fixes'] + + +def test_diagnostics_init(): + diagnostics = Diagnostics(warnings=[{}]) + assert diagnostics.warnings() == [{}] + + +def test_diagnostics_fixes(): + diagnostics = Diagnostics(fixes=[('', DummyDiff())]) + assert len(diagnostics.fixes()) == 1 + + +@pytest.mark.parametrize('sample', get_all_samples()) +def test_from_coala_op_json(sample): + coala = sample['coala'] + exp_langserver = sample['langserver'] + + coala_json_op = dumps(coala) + gen_diags = Diagnostics.from_coala_json(coala_json_op) + assert gen_diags.warnings() == exp_langserver + + +@pytest.mark.parametrize('sample', get_all_fixes_samples()) +def test_fixes_load_from_coala_json(sample): + diags_fixes = Diagnostics.from_coala_json(dumps(sample)) + assert len(diags_fixes.fixes()) == 3 + + +def test_fixes_to_text_edits(): + failure3 = url('failure3.py') + + with open(failure3) as failure3_file: + failure3_content = failure3_file.read() + + # sample patch with that will always mismatch failure3 + # and raise an error when applied. + wronged = coalaPatch('--- \n+++ \n@@ -2,5 +2,4 @@\n hello = \"Hey\"' + '\n \n if Trues:\n- poss\n-\n+ pass\n') + + proxy = FileProxy(failure3) + patch = coalaPatch('') + fixes = Diagnostics(fixes=[ + (failure3, patch), ('random.py', patch), (failure3, wronged)]) + + text_edits = fixes.fixes_to_text_edits(proxy) + text_edits_list = list(text_edits.get()) + + assert isinstance(text_edits, TextEdits) + replace_text = text_edits_list[0]['newText'] + assert replace_text == failure3_content diff --git a/tests/tests_coalals/tests_results/test_fixes__TextEdits.py b/tests/tests_coalals/tests_results/test_fixes__TextEdits.py new file mode 100644 index 0000000..7331999 --- /dev/null +++ b/tests/tests_coalals/tests_results/test_fixes__TextEdits.py @@ -0,0 +1,68 @@ +import pytest + +from coalals.results.fixes import (TextEdit, + TextEdits) + + +@pytest.fixture +def sel_range(): + def _internal(sl, sc, el, ec): + return { + 'start': { + 'line': sl, + 'character': sc, + }, + + 'end': { + 'line': el, + 'character': ec, + }, + } + + return _internal + + +@pytest.fixture +def text_edit(sel_range): + sel_range = sel_range(0, 10, 3, 10) + new_text = 'Sample TextEdit!' + + return TextEdit(sel_range, new_text) + + +def test_text_edit_init(sel_range): + sel_range = sel_range(0, 10, 3, 10) + new_text = 'Sample TextEdit!' + + te_entity = TextEdit(sel_range, new_text) + assert te_entity._text_edit['range'] == sel_range + assert te_entity._text_edit['newText'] == new_text + + +def test_text_edit_get(sel_range): + sel_range = sel_range(0, 10, 3, 10) + new_text = 'Sample TextEdit!' + + te_entity = TextEdit(sel_range, new_text) + assert te_entity.get() == { + 'range': sel_range, + 'newText': new_text, + } + + +def test_text_edits_add(text_edit): + text_edits = TextEdits() + + text_edits.add(text_edit) + text_edits.add(text_edit) + + assert len(text_edits._edits) == 2 + + +def test_text_edits_get(text_edit): + text_edits = TextEdits() + + text_edits.add(text_edit) + text_edits.add(text_edit) + + assert list(text_edits.get()) diff --git a/tests/tests_coalals/tests_results/test_fixes__coalaPatch.py b/tests/tests_coalals/tests_results/test_fixes__coalaPatch.py new file mode 100644 index 0000000..30a25d7 --- /dev/null +++ b/tests/tests_coalals/tests_results/test_fixes__coalaPatch.py @@ -0,0 +1,65 @@ +import pytest +from json import loads +from types import GeneratorType + +from coalals.results.fixes import coalaPatch +from helpers.resources import sample_fixes_file + + +@pytest.fixture +def sample_fixes(): + with open(sample_fixes_file) as sample: + return loads(sample.read()) + + +def test_coalaPatch_join(): + sample = ['Sample', 'String'] + joined = coalaPatch.join_parts(sample) + + # assuming that the splitlines() behaves + # as required and does split by newlines. + assert list(joined.splitlines()) == sample + + # TODO Add platform dependent newline combins. + + +def test_coalaPatch_init(sample_fixes): + for fix in sample_fixes['diffs']: + coalaPatch(fix['diff']) + + +def test_coalaPatch_wp_parsed(sample_fixes): + for fix in sample_fixes['diffs']: + coa = coalaPatch(fix['diff']) + + # should not raise any issue + assert isinstance(coa.wp_parsed(), GeneratorType) + + +def test_coalaPatch_apply(sample_fixes): + for l, fix in enumerate(sample_fixes['diffs']): + orig, patched = fix['original'], fix['patched'] + diff = fix['diff'] + + coa = coalaPatch(diff) + parsed_patch = None + + if l % 2 == 1: + parsed_patch = coa.wp_parsed() + + modified = coa.apply(orig, parsed_patch) + assert modified == patched + + +def test_coalaPatch_apply_diff(sample_fixes): + for fix in sample_fixes['diffs']: + orig, patched = fix['original'], fix['patched'] + text_diff = fix['diff'] + + coa = coalaPatch(text_diff) + + patches = coa.wp_parsed() + diff = list(patches)[0] + + modified = coa.apply_diff(diff, orig) + assert coalaPatch.join_parts(modified) == patched diff --git a/tests/tests_coalals/tests_utils/test_files__FileProxy.py b/tests/tests_coalals/tests_utils/test_files__FileProxy.py new file mode 100644 index 0000000..7d2b70d --- /dev/null +++ b/tests/tests_coalals/tests_utils/test_files__FileProxy.py @@ -0,0 +1,102 @@ +import pytest +from os.path import relpath +from tempfile import NamedTemporaryFile + +from coalals.utils.files import FileProxy +from helpers.resources import url +from helpers.utils import get_random_path + + +@pytest.fixture +def temporary_file(): + temp = NamedTemporaryFile(delete=False) + temp.write('coala'.encode('utf8')) + temp.close() + + return temp.name + + +@pytest.fixture +def empty_fileproxy(): + random_path = get_random_path('1') + return FileProxy(random_path) + + +def test_fileproxy_relative_name(): + failure_path = url('failure.py') + rel = relpath(failure_path, __file__) + # FIXME __file__ is unreliable here + + with pytest.raises(Exception): + FileProxy(rel) + + +def test_fileproxy_init(empty_fileproxy): + assert empty_fileproxy.version == -1 + assert empty_fileproxy.contents() == '' + assert empty_fileproxy.workspace is None + + +def test_fileproxy_str(empty_fileproxy): + gen_str = ''.format( + empty_fileproxy.filename, empty_fileproxy.version) + assert gen_str == str(empty_fileproxy) + + +def test_fileproxy_from_name(temporary_file): + fileproxy = FileProxy.from_name(temporary_file, '.') + + assert fileproxy.version == -1 + assert fileproxy.workspace == '.' + assert fileproxy.contents() == 'coala' + assert fileproxy.filename == temporary_file + + +def test_file_from_name_missing_file(): + random_path = get_random_path('5', py=False) + + with pytest.raises(FileNotFoundError): + FileProxy.from_name(random_path, '.') + + +def test_fileproxy_close(empty_fileproxy): + empty_fileproxy.close() + assert empty_fileproxy.contents() == '' + + +def test_fileproxy_replace(temporary_file): + fileproxy = FileProxy.from_name(temporary_file, '.') + + assert fileproxy.version == -1 + assert fileproxy.contents() == 'coala' + + assert fileproxy.replace('coala-rocks', 1) + assert fileproxy.contents() == 'coala-rocks' + + assert not fileproxy.replace('coala-mountains', 1) + assert fileproxy.contents() == 'coala-rocks' + + assert not fileproxy.replace('coala-mountains', 0) + assert fileproxy.contents() == 'coala-rocks' + + +def test_fileproxy_update(temporary_file): + class Diff: + pass + + fileproxy = FileProxy.from_name(temporary_file, '.') + one, two, three = Diff(), Diff(), Diff() + assert fileproxy._changes_history == [] + + fileproxy.update(one) + assert fileproxy._changes_history == [one] + + fileproxy.update([two, three]) + assert fileproxy._changes_history == [one, two, three] + + +def test_fileproxy_get_disk_contents(temporary_file): + proxy = FileProxy(temporary_file) + + contents = proxy.get_disk_contents() + assert contents == 'coala' diff --git a/tests/tests_coalals/tests_utils/test_files__FileProxyMap.py b/tests/tests_coalals/tests_utils/test_files__FileProxyMap.py new file mode 100644 index 0000000..297671d --- /dev/null +++ b/tests/tests_coalals/tests_utils/test_files__FileProxyMap.py @@ -0,0 +1,109 @@ +import pytest +from pathlib import Path +from random import randint +from tempfile import NamedTemporaryFile + +from coalals.utils.files import FileProxy, FileProxyMap +from helpers.utils import get_random_path + + +@pytest.fixture +def random_filename(): + def _internal(): + rand_no = randint(1, 9999) + return get_random_path(rand_no) + return _internal + + +@pytest.fixture +def random_fileproxy(random_filename): + def _internal(): + random = random_filename() + return FileProxy(random) + return _internal + + +@pytest.fixture +def empty_proxymap(random_filename): + return FileProxyMap() + + +def test_proxymap_empty(empty_proxymap): + assert empty_proxymap._map == {} + + +def test_proxymap_add(empty_proxymap, random_fileproxy): + assert empty_proxymap.add(123) is False + assert empty_proxymap.add('coala') is False + + proxy_one = random_fileproxy() + assert empty_proxymap.add(proxy_one) is True + assert empty_proxymap._map == {proxy_one.filename: proxy_one} + + proxy_two = FileProxy(proxy_one.filename, '.', 'coala-rocks') + assert empty_proxymap.add(proxy_two, replace=False) is False + assert empty_proxymap.add(proxy_two, replace=True) is True + + added_proxy = empty_proxymap._map[proxy_two.filename] + assert added_proxy.contents() == 'coala-rocks' + + +def test_proxymap_remove(empty_proxymap, random_fileproxy, random_filename): + random = random_fileproxy() + empty_proxymap.add(random) + + assert len(empty_proxymap._map) == 1 + empty_proxymap.remove(random.filename) + assert len(empty_proxymap._map) == 0 + + search_for = random_filename() + assert empty_proxymap.remove(search_for) is None + + +def test_proxymap_get(empty_proxymap, random_fileproxy, random_filename): + search_for = random_filename() + assert empty_proxymap.get(search_for) is None + + random = random_fileproxy() + empty_proxymap.add(random) + assert empty_proxymap.get(random.filename) == random + + +def test_proxymap_resolve_finds(empty_proxymap, random_fileproxy): + random = random_fileproxy() + + empty_proxymap.add(random) + assert empty_proxymap.resolve(random.filename) == random + + +def test_proxymap_resolve_creates(empty_proxymap): + file = NamedTemporaryFile(delete=False) + file.write('coala-rocks'.encode('utf-8')) + file.close() + + proxy = empty_proxymap.resolve(file.name) + assert proxy.contents() == 'coala-rocks' + + +def test_proxymap_resolve_not_finds_hard(empty_proxymap, random_filename): + filename = random_filename() + assert empty_proxymap.resolve(filename, hard_sync=True) is False + + +def test_proxymap_resolve_create_soft_err(empty_proxymap): + random_path = get_random_path('1', True) + relative = random_path.relative_to(Path.cwd()) + + assert empty_proxymap.resolve(str(relative), hard_sync=False) is False + + +def test_proxymap_resolve_not_finds_soft(empty_proxymap, random_fileproxy): + random = random_fileproxy() + + proxy = empty_proxymap.resolve(random.filename, random.workspace, + hard_sync=False) + + assert proxy.filename == random.filename + assert proxy.workspace == random.workspace + assert proxy.contents() == '' + assert proxy.version == -1 diff --git a/tests/tests_coalals/tests_utils/test_files__UriUtils.py b/tests/tests_coalals/tests_utils/test_files__UriUtils.py new file mode 100644 index 0000000..4198eb2 --- /dev/null +++ b/tests/tests_coalals/tests_utils/test_files__UriUtils.py @@ -0,0 +1,47 @@ +import pytest +from pathlib import Path + +from coalals.utils.files import UriUtils +from helpers.utils import get_random_path + + +@pytest.fixture +def valid_uri(): + path = get_random_path('1', True) + return path.as_uri() + + +@pytest.fixture +def valid_path(): + return get_random_path('1') + + +@pytest.fixture +def valid_dir(): + path = get_random_path('1', True) + return str(path.parent) + + +def test_path_from_uri_with_uri(valid_uri, valid_path): + gen_path = UriUtils.path_from_uri(valid_uri) + assert gen_path == valid_path + + +def test_path_from_uri_with_path(valid_path): + gen_path = UriUtils.path_from_uri(valid_path) + assert gen_path == valid_path + + +def test_dir_from_uri_with_uri(valid_uri, valid_dir): + gen_dir = UriUtils.dir_from_uri(valid_uri) + assert gen_dir == valid_dir + + +def test_dir_from_uri_with_path(valid_path, valid_dir): + gen_dir = UriUtils.dir_from_uri(valid_path) + assert gen_dir == valid_dir + + +def test_file_to_uri(valid_path, valid_uri): + gen_uri = UriUtils.file_to_uri(valid_path) + assert gen_uri == valid_uri diff --git a/tests/tests_coalals/tests_utils/test_wrappers.py b/tests/tests_coalals/tests_utils/test_wrappers.py new file mode 100644 index 0000000..9dac54a --- /dev/null +++ b/tests/tests_coalals/tests_utils/test_wrappers.py @@ -0,0 +1,55 @@ +import pytest +from tempfile import TemporaryFile + +from helpers.dummies import DummyLangServer +from coalals.utils.wrappers import func_wrapper, StreamHandlerWrapper + + +class DummyStreamRequestHandler: + + rfile = None + wfile = None + + def __init__(self, *args, **kargs): + pass + + def setup(self): + pass + + +def test_func_wrapper(): + def _sum(*args): + return sum(args) + + def _act(*args, _max=True): + if _max: + return max(args) + return min(args) + + assert func_wrapper(_sum, 1, 2) == 3 + assert func_wrapper(_act, 5, 6, 8, _max=True) == 8 + + +def test_streamhandler(): + bases = StreamHandlerWrapper.__bases__ + StreamHandlerWrapper.__bases__ = (DummyStreamRequestHandler,) + + wrapped_type = type( + 'DummyWrapped', + (StreamHandlerWrapper,), + {'DELEGATE_CLASS': DummyLangServer, }) + + instance = wrapped_type() + + rtemp, wtemp = TemporaryFile(), TemporaryFile() + DummyStreamRequestHandler.rfile = rtemp + DummyStreamRequestHandler.wfile = wtemp + + instance.setup() + assert instance.delegate.f_rfile == rtemp + assert instance.delegate.f_wfile == wtemp + + instance.handle() + assert instance.delegate.started is True + + StreamHandlerWrapper.__bases__ = bases diff --git a/vscode-client/.gitignore b/vscode-client/.gitignore deleted file mode 100644 index faeedd6..0000000 --- a/vscode-client/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -out -server -node_modules -.vscode-dev -.vscode-test - diff --git a/vscode-client/.vscode/launch.json b/vscode-client/.vscode/launch.json deleted file mode 100644 index d60c89d..0000000 --- a/vscode-client/.vscode/launch.json +++ /dev/null @@ -1,28 +0,0 @@ -// A launch configuration that compiles the extension and then opens it inside a new window -{ - "version": "0.1.0", - "configurations": [ - { - "name": "Launch Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/out/src", - "preLaunchTask": "npm" - }, - { - "name": "Launch Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/out/test", - "preLaunchTask": "npm" - } - ] -} \ No newline at end of file diff --git a/vscode-client/.vscode/settings.json b/vscode-client/.vscode/settings.json deleted file mode 100644 index 8ab1aa8..0000000 --- a/vscode-client/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -// Place your settings in this file to overwrite default and user settings. -{ - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - "typescript.tsdk": "/dev/null", - "vsicons.presets.angular": false -} \ No newline at end of file diff --git a/vscode-client/.vscode/tasks.json b/vscode-client/.vscode/tasks.json deleted file mode 100644 index 1992757..0000000 --- a/vscode-client/.vscode/tasks.json +++ /dev/null @@ -1,30 +0,0 @@ -// Available variables which can be used inside of strings. -// ${workspaceRoot}: the root folder of the team -// ${file}: the current opened file -// ${fileBasename}: the current opened file's basename -// ${fileDirname}: the current opened file's dirname -// ${fileExtname}: the current opened file's extension -// ${cwd}: the current working directory of the spawned process - -// A task runner that calls a custom npm script that compiles the extension. -{ - "version": "0.1.0", - - // we want to run npm - "command": "npm", - - // the command is a shell script - "isShellCommand": true, - - // show the output window only if unrecognized errors occur. - "showOutput": "silent", - - // we run the custom script "compile" as defined in package.json - "args": ["run", "compile", "--loglevel", "silent"], - - // The tsc compiler is started in watching mode - "isWatching": true, - - // use the standard tsc in watch mode problem matcher to find compile problems in the output. - "problemMatcher": "$tsc-watch" -} \ No newline at end of file diff --git a/vscode-client/.vscodeignore b/vscode-client/.vscodeignore deleted file mode 100644 index 795e714..0000000 --- a/vscode-client/.vscodeignore +++ /dev/null @@ -1,9 +0,0 @@ -.vscode/** -typings/** -out/test/** -test/** -src/** -**/*.map -.gitignore -tsconfig.json -vsc-extension-quickstart.md diff --git a/vscode-client/License.txt b/vscode-client/License.txt deleted file mode 100644 index 8e1db29..0000000 --- a/vscode-client/License.txt +++ /dev/null @@ -1,11 +0,0 @@ -Copyright (c) Microsoft Corporation - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vscode-client/README-vscode-client.md b/vscode-client/README-vscode-client.md deleted file mode 100644 index cc8adfe..0000000 --- a/vscode-client/README-vscode-client.md +++ /dev/null @@ -1,34 +0,0 @@ -# vscode-client - -The vscode-client extension for Visual Studio Code helps you develop -and debug language servers. It lets you run multiple language servers -at once with minimal extra configuration per language. - -## Using this extension - -1. Follow the [Getting Started instructions for this project](../README.md) -1. Run `npm install`. -1. Run `npm run vscode` to start a new VSCode instance. Use `npm run vscode -- /path/to/mydir` to open the editor to a specific directory. -1. Open a `.go` file and hover over text to start using the Go language server. - -To view a language server's stderr output in VSCode, select View → Output. -To debug further, see the "Hacking on this extension" section below. - -After updating the binary for a language server (during development or after an upgrade), just kill the process (e.g., `killall langserver-go`). -VSCode will automatically restart and reconnect to the language server process. - -> **Note for those who use VSCode as their primary editor:** Because this extension's functionality conflicts with other VSCode extensions -(e.g., showing Go hover information), the `npm run vscode` script launches an separate instance of VSCode and stores its config in `../.vscode-dev`. -It will still show your existing extensions in the panel (which seems to be a VSCode bug), but they won't be activated. - -## Adding a language server - -Register your language server at the bottom of [`extension.ts`](https://github.com/sourcegraph/langserver/blob/master/vscode-client/src/extension.ts). - -## Hacking on this extension - -1. Run `npm install` in this directory (`vscode-client`). -1. Open this directory by itself in Visual Studio Code. -1. Hit F5 to open a new VSCode instance in a debugger running this extension. (This is equivalent to going to the Debug pane on the left and running the "Launch Extension" task.) - -See the [Node.js example language server tutorial](https://code.visualstudio.com/docs/extensions/example-language-server) under "To test the language server" for more information. diff --git a/vscode-client/README.md b/vscode-client/README.md deleted file mode 100644 index b6bcd03..0000000 --- a/vscode-client/README.md +++ /dev/null @@ -1,20 +0,0 @@ -

-
- logo -
- VS Code - coala -
-
-

- -

Provides a unified interface for linting and fixing all your code, regardless of the programming languages you use.

- -![Demo](https://raw.githubusercontent.com/coala/coala-vs-code/master/docs/images/demo.gif) - -## Contributing - -Welcome to our community in [gitter](https://coala.io/chat). And feel free to contribute to [github.com/coala/coala-vs-code](https://github.com/coala/coala-vs-code). - -## AUTHORS - -* [gaocegege](https://github.com/gaocegege) diff --git a/vscode-client/ThirdPartyNotices.txt b/vscode-client/ThirdPartyNotices.txt deleted file mode 100644 index 114129b..0000000 --- a/vscode-client/ThirdPartyNotices.txt +++ /dev/null @@ -1,31 +0,0 @@ -THIRD-PARTY SOFTWARE NOTICES AND INFORMATION -For Microsoft vscode-languageserver-node-example - -This project incorporates material from the project(s) listed below (collectively, “Third Party Code”). -Microsoft is not the original author of the Third Party Code. The original copyright notice and license -under which Microsoft received such Third Party Code are set out below. This Third Party Code is licensed -to you under their original license terms set forth below. Microsoft reserves all other rights not expressly -granted, whether by implication, estoppel or otherwise. - -1. DefinitelyTyped version 0.0.1 (https://github.com/borisyankov/DefinitelyTyped) - -This project is licensed under the MIT license. -Copyrights are respective of each contributor listed at the beginning of each definition file. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/vscode-client/package.json b/vscode-client/package.json deleted file mode 100644 index b8bfbc6..0000000 --- a/vscode-client/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "coala", - "displayName": "coala plugin for VS Code", - "description": "Provides a unified interface for linting and fixing all your code, regardless of the programming languages you use.", - "author": "coala devs", - "license": "MIT", - "version": "0.0.10", - "publisher": "coala", - "engines": { - "vscode": "^1.4.0" - }, - "categories": [ - "Linters" - ], - "activationEvents": [ - "*" - ], - "main": "./out/src/extension", - "scripts": { - "vscode:prepublish": "cp -r ../coala-langserver.sh ../coala-langserver.py ../coala_langserver ./out && node ./node_modules/vscode/bin/compile", - "compile": "python3 ../setup.py install && node ./node_modules/vscode/bin/compile -watch -p ./", - "postinstall": "node ./node_modules/vscode/bin/install", - "vscode": "npm run vscode:prepublish && VSCODE=$(which code-insiders || which code || echo echo ERROR: neither the code nor code-insiders vscode executable is installed); USER=dummy-dont-share-vscode-instance $VSCODE --user-data-dir=$PWD/.vscode-dev/user-data --extensionHomePath=$PWD/.vscode-dev/extensions --extensionDevelopmentPath=$PWD $*", - "test": "node ./node_modules/vscode/bin/test" - }, - "devDependencies": { - "typescript": "^1.8.9", - "vscode": "^0.11.0", - "vsce": "^1.18.0" - }, - "dependencies": { - "vscode-languageclient": "^2.4.2-next.12" - } -} diff --git a/vscode-client/src/extension.ts b/vscode-client/src/extension.ts deleted file mode 100644 index 1254bcc..0000000 --- a/vscode-client/src/extension.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import * as net from 'net'; - -import { workspace, Disposable, ExtensionContext } from 'vscode'; -import { LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, ErrorAction, ErrorHandler, CloseAction, TransportKind } from 'vscode-languageclient'; - -function startLangServer(command: string, documentSelector: string | string[]): Disposable { - const serverOptions: ServerOptions = { - command: command, - }; - const clientOptions: LanguageClientOptions = { - documentSelector: documentSelector, - } - return new LanguageClient(command, serverOptions, clientOptions).start(); -} - -function startLangServerTCP(addr: number, documentSelector: string | string[]): Disposable { - const serverOptions: ServerOptions = function() { - return new Promise((resolve, reject) => { - var client = new net.Socket(); - client.connect(addr, "127.0.0.1", function() { - resolve({ - reader: client, - writer: client - }); - }); - }); - } - - const clientOptions: LanguageClientOptions = { - documentSelector: documentSelector, - synchronize: { - // Notify the server about file changes to '.clientrc files contain in the workspace - fileEvents: workspace.createFileSystemWatcher('**/.py') - } - } - return new LanguageClient(`tcp lang server (port ${addr})`, serverOptions, clientOptions).start(); -} - -export function activate(context: ExtensionContext) { - context.subscriptions.push(startLangServer - (require("path").resolve(__dirname, '../coala-langserver.sh'), ["python"])); - // For Debug - // context.subscriptions.push(startLangServerTCP(2087, ["python"])); - console.log("coala language server is running."); -} diff --git a/vscode-client/test/extension.test.js b/vscode-client/test/extension.test.js deleted file mode 100644 index a98fd02..0000000 --- a/vscode-client/test/extension.test.js +++ /dev/null @@ -1,17 +0,0 @@ -// The module 'assert' provides assertion methods from node -import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -// import * as vscode from 'vscode'; -// import * as myExtension from '../extension'; - -// Defines a Mocha test suite to group tests of similar kind together -suite('Extension Tests', () => { - - // Defines a Mocha unit test - test('Something 1', () => { - assert.equal(-1, [1, 2, 3].indexOf(5)); - assert.equal(-1, [1, 2, 3].indexOf(0)); - }); -}); diff --git a/vscode-client/test/index.ts b/vscode-client/test/index.ts deleted file mode 100644 index 9cd3b60..0000000 --- a/vscode-client/test/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -// -// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING -// -// This file is providing the test runner to use when running extension tests. -// By default the test runner in use is Mocha based. -// -// You can provide your own test runner if you want to override it by exporting -// a function run(testRoot: string, clb: (error:Error) => void) that the extension -// host can call to run the tests. The test runner is expected to use console.log -// to report the results back to the caller. When the tests are finished, return -// a possible error to the callback or null if none. - -var testRunner = require('vscode/lib/testrunner'); - -// You can directly control Mocha options by uncommenting the following lines -// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info -testRunner.configure({ - ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - useColors: true // colored output from test results -}); - -module.exports = testRunner; diff --git a/vscode-client/tsconfig.json b/vscode-client/tsconfig.json deleted file mode 100644 index 813049f..0000000 --- a/vscode-client/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ES5", - "module": "commonjs", - "moduleResolution": "node", - "outDir": "out", - "noLib": true, - "sourceMap": true - }, - "exclude": [ - "node_modules", - "server" - ] -} \ No newline at end of file diff --git a/vscode-client/typings/node.d.ts b/vscode-client/typings/node.d.ts deleted file mode 100644 index 5ed7730..0000000 --- a/vscode-client/typings/node.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/vscode-client/typings/vscode-typings.d.ts b/vscode-client/typings/vscode-typings.d.ts deleted file mode 100644 index 61430b1..0000000 --- a/vscode-client/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -///