diff --git a/event_listeners.py b/event_listeners.py index d9091f4..f755b01 100644 --- a/event_listeners.py +++ b/event_listeners.py @@ -10,8 +10,7 @@ from req import Req from win import Win from stack_ide_manager import StackIDEManager, send_request -import response as res - +from response import parse_autocompletions class StackIDESaveListener(sublime_plugin.EventListener): """ @@ -58,6 +57,8 @@ class StackIDEAutocompleteHandler(sublime_plugin.EventListener): def __init__(self): super(StackIDEAutocompleteHandler, self).__init__() self.returned_completions = [] + self.view = None + self.refreshing = False def on_query_completions(self, view, prefix, locations): @@ -67,16 +68,16 @@ def on_query_completions(self, view, prefix, locations): window = view.window() if not StackIDEManager.is_running(window): return - # Check if this completion query is due to our refreshing the completions list # after receiving a response from stack-ide, and if so, don't send # another request for completions. - if not view.settings().get("refreshing_auto_complete"): + if not self.refreshing: + self.view = view request = Req.get_autocompletion(filepath=relative_view_file_name(view),prefix=prefix) - send_request(window, request, Win(window).update_completions) + send_request(window, request, self._handle_response) - # Clear the flag to uninhibit future completion queries - view.settings().set("refreshing_auto_complete", False) + # Clear the flag to allow future completion queries + self.refreshing = False return list(self.format_completion(*completion) for completion in self.returned_completions) @@ -86,42 +87,17 @@ def format_completion(self, prop, scope): scope.importedFrom.module if scope else ''), prop.name] + def _handle_response(self, response): + self.returned_completions = list(parse_autocompletions(response)) + self.view.run_command('hide_auto_complete') + sublime.set_timeout(self.run_auto_complete, 0) - def on_window_command(self, window, command_name, args): - """ - Implements a hacky way of returning data to the StackIDEAutocompleteHandler instance, - wherein SendStackIDERequestCommand calls a update_completions command on the window, - which is really just a dummy command that we intercept here in order to assign the resulting - completions to returned_completions to then, finally, return the next time on_query_completions - is called. - """ - if not StackIDEManager.is_running(window): - return - if args == None: - return None - completions = args.get("completions") - if command_name == "update_completions" and completions: - # Log.debug("INTERCEPTED:\n " + str(completions) + "\n") - self.returned_completions = list(res.parse_autocompletions(completions)) - - # Hide the auto_complete popup so we can reopen it, - # triggering a new on_query_completions - # call to pickup our new self.returned_completions. - window.active_view().run_command('hide_auto_complete') - - def reactivate(): - # We read this in on_query_completions to prevent sending a duplicate - # request for completions when we're only trying to re-trigger the completions - # popup; otherwise we get an infinite loop of - # autocomplete > request completions > receive response > close/reopen to refresh - # > autocomplete > request completions > etc. - window.active_view().settings().set("refreshing_auto_complete", True) - window.active_view().run_command('auto_complete', { - 'disable_auto_insert': True, - # 'api_completions_only': True, - 'next_competion_if_showing': False - }) - # Wait one runloop before reactivating, to give the hide command a chance to finish - sublime.set_timeout(reactivate, 0) - return None + def run_auto_complete(self): + self.refreshing = True + self.view.run_command("auto_complete", { + 'disable_auto_insert': True, + # 'api_completions_only': True, + 'next_completion_if_showing': False, + # 'auto_complete_commit_on_tab': True, + }) diff --git a/stack_ide.py b/stack_ide.py index f684a46..de5fc0e 100644 --- a/stack_ide.py +++ b/stack_ide.py @@ -34,21 +34,23 @@ def __init__(self, window, settings, backend=None): self.is_alive = True self.is_active = False self.process = None - folder = first_folder(window) - (project_in, project_name) = os.path.split(folder) + self.project_path = first_folder(window) + (project_in, project_name) = os.path.split(self.project_path) self.project_name = project_name - self.alt_env = os.environ.copy() - add_to_PATH = settings.add_to_PATH - if len(add_to_PATH) > 0: - self.alt_env["PATH"] = os.pathsep.join(add_to_PATH + [self.alt_env.get("PATH","")]) + reset_env(settings.add_to_PATH) if backend is None: - self._backend = boot_ide_backend(folder, self.alt_env, self.handle_response) - else: + self._backend = stack_ide_start(self.project_path, self.project_name, self.handle_response) + else: # for testing self._backend = backend + self._backend.handler = self.handle_response + self.is_active = True self.include_targets = set() + + # TODO: could check packages here to fix the 'project_dir must equal packagename issue' + sublime.set_timeout_async(self.load_initial_targets, 0) @@ -72,14 +74,7 @@ def load_initial_targets(self): """ Get the initial list of files to check """ - - proc = subprocess.Popen(["stack", "ide", "load-targets", self.project_name], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=first_folder(self.window), env=self.alt_env, - universal_newlines=True, - creationflags=CREATE_NO_WINDOW) - outs, errs = proc.communicate() - initial_targets = outs.splitlines() + initial_targets = stack_ide_loadtargets(self.project_path, self.project_name) sublime.set_timeout(lambda: self.update_files(initial_targets), 0) @@ -118,37 +113,51 @@ def handle_response(self, data): seq_id = data.get("seq") if seq_id is not None: - handler = self.conts.get(seq_id) - del self.conts[seq_id] - if handler is not None: - if contents is not None: - sublime.set_timeout(lambda:handler(contents), 0) - else: - Log.warning("Handler not found for seq", seq_id) - - # Check that stack-ide talks a version of the protocal we understand + self._send_to_handler(contents, seq_id) + elif tag == "ResponseWelcome": - expected_version = (0,1,1) - version_got = tuple(contents) if type(contents) is list else contents - if expected_version > version_got: - Log.error("Old stack-ide protocol:", version_got, '\n', 'Want version:', expected_version) - complain("wrong-stack-ide-version", - "Please upgrade stack-ide to a newer version.") - elif expected_version < version_got: - Log.warning("stack-ide protocol may have changed:", version_got) - else: - Log.debug("stack-ide protocol version:", version_got) - - # Pass progress messages to the status bar + self._handle_welcome(contents) + elif tag == "ResponseUpdateSession": self._handle_update_session(contents) + elif tag == "ResponseShutdownSession": + Log.debug("Stack-ide process has shut down") + elif tag == "ResponseLog": Log.debug(contents.rstrip()) else: Log.normal("Unhandled response: ", data) + def _send_to_handler(self, contents, seq_id): + """ + Looks up a previously registered handler for the incoming response + """ + handler = self.conts.get(seq_id) + del self.conts[seq_id] + if handler is not None: + if contents is not None: + sublime.set_timeout(lambda:handler(contents), 0) + else: + Log.warning("Handler not found for seq", seq_id) + + + def _handle_welcome(self, welcome): + """ + Identifies if we support the current version of the stack ide api + """ + expected_version = (0,1,1) + version_got = tuple(welcome) if type(welcome) is list else welcome + if expected_version > version_got: + Log.error("Old stack-ide protocol:", version_got, '\n', 'Want version:', expected_version) + complain("wrong-stack-ide-version", + "Please upgrade stack-ide to a newer version.") + elif expected_version < version_got: + Log.warning("stack-ide protocol may have changed:", version_got) + else: + Log.debug("stack-ide protocol version:", version_got) + def _handle_update_session(self, update_session): """ @@ -169,19 +178,48 @@ def __del__(self): finally: self.process = None -def boot_ide_backend(folder, env, response_handler): +env = {} + +def reset_env(add_to_PATH): + global env + env = os.environ.copy() + if len(add_to_PATH) > 0: + env["PATH"] = os.pathsep.join(add_to_PATH + [env.get("PATH","")]) + + +def stack_ide_packages(project_path): + proc = subprocess.Popen(["stack", "ide", "packages"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + cwd=project_path, env=env, + universal_newlines=True, + creationflags=CREATE_NO_WINDOW) + outs, errs = proc.communicate() + return outs.splitlines() + + +def stack_ide_loadtargets(project_path, package): + + Log.debug("Requesting load targets for ", package) + proc = subprocess.Popen(["stack", "ide", "load-targets", package], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + cwd=project_path, env=env, + universal_newlines=True, + creationflags=CREATE_NO_WINDOW) + outs, errs = proc.communicate() + # TODO: check response! + return outs.splitlines() + + +def stack_ide_start(project_path, package, response_handler): """ Start up a stack-ide subprocess for the window, and a thread to consume its stdout. """ - # Assumes the library target name is the same as the project dir - (project_in, project_name) = os.path.split(folder) - - Log.debug("Calling stack with PATH:", env['PATH'] if env else os.environ['PATH']) + Log.debug("Calling stack ide start with PATH:", env['PATH'] if env else os.environ['PATH']) - process = subprocess.Popen(["stack", "ide", "start", project_name], + process = subprocess.Popen(["stack", "ide", "start", package], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=folder, env=env, + cwd=project_path, env=env, creationflags=CREATE_NO_WINDOW ) diff --git a/test/data.py b/test/data.py new file mode 100644 index 0000000..63b3804 --- /dev/null +++ b/test/data.py @@ -0,0 +1,20 @@ +from settings import Settings + +type_info = "FilePath -> IO String" +span = { + "spanFromLine": 1, + "spanFromColumn": 1, + "spanToLine": 1, + "spanToColumn": 5 +} +exp_types_response = {"tag": "", "contents": [[type_info, span]]} +someFunc_span_info = {'contents': [[{'contents': {'idProp': {'idDefinedIn': {'moduleName': 'Lib', 'modulePackage': {'packageVersion': None, 'packageName': 'main', 'packageKey': 'main'}}, 'idSpace': 'VarName', 'idType': 'IO ()', 'idDefSpan': {'contents': {'spanFromLine': 9, 'spanFromColumn': 1, 'spanToColumn': 9, 'spanFilePath': 'src/Lib.hs', 'spanToLine': 9}, 'tag': 'ProperSpan'}, 'idName': 'someFunc', 'idHomeModule': None}, 'idScope': {'idImportQual': '', 'idImportedFrom': {'moduleName': 'Lib', 'modulePackage': {'packageVersion': None, 'packageName': 'main', 'packageKey': 'main'}}, 'idImportSpan': {'contents': {'spanFromLine': 3, 'spanFromColumn': 1, 'spanToColumn': 11, 'spanFilePath': 'app/Main.hs', 'spanToLine': 3}, 'tag': 'ProperSpan'}, 'tag': 'Imported'}}, 'tag': 'SpanId'}, {'spanFromLine': 7, 'spanFromColumn': 27, 'spanToColumn': 35, 'spanFilePath': 'app/Main.hs', 'spanToLine': 7}]], 'seq': '724752c9-a7bf-4658-834a-3ff7df64e7e5', 'tag': 'ResponseGetSpanInfo'} +putStrLn_span_info = {'contents': [[{'contents': {'idProp': {'idDefinedIn': {'moduleName': 'System.IO', 'modulePackage': {'packageVersion': '4.8.1.0', 'packageName': 'base', 'packageKey': 'base'}}, 'idSpace': 'VarName', 'idType': 'String -> IO ()', 'idDefSpan': {'contents': '', 'tag': 'TextSpan'}, 'idName': 'putStrLn', 'idHomeModule': {'moduleName': 'System.IO', 'modulePackage': {'packageVersion': '4.8.1.0', 'packageName': 'base', 'packageKey': 'base'}}}, 'idScope': {'idImportQual': '', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageVersion': '4.8.1.0', 'packageName': 'base', 'packageKey': 'base'}}, 'idImportSpan': {'contents': {'spanFromLine': 1, 'spanFromColumn': 8, 'spanToColumn': 12, 'spanFilePath': 'app/Main.hs', 'spanToLine': 1}, 'tag': 'ProperSpan'}, 'tag': 'Imported'}}, 'tag': 'SpanId'}, {'spanFromLine': 7, 'spanFromColumn': 41, 'spanToColumn': 49, 'spanFilePath': 'app/Main.hs', 'spanToLine': 7}]], 'seq': '6ee8d949-82bd-491d-8b79-ffcaa3e65fde', 'tag': 'ResponseGetSpanInfo'} +test_settings = Settings("none", [], False) +source_errors = {'seq': 'd0599c00-0b77-441c-8947-b3882cab298c', 'tag': 'ResponseGetSourceErrors', 'contents': [{'errorSpan': {'tag': 'ProperSpan', 'contents': {'spanFromColumn': 22, 'spanFromLine': 11, 'spanFilePath': 'src/Lib.hs', 'spanToColumn': 28, 'spanToLine': 11}}, 'errorKind': 'KindError', 'errorMsg': 'Couldn\'t match expected type ‘Integer’ with actual type ‘[Char]’\nIn the first argument of ‘greet’, namely ‘"You!"’\nIn the second argument of ‘($)’, namely ‘greet "You!"’\nIn a stmt of a \'do\' block: putStrLn $ greet "You!"'}, {'errorSpan': {'tag': 'ProperSpan', 'contents': {'spanFromColumn': 24, 'spanFromLine': 15, 'spanFilePath': 'src/Lib.hs', 'spanToColumn': 25, 'spanToLine': 15}}, 'errorKind': 'KindError', 'errorMsg': 'Couldn\'t match expected type ‘[Char]’ with actual type ‘Integer’\nIn the second argument of ‘(++)’, namely ‘s’\nIn the expression: "Hello, " ++ s'}]} +many_completions = {'tag': 'ResponseGetAutocompletion', 'contents': [{'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.List', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'GHC.OldList', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '!!', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Data.List', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 4, 'spanToLine': 4, 'spanFromColumn': 1, 'spanToColumn': 17}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Base', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Data.Function', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': '(a -> b) -> a -> b', 'idName': '$', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'WiredIn', 'contents': []}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Base', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '$!', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Classes', 'modulePackage': {'packageName': 'ghc-prim', 'packageKey': 'ghc-prim', 'packageVersion': '0.4.0.0'}}, 'idHomeModule': {'moduleName': 'Data.Bool', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '&&', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Num', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '*', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Float', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '**', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Base', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Control.Applicative', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '*>', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Num', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '+', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}]} +readFile_exp_types = {'tag': 'ResponseGetExpTypes', 'contents': [['FilePath -> IO String', {'spanToColumn': 25, 'spanToLine': 10, 'spanFromColumn': 17, 'spanFromLine': 10, 'spanFilePath': 'src/Lib.hs'}], ['IO String', {'spanToColumn': 36, 'spanToLine': 10, 'spanFromColumn': 17, 'spanFromLine': 10, 'spanFilePath': 'src/Lib.hs'}], ['IO ()', {'spanToColumn': 28, 'spanToLine': 11, 'spanFromColumn': 12, 'spanFromLine': 9, 'spanFilePath': 'src/Lib.hs'}]], 'seq': 'fd3eb2a5-e390-4ad7-be72-8b2e82441a95'} +status_progress_restart = {'contents': {'contents': [], 'tag': 'UpdateStatusRequiredRestart'}, 'tag': 'ResponseUpdateSession'} +status_progress_1 = {'contents': {'contents': {'progressParsedMsg': 'Compiling Lib', 'progressNumSteps': 2, 'progressStep': 1, 'progressOrigMsg': '[1 of 2] Compiling Lib ( /Users/tomv/Projects/Personal/haskell/helloworld/src/Lib.hs, interpreted )'}, 'tag': 'UpdateStatusProgress'}, 'tag': 'ResponseUpdateSession'} +status_progress_2 = {'contents': {'contents': {'progressParsedMsg': 'Compiling Main', 'progressNumSteps': 2, 'progressStep': 2, 'progressOrigMsg': '[2 of 2] Compiling Main ( /Users/tomv/Projects/Personal/haskell/helloworld/app/Main.hs, interpreted )'}, 'tag': 'UpdateStatusProgress'}, 'tag': 'ResponseUpdateSession'} +status_progress_done = {'contents': {'contents': [], 'tag': 'UpdateStatusDone'}, 'tag': 'ResponseUpdateSession'} diff --git a/test/fakebackend.py b/test/fakebackend.py new file mode 100644 index 0000000..cbc5349 --- /dev/null +++ b/test/fakebackend.py @@ -0,0 +1,52 @@ +from .data import exp_types_response + +def seq_response(seq_id, contents): + contents['seq']= seq_id + return contents + + +def make_response(seq_id, contents): + return {'seq': seq_id, 'contents': contents} + +class FakeBackend(): + """ + Fakes responses from the stack-ide process + Override responses by passing in a dict keyed by tag + """ + + def __init__(self, responses={}): + self.responses = responses + if self.responses is None: + raise Exception('stopthat!') + + def send_request(self, req): + + if self.handler: + self.return_test_data(req) + + def return_test_data(self, req): + + tag = req.get('tag') + seq_id = req.get('seq') + + # overrides + if self.responses is None: + raise Exception('wtf!') + override = self.responses.get(tag) + if override: + self.handler(seq_response(seq_id, override)) + return + + # default responses + if tag == 'RequestUpdateSession': + return + if tag == 'RequestShutdownSession': + return + if tag == 'RequestGetSourceErrors': + self.handler(make_response(seq_id, [])) + return + if tag == 'RequestGetExpTypes': + self.handler(seq_response(seq_id, exp_types_response)) + return + else: + raise Exception(tag) diff --git a/test/mocks.py b/test/mocks.py index b762844..f68fd6e 100644 --- a/test/mocks.py +++ b/test/mocks.py @@ -1,7 +1,13 @@ import os from unittest.mock import Mock, MagicMock +from stack_ide import StackIDE +from stack_ide_manager import StackIDEManager +from .fakebackend import FakeBackend +from settings import Settings + cur_dir = os.path.dirname(os.path.realpath(__file__)) +test_settings = Settings("none", [], False) def mock_window(paths=[]): window = MagicMock() @@ -24,3 +30,28 @@ def mock_view(file_path, window): view.rowcol = Mock(return_value=(0, 0)) view.text_point = Mock(return_value=20) return view + +def setup_fake_backend(window, responses={}): + backend = FakeBackend(responses) + instance = StackIDE(window, test_settings, backend) + backend.handler = instance.handle_response + StackIDEManager.ide_backend_instances[ + window.id()] = instance + return backend + +def setup_mock_backend(window): + backend = MagicMock() + instance = StackIDE(window, test_settings, backend) + # backend.handler = instance.handle_response + StackIDEManager.ide_backend_instances[ + window.id()] = instance + return backend + + +def default_mock_window(): + """ + Returns a (window, view) tuple pointing to /projects/helloworld/src/Main.hs + """ + window = mock_window([cur_dir + '/projects/helloworld']) + view = mock_view('src/Main.hs', window) + return (window, view) diff --git a/test/stubs/backend.py b/test/stubs/backend.py deleted file mode 100644 index ced28cc..0000000 --- a/test/stubs/backend.py +++ /dev/null @@ -1,10 +0,0 @@ -class FakeBackend(): - - def __init__(self, response={}): - self.response = response - self.handler = None - - def send_request(self, req): - self.response["seq"] = req.get("seq") - if not self.handler is None: - self.handler(self.response) diff --git a/test/stubs/sublime.py b/test/stubs/sublime.py index 9d45257..47f0eaf 100644 --- a/test/stubs/sublime.py +++ b/test/stubs/sublime.py @@ -12,7 +12,7 @@ def error_message(msg): current_error = msg def set_timeout_async(fn, delay): - pass + fn() def set_timeout(fn, delay): fn() @@ -41,6 +41,9 @@ def id(self): def folders(self): return self._folders + # def create_output_panel(): + # return None + fake_windows = [] ENCODED_POSITION = 1 #flag used for window.open_file @@ -54,6 +57,9 @@ def create_window(path): fake_windows.append(window) return window +def add_window(window): + fake_windows.append(window) + def destroy_windows(): global fake_windows fake_windows = [] diff --git a/test/test_commands.py b/test/test_commands.py index b75c6f3..a78f34b 100644 --- a/test/test_commands.py +++ b/test/test_commands.py @@ -1,27 +1,17 @@ import unittest from unittest.mock import MagicMock, Mock, ANY -from stack_ide_manager import StackIDEManager import stack_ide as stackide -from .mocks import mock_window, mock_view, cur_dir +from .mocks import cur_dir, default_mock_window, setup_fake_backend from text_commands import ClearErrorPanelCommand, UpdateErrorPanelCommand, ShowHsTypeAtCursorCommand, ShowHsInfoAtCursorCommand, CopyHsTypeAtCursorCommand, GotoDefinitionAtCursorCommand from .stubs import sublime -from .stubs.backend import FakeBackend -from settings import Settings - -type_info = "FilePath -> IO String" -span = { - "spanFromLine": 1, - "spanFromColumn": 1, - "spanToLine": 1, - "spanToColumn": 5 -} -exp_types_response = {"tag": "", "contents": [[type_info, span]]} -someFunc_span_info = {'contents': [[{'contents': {'idProp': {'idDefinedIn': {'moduleName': 'Lib', 'modulePackage': {'packageVersion': None, 'packageName': 'main', 'packageKey': 'main'}}, 'idSpace': 'VarName', 'idType': 'IO ()', 'idDefSpan': {'contents': {'spanFromLine': 9, 'spanFromColumn': 1, 'spanToColumn': 9, 'spanFilePath': 'src/Lib.hs', 'spanToLine': 9}, 'tag': 'ProperSpan'}, 'idName': 'someFunc', 'idHomeModule': None}, 'idScope': {'idImportQual': '', 'idImportedFrom': {'moduleName': 'Lib', 'modulePackage': {'packageVersion': None, 'packageName': 'main', 'packageKey': 'main'}}, 'idImportSpan': {'contents': {'spanFromLine': 3, 'spanFromColumn': 1, 'spanToColumn': 11, 'spanFilePath': 'app/Main.hs', 'spanToLine': 3}, 'tag': 'ProperSpan'}, 'tag': 'Imported'}}, 'tag': 'SpanId'}, {'spanFromLine': 7, 'spanFromColumn': 27, 'spanToColumn': 35, 'spanFilePath': 'app/Main.hs', 'spanToLine': 7}]], 'seq': '724752c9-a7bf-4658-834a-3ff7df64e7e5', 'tag': 'ResponseGetSpanInfo'} -putStrLn_span_info = {'contents': [[{'contents': {'idProp': {'idDefinedIn': {'moduleName': 'System.IO', 'modulePackage': {'packageVersion': '4.8.1.0', 'packageName': 'base', 'packageKey': 'base'}}, 'idSpace': 'VarName', 'idType': 'String -> IO ()', 'idDefSpan': {'contents': '', 'tag': 'TextSpan'}, 'idName': 'putStrLn', 'idHomeModule': {'moduleName': 'System.IO', 'modulePackage': {'packageVersion': '4.8.1.0', 'packageName': 'base', 'packageKey': 'base'}}}, 'idScope': {'idImportQual': '', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageVersion': '4.8.1.0', 'packageName': 'base', 'packageKey': 'base'}}, 'idImportSpan': {'contents': {'spanFromLine': 1, 'spanFromColumn': 8, 'spanToColumn': 12, 'spanFilePath': 'app/Main.hs', 'spanToLine': 1}, 'tag': 'ProperSpan'}, 'tag': 'Imported'}}, 'tag': 'SpanId'}, {'spanFromLine': 7, 'spanFromColumn': 41, 'spanToColumn': 49, 'spanFilePath': 'app/Main.hs', 'spanToLine': 7}]], 'seq': '6ee8d949-82bd-491d-8b79-ffcaa3e65fde', 'tag': 'ResponseGetSpanInfo'} -test_settings = Settings("none", [], False) +from .data import type_info, someFunc_span_info, putStrLn_span_info + class CommandTests(unittest.TestCase): + def setUp(self): + stackide.stack_ide_loadtargets = Mock(return_value=['app/Main.hs', 'src/Lib.hs']) + def test_can_clear_panel(self): cmd = ClearErrorPanelCommand() cmd.view = MagicMock() @@ -36,90 +26,68 @@ def test_can_update_panel(self): cmd.view.insert.assert_called_with(ANY, 0, "message\n\n") def test_can_show_type_at_cursor(self): - cmd = ShowHsTypeAtCursorCommand() - window = mock_window([cur_dir + '/projects/helloworld']) - cmd.view = mock_view('src/Main.hs', window) - cmd.view.show_popup = Mock() - backend = FakeBackend(exp_types_response) - instance = stackide.StackIDE(cmd.view.window(), test_settings, backend) - backend.handler = instance.handle_response - - StackIDEManager.ide_backend_instances[ - cmd.view.window().id()] = instance + cmd = ShowHsTypeAtCursorCommand() + (window, view) = default_mock_window() + cmd.view = view + setup_fake_backend(window) cmd.run(None) cmd.view.show_popup.assert_called_with(type_info) def test_can_copy_type_at_cursor(self): - cmd = CopyHsTypeAtCursorCommand() - window = mock_window([cur_dir + '/projects/helloworld']) - cmd.view = mock_view('src/Main.hs', window) - - backend = FakeBackend(exp_types_response) - instance = stackide.StackIDE(window, test_settings, backend) - backend.handler = instance.handle_response - StackIDEManager.ide_backend_instances[ - window.id()] = instance + cmd = CopyHsTypeAtCursorCommand() + (window, view) = default_mock_window() + cmd.view = view + setup_fake_backend(window) cmd.run(None) + self.assertEqual(sublime.clipboard, type_info) def test_can_request_show_info_at_cursor(self): - cmd = ShowHsInfoAtCursorCommand() - window = mock_window([cur_dir + '/projects/helloworld']) - cmd.view = mock_view('src/Main.hs', window) - cmd.view.show_popup = Mock() - backend = FakeBackend(someFunc_span_info) - instance = stackide.StackIDE(cmd.view.window(), test_settings, backend) - backend.handler = instance.handle_response + cmd = ShowHsInfoAtCursorCommand() + (window, view) = default_mock_window() + cmd.view = view - StackIDEManager.ide_backend_instances[ - window.id()] = instance + setup_fake_backend(window, {'RequestGetSpanInfo': someFunc_span_info}) cmd.run(None) cmd.view.show_popup.assert_called_with("someFunc :: IO () (Defined in src/Lib.hs:9:1)") def test_show_info_from_module(self): + cmd = ShowHsInfoAtCursorCommand() - window = mock_window([cur_dir + '/projects/helloworld']) - cmd.view = mock_view('src/Main.hs', window) - cmd.view.show_popup = Mock() + (window, view) = default_mock_window() + cmd.view = view - backend = FakeBackend(putStrLn_span_info) - instance = stackide.StackIDE(cmd.view.window(), test_settings, backend) - backend.handler = instance.handle_response - StackIDEManager.ide_backend_instances[ - window.id()] = instance + setup_fake_backend(window, {'RequestGetSpanInfo':putStrLn_span_info}) cmd.run(None) + cmd.view.show_popup.assert_called_with("putStrLn :: String -> IO () (Imported from Prelude)") def test_goto_definition_at_cursor(self): - global cur_dir - cmd = GotoDefinitionAtCursorCommand() - window = mock_window([cur_dir + '/projects/helloworld']) - window.open_file = Mock() - cmd.view = mock_view('src/Main.hs', window) - backend = FakeBackend(someFunc_span_info) - instance = stackide.StackIDE(cmd.view.window(), test_settings, backend) - backend.handler = instance.handle_response + cmd = GotoDefinitionAtCursorCommand() + (window, view) = default_mock_window() + cmd.view = view - StackIDEManager.ide_backend_instances[ - window.id()] = instance + setup_fake_backend(window, {'RequestGetSpanInfo': someFunc_span_info}) cmd.run(None) + window.open_file.assert_called_with(cur_dir + "/projects/helloworld/src/Lib.hs:9:1", sublime.ENCODED_POSITION) def test_goto_definition_of_module(self): - global cur_dir + cmd = GotoDefinitionAtCursorCommand() - window = mock_window([cur_dir + '/projects/helloworld']) - cmd.view = mock_view('src/Main.hs', window) - window.status_message = Mock() + (window, view) = default_mock_window() + cmd.view = view + cmd._handle_response(putStrLn_span_info.get('contents')) + self.assertEqual("Cannot navigate to putStrLn, it is imported from Prelude", sublime.current_status) diff --git a/test/test_listeners.py b/test/test_listeners.py index 755997f..d95b77e 100644 --- a/test/test_listeners.py +++ b/test/test_listeners.py @@ -1,14 +1,13 @@ import unittest -from unittest.mock import MagicMock, Mock, ANY +from unittest.mock import Mock, ANY from event_listeners import StackIDESaveListener, StackIDETypeAtCursorHandler, StackIDEAutocompleteHandler from req import Req -from .stubs.backend import FakeBackend from .stubs import sublime -from stack_ide import StackIDE -from stack_ide_manager import StackIDEManager -from .mocks import mock_window, mock_view, cur_dir +from .mocks import default_mock_window, setup_fake_backend, setup_mock_backend from settings import Settings +import stack_ide import utility as util +from .data import many_completions test_settings = Settings("none", [], False) type_info = "FilePath -> IO String" @@ -23,75 +22,70 @@ class ListenerTests(unittest.TestCase): + def setUp(self): + stack_ide.stack_ide_loadtargets = Mock(return_value=['app/Main.hs', 'src/Lib.hs']) + def test_requests_update_on_save(self): listener = StackIDESaveListener() - window = mock_window([cur_dir + '/projects/helloworld']) - view = mock_view('src/Main.hs', window) + (window, view) = default_mock_window() + backend = setup_mock_backend(window) + backend.send_request.reset_mock() - backend = MagicMock() - backend.send_request = Mock() + listener.on_post_save(view) + backend.send_request.assert_called_with(ANY) - instance = StackIDE(window, test_settings, backend) - # backend.handler = instance.handle_response - StackIDEManager.ide_backend_instances[ - window.id()] = instance - - listener.on_post_save(view) - backend.send_request.assert_any_call(request_include_targets) def test_ignores_non_haskell_views(self): listener = StackIDESaveListener() - - window = mock_window([cur_dir + '/projects/helloworld']) - view = mock_view('src/Main.hs', window) + (window, view) = default_mock_window() view.match_selector.return_value = False + backend = setup_mock_backend(window) - backend = MagicMock() - backend.send_request = Mock() - - instance = StackIDE(window, test_settings, backend) - - StackIDEManager.ide_backend_instances[ - window.id()] = instance + backend.send_request.reset_mock() listener.on_post_save(view) - self.assertFalse(backend.send_request.called) + backend.send_request.assert_not_called() + def test_type_at_cursor_tests(self): listener = StackIDETypeAtCursorHandler() - - window = mock_window([cur_dir + '/projects/helloworld']) - view = mock_view('src/Main.hs', window) - - backend = FakeBackend(exp_types_response) - instance = StackIDE(window, test_settings, backend) - backend.handler = instance.handle_response - - StackIDEManager.ide_backend_instances[ - window.id()] = instance + (window, view) = default_mock_window() + setup_fake_backend(window, exp_types_response) listener.on_selection_modified(view) + view.set_status.assert_called_with("type_at_cursor", type_info) view.add_regions.assert_called_with("type_at_cursor", ANY, "storage.type", "", sublime.DRAW_OUTLINED) def test_request_completions(self): - window = mock_window([cur_dir + '/projects/helloworld']) - view = mock_view('src/Main.hs', window) listener = StackIDEAutocompleteHandler() - backend = MagicMock() - + (window, view) = default_mock_window() view.settings().get = Mock(return_value=False) - instance = StackIDE(window, test_settings, backend) - StackIDEManager.ide_backend_instances[ - window.id()] = instance + backend = setup_mock_backend(window) listener.on_query_completions(view, 'm', []) #locations not used. req = Req.get_autocompletion(filepath=util.relative_view_file_name(view),prefix="m") req['seq'] = ANY - backend.send_request.assert_called_with(req) + + def test_returns_completions(self): + listener = StackIDEAutocompleteHandler() + (window, view) = default_mock_window() + view.settings().get = Mock(side_effect=[False, True]) + setup_fake_backend(window, {'RequestGetAutocompletion': many_completions}) + + completions = listener.on_query_completions(view, 'm', []) #locations not used. + + self.assertEqual(8, len(completions)) + self.assertEqual(['!!\t\tData.List', '!!'], completions[0]) + + # in live situations on_query_completions returns [] first while we retrieve results + # here we make sure that the re-trigger calls are still in place + view.run_command.assert_any_call('hide_auto_complete') + view.run_command.assert_any_call('auto_complete', ANY) + diff --git a/test/test_response.py b/test/test_response.py index 8ea7c29..d3ed243 100644 --- a/test/test_response.py +++ b/test/test_response.py @@ -1,14 +1,6 @@ import unittest import response as res - -source_errors = {'seq': 'd0599c00-0b77-441c-8947-b3882cab298c', 'tag': 'ResponseGetSourceErrors', 'contents': [{'errorSpan': {'tag': 'ProperSpan', 'contents': {'spanFromColumn': 22, 'spanFromLine': 11, 'spanFilePath': 'src/Lib.hs', 'spanToColumn': 28, 'spanToLine': 11}}, 'errorKind': 'KindError', 'errorMsg': 'Couldn\'t match expected type ‘Integer’ with actual type ‘[Char]’\nIn the first argument of ‘greet’, namely ‘"You!"’\nIn the second argument of ‘($)’, namely ‘greet "You!"’\nIn a stmt of a \'do\' block: putStrLn $ greet "You!"'}, {'errorSpan': {'tag': 'ProperSpan', 'contents': {'spanFromColumn': 24, 'spanFromLine': 15, 'spanFilePath': 'src/Lib.hs', 'spanToColumn': 25, 'spanToLine': 15}}, 'errorKind': 'KindError', 'errorMsg': 'Couldn\'t match expected type ‘[Char]’ with actual type ‘Integer’\nIn the second argument of ‘(++)’, namely ‘s’\nIn the expression: "Hello, " ++ s'}]} -many_completions = {'tag': 'ResponseGetAutocompletion', 'contents': [{'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.List', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'GHC.OldList', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '!!', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Data.List', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 4, 'spanToLine': 4, 'spanFromColumn': 1, 'spanToColumn': 17}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Base', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Data.Function', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': '(a -> b) -> a -> b', 'idName': '$', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'WiredIn', 'contents': []}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Base', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '$!', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Classes', 'modulePackage': {'packageName': 'ghc-prim', 'packageKey': 'ghc-prim', 'packageVersion': '0.4.0.0'}}, 'idHomeModule': {'moduleName': 'Data.Bool', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '&&', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Num', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '*', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Float', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '**', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Base', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Control.Applicative', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '*>', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Num', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '+', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}]} -readFile_exp_types = {'tag': 'ResponseGetExpTypes', 'contents': [['FilePath -> IO String', {'spanToColumn': 25, 'spanToLine': 10, 'spanFromColumn': 17, 'spanFromLine': 10, 'spanFilePath': 'src/Lib.hs'}], ['IO String', {'spanToColumn': 36, 'spanToLine': 10, 'spanFromColumn': 17, 'spanFromLine': 10, 'spanFilePath': 'src/Lib.hs'}], ['IO ()', {'spanToColumn': 28, 'spanToLine': 11, 'spanFromColumn': 12, 'spanFromLine': 9, 'spanFilePath': 'src/Lib.hs'}]], 'seq': 'fd3eb2a5-e390-4ad7-be72-8b2e82441a95'} -status_progress_restart = {'contents': {'contents': [], 'tag': 'UpdateStatusRequiredRestart'}, 'tag': 'ResponseUpdateSession'} -status_progress_1 = {'contents': {'contents': {'progressParsedMsg': 'Compiling Lib', 'progressNumSteps': 2, 'progressStep': 1, 'progressOrigMsg': '[1 of 2] Compiling Lib ( /Users/tomv/Projects/Personal/haskell/helloworld/src/Lib.hs, interpreted )'}, 'tag': 'UpdateStatusProgress'}, 'tag': 'ResponseUpdateSession'} -status_progress_2 = {'contents': {'contents': {'progressParsedMsg': 'Compiling Main', 'progressNumSteps': 2, 'progressStep': 2, 'progressOrigMsg': '[2 of 2] Compiling Main ( /Users/tomv/Projects/Personal/haskell/helloworld/app/Main.hs, interpreted )'}, 'tag': 'UpdateStatusProgress'}, 'tag': 'ResponseUpdateSession'} -status_progress_done = {'contents': {'contents': [], 'tag': 'UpdateStatusDone'}, 'tag': 'ResponseUpdateSession'} - +from .data import source_errors, status_progress_1, status_progress_2, status_progress_done, status_progress_restart, many_completions, readFile_exp_types class ParsingTests(unittest.TestCase): diff --git a/test/test_stack_ide_manager.py b/test/test_stack_ide_manager.py index f213c08..d4cdf78 100644 --- a/test/test_stack_ide_manager.py +++ b/test/test_stack_ide_manager.py @@ -1,16 +1,15 @@ import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, Mock from stack_ide_manager import NoStackIDE, StackIDEManager, configure_instance -import stack_ide as stackide +import stack_ide from .mocks import mock_window, cur_dir from .stubs import sublime -from .stubs.backend import FakeBackend +from .fakebackend import FakeBackend +from .data import test_settings from log import Log from req import Req -from settings import Settings import watchdog as wd -test_settings = Settings("none", [], False) class WatchdogTests(unittest.TestCase): @@ -27,7 +26,6 @@ def test_managed_by_plugin_events(self): self.assertIsNone(wd.watchdog) - class StackIDEManagerTests(unittest.TestCase): @@ -40,10 +38,8 @@ def test_defaults(self): def test_creates_initial_window(self): sublime.create_window('.') - StackIDEManager.check_windows() self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) - sublime.destroy_windows() def test_monitors_closed_windows(self): @@ -65,12 +61,15 @@ def test_monitors_new_windows(self): sublime.destroy_windows() def test_retains_live_instances(self): - window = sublime.create_window('.') + + window = mock_window(['.']) + sublime.add_window(window) + StackIDEManager.check_windows() self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) # substitute a 'live' instance - instance = stackide.StackIDE(window, test_settings, FakeBackend()) + instance = stack_ide.StackIDE(window, test_settings, FakeBackend()) StackIDEManager.ide_backend_instances[window.id()] = instance # instance should still exist. @@ -87,7 +86,8 @@ def test_kills_live_orphans(self): # substitute a 'live' instance backend = MagicMock() - instance = stackide.StackIDE(window, test_settings, backend) + stack_ide.stack_ide_loadtargets = Mock(return_value=['app/Main.hs', 'src/Lib.hs']) + instance = stack_ide.StackIDE(window, test_settings, backend) StackIDEManager.ide_backend_instances[window.id()] = instance # close the window @@ -150,11 +150,12 @@ def test_launch_window_with_wrong_cabal_file(self): def test_launch_window_with_helloworld_project(self): instance = configure_instance( mock_window([cur_dir + '/projects/helloworld']), test_settings) - self.assertIsInstance(instance, stackide.StackIDE) + self.assertIsInstance(instance, stack_ide.StackIDE) instance.end() - @patch('stack_ide.boot_ide_backend', side_effect=FileNotFoundError()) - def test_launch_window_stack_not_found(self, boot_mock): + def test_launch_window_stack_not_found(self): + + stack_ide.stack_ide_start = Mock(side_effect=FileNotFoundError()) instance = configure_instance( mock_window([cur_dir + '/projects/helloworld']), test_settings) self.assertIsInstance(instance, NoStackIDE) @@ -162,8 +163,9 @@ def test_launch_window_stack_not_found(self, boot_mock): instance.reason, "instance init failed -- stack not found") self.assertRegex(sublime.current_error, "Could not find program 'stack'!") - @patch('stack_ide.boot_ide_backend', side_effect=Exception()) - def test_launch_window_stack_not_found(self, boot_mock): + def test_launch_window_stack_unknown_error(self): + + stack_ide.stack_ide_start = Mock(side_effect=Exception()) instance = configure_instance( mock_window([cur_dir + '/projects/helloworld']), test_settings) self.assertIsInstance(instance, NoStackIDE) diff --git a/test/test_stackide.py b/test/test_stackide.py index be13cbb..b3c0c15 100644 --- a/test/test_stackide.py +++ b/test/test_stackide.py @@ -1,25 +1,33 @@ import unittest -from unittest.mock import Mock, MagicMock +from unittest.mock import Mock, MagicMock, patch import stack_ide as stackide from .stubs import sublime -from .stubs.backend import FakeBackend +from .fakebackend import FakeBackend from .mocks import mock_window, cur_dir from settings import Settings +from .data import status_progress_1 from req import Req test_settings = Settings("none", [], False) - +@patch('stack_ide.stack_ide_loadtargets', return_value=['app/Main.hs', 'src/Lib.hs']) class StackIDETests(unittest.TestCase): - def test_can_create(self): + def test_can_create(self, loadtargets_mock): instance = stackide.StackIDE( mock_window([cur_dir + '/mocks/helloworld/']), test_settings, FakeBackend()) self.assertIsNotNone(instance) self.assertTrue(instance.is_active) self.assertTrue(instance.is_alive) - def test_can_send_source_errors_request(self): + # it got the load targets + self.assertEqual(2, len(instance.include_targets)) + + # it should also have called get source errors, + # but FakeBackend sends no errors back by default. + + + def test_can_send_source_errors_request(self, loadtargets_mock): backend = FakeBackend() backend.send_request = Mock() instance = stackide.StackIDE( @@ -31,7 +39,7 @@ def test_can_send_source_errors_request(self): instance.send_request(req) backend.send_request.assert_called_with(req) - def test_handle_welcome_stack_ide_outdated(self): + def test_handle_welcome_stack_ide_outdated(self, loadtargets_mock): backend = MagicMock() welcome = { @@ -39,16 +47,23 @@ def test_handle_welcome_stack_ide_outdated(self): "contents": [0, 0, 0] } - instance = stackide.StackIDE(mock_window([cur_dir + '/mocks/helloworld/']), test_settings, backend) + instance = stackide.StackIDE(mock_window([cur_dir + '/projects/helloworld/']), test_settings, backend) instance.handle_response(welcome) self.assertEqual(sublime.current_error, "Please upgrade stack-ide to a newer version.") - def test_can_shutdown(self): + def test_handle_progress_update(self, loadtargets_mock): + backend = MagicMock() + instance = stackide.StackIDE(mock_window([cur_dir + '/projects/helloworld/']), test_settings, backend) + instance.handle_response(status_progress_1) + self.assertEqual(sublime.current_status, "Compiling Lib") + + + def test_can_shutdown(self, loadtargets_mock): backend = FakeBackend() backend.send_request = Mock() instance = stackide.StackIDE( - mock_window([cur_dir + '/mocks/helloworld/']), test_settings, backend) + mock_window([cur_dir + '/projects/helloworld/']), test_settings, backend) self.assertIsNotNone(instance) self.assertTrue(instance.is_active) self.assertTrue(instance.is_alive) diff --git a/test/test_win.py b/test/test_win.py index 88aa822..b35d2bf 100644 --- a/test/test_win.py +++ b/test/test_win.py @@ -2,93 +2,126 @@ from unittest.mock import MagicMock, Mock, ANY from win import Win from .stubs import sublime -from .mocks import mock_view, mock_window, cur_dir +from .mocks import cur_dir, default_mock_window from utility import relative_view_file_name +def create_source_error(filePath, kind, message): + return { + "errorKind": kind, + "errorMsg": message, + "errorSpan": { + "tag": "ProperSpan", + "contents": { + "spanFilePath": filePath, + "spanFromLine": 1, + "spanFromColumn": 1, + "spanToLine": 1, + "spanToColumn": 5 + } + } + } + + class WinTests(unittest.TestCase): def test_highlight_type_clear(self): - window = mock_window([cur_dir + '/projects/helloworld']) - view = mock_view('src/Main.hs', window) + (window, view) = default_mock_window() Win(window).highlight_type([]) + view.set_status.assert_called_with("type_at_cursor", "") view.add_regions.assert_called_with("type_at_cursor", [], "storage.type", "", sublime.DRAW_OUTLINED) def test_highlight_no_errors(self): - window = mock_window([cur_dir + '/projects/helloworld']) - view = mock_view('src/Main.hs', window) - window.run_command = Mock() + (window, view) = default_mock_window() panel = MagicMock() - window.create_output_panel = Mock(return_value=panel) + errors = [] Win(window).handle_source_errors(errors) - window.create_output_panel.assert_called_with("hide_errors") + # panel recreated + window.create_output_panel.assert_called_with("hide_errors") panel.settings().set.assert_called_with("result_file_regex", "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$") window.run_command.assert_any_call("hide_panel", {"panel": "output.hide_errors"}) panel.run_command.assert_called_with("clear_error_panel") panel.set_read_only.assert_any_call(False) + # regions created in view view.add_regions.assert_any_call("errors", [], "invalid", "dot", sublime.DRAW_OUTLINED) view.add_regions.assert_any_call("warnings", [], "comment", "dot", sublime.DRAW_OUTLINED) + # panel hidden and locked window.run_command.assert_called_with("hide_panel", {"panel": "output.hide_errors"}) panel.set_read_only.assert_any_call(True) + + def test_highlight_errors_and_warnings(self): - window = mock_window([cur_dir + '/projects/helloworld']) - view = mock_view('src/Main.hs', window) + (window, view) = default_mock_window() - window.run_command = Mock() panel = MagicMock() window.create_output_panel = Mock(return_value=panel) - error = { - "errorKind": "KindError", - "errorMsg": "", - "errorSpan": { - "tag": "ProperSpan", - "contents": { - "spanFilePath": relative_view_file_name(view), - "spanFromLine": 1, - "spanFromColumn": 1, - "spanToLine": 1, - "spanToColumn": 5 - } - } - } - warning = { - "errorKind": "KindWarning", - "errorMsg": "", - "errorSpan": { - "tag": "ProperSpan", - "contents": { - "spanFilePath": relative_view_file_name(view), - "spanFromLine": 1, - "spanFromColumn": 1, - "spanToLine": 1, - "spanToColumn": 5 - } - } - } + + filePath = relative_view_file_name(view) + error = create_source_error(filePath, "KindError", "") + warning = create_source_error(filePath, "KindWarning", "") errors = [error, warning] + Win(window).handle_source_errors(errors) - window.create_output_panel.assert_called_with("hide_errors") + # panel recreated + window.create_output_panel.assert_called_with("hide_errors") panel.settings().set.assert_called_with("result_file_regex", "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$") window.run_command.assert_any_call("hide_panel", {"panel": "output.hide_errors"}) # panel.run_command.assert_any_call("clear_error_panel") panel.set_read_only.assert_any_call(False) - # panel should have received messages + # panel should have received two messages panel.run_command.assert_any_call("update_error_panel", {"message": "src/Main.hs:1:1: KindError:\n"}) panel.run_command.assert_any_call("update_error_panel", {"message": "src/Main.hs:1:1: KindWarning:\n"}) + # regions added view.add_regions.assert_called_with("warnings", [ANY], "comment", "dot", sublime.DRAW_OUTLINED) view.add_regions.assert_any_call('errors', [ANY], 'invalid', 'dot', 2) + + # panel shown and locked + window.run_command.assert_called_with("show_panel", {"panel": "output.hide_errors"}) + panel.set_read_only.assert_any_call(True) + + def test_opens_views_for_errors(self): + + (window, view) = default_mock_window() + window.find_open_file = Mock(side_effect=[None, view]) # first call None, second call is created + + panel = MagicMock() + window.create_output_panel = Mock(return_value=panel) + + error = create_source_error("src/Lib.hs", "KindError", "") + errors = [error] + + Win(window).handle_source_errors(errors) + + # should have opened the file for us. + window.open_file.assert_called_with(cur_dir + "/projects/helloworld/src/Lib.hs") + + # panel recreated + window.create_output_panel.assert_called_with("hide_errors") + panel.settings().set.assert_called_with("result_file_regex", "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$") + window.run_command.assert_any_call("hide_panel", {"panel": "output.hide_errors"}) + # panel.run_command.assert_any_call("clear_error_panel") + panel.set_read_only.assert_any_call(False) + + # panel should have received two messages + panel.run_command.assert_any_call("update_error_panel", {"message": "src/Lib.hs:1:1: KindError:\n"}) + + # regions added + view.add_regions.assert_called_with("warnings", [], "comment", "dot", sublime.DRAW_OUTLINED) + view.add_regions.assert_any_call('errors', [ANY], 'invalid', 'dot', 2) + + # panel shown and locked window.run_command.assert_called_with("show_panel", {"panel": "output.hide_errors"}) panel.set_read_only.assert_any_call(True) diff --git a/win.py b/win.py index 809a0f8..7ef193c 100644 --- a/win.py +++ b/win.py @@ -108,9 +108,10 @@ def reset_error_panel(self): # Seems to force the panel to refresh after we clear it: self.window.run_command("hide_panel", {"panel": "output.hide_errors"}) - # Clear the panel + # Clear the panel. TODO: should be unnecessary? https://www.sublimetext.com/forum/viewtopic.php?f=6&t=2044 panel.run_command("clear_error_panel") + # TODO store the panel somewhere so we can reuse it. return panel diff --git a/window_commands.py b/window_commands.py index 9fb7934..498a6bd 100644 --- a/window_commands.py +++ b/window_commands.py @@ -5,13 +5,6 @@ from stack_ide_manager import StackIDEManager -class UpdateCompletionsCommand(sublime_plugin.WindowCommand): - """ - This class only exists so that the command can be called and intercepted by - StackIDEAutocompleteHandler to update its completions list. - """ - def run(self, completions): - return None class SendStackIdeRequestCommand(sublime_plugin.WindowCommand): """