From d0453d51ee7713f794845ef6481a2fe05ca38d39 Mon Sep 17 00:00:00 2001 From: Abhishek's Macbook Pro Date: Sat, 11 May 2024 15:48:48 +0530 Subject: [PATCH] Version 2.0.0 --- assets/beaker.png | Bin 0 -> 296 bytes assets/debug-start.png | Bin 0 -> 505 bytes assets/run_test_annotation.html | 35 ++ assets/test_output.sublime-syntax | 86 ++++ assets/test_output_color_scheme.tmTheme | 58 +++ package-metadata.json | 1 + py_rock.py | 41 +- src/commands/annotate_and_test_runner.py | 368 ++++++++++++++++++ src/commands/copy_test_path.py | 38 ++ src/commands/import_symbol.py | 8 +- src/commands/output_panel.py | 58 +++ src/commands/unittest_path_generator.py | 259 ++++++++++++ src/constants.py | 16 +- src/exceptions.py | 20 +- src/serialized_settings.json | 1 + src/settings.py | 55 ++- src/utils.py | 11 + tests/fixtures/test_fixture.py | 14 + .../commands/test_annotate_and_test_runner.py | 166 ++++++++ tests/src/commands/test_base_indexer.py | 1 - tests/src/commands/test_copy_test_path.py | 202 ++++++++++ tests/src/commands/test_import_symbol.py | 29 ++ tests/src/commands/test_re_index_imports.py | 29 ++ 23 files changed, 1482 insertions(+), 14 deletions(-) create mode 100644 assets/beaker.png create mode 100644 assets/debug-start.png create mode 100644 assets/run_test_annotation.html create mode 100644 assets/test_output.sublime-syntax create mode 100644 assets/test_output_color_scheme.tmTheme create mode 100644 package-metadata.json create mode 100644 src/commands/annotate_and_test_runner.py create mode 100644 src/commands/copy_test_path.py create mode 100644 src/commands/output_panel.py create mode 100644 src/commands/unittest_path_generator.py create mode 100644 src/serialized_settings.json create mode 100644 tests/fixtures/test_fixture.py create mode 100644 tests/src/commands/test_annotate_and_test_runner.py create mode 100644 tests/src/commands/test_copy_test_path.py create mode 100644 tests/src/commands/test_re_index_imports.py diff --git a/assets/beaker.png b/assets/beaker.png new file mode 100644 index 0000000000000000000000000000000000000000..7a75a98e405bb5dbdb9d4a5cde7c4d43c4171271 GIT binary patch literal 296 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{$5WP$B+ufvlHKP9x@PUd%x&)#}3E4F6{yn zm6lG?dL^*Sb9dtk<{Pd>=}Ej@mc9%AefTL{v~$nQ488a-J^#AVL*RYrm3xazx7Ru`%>H~MDEi`R>qY0B qncnIs2uCXfO})wBa^b-G9gMGj5j{NS{8?XL5Ya~FpzN}r$sBmU9t4v940cY1V65X_L zLDxnCY`E4I5N_vJ4rdWHS|cs2;aXb&aP}fUjW%9HtsW4r_lUibH(i4v%5#8RuRrmt vX`{HHZ~``6%M?n$wreHm$!$YLMTvX^)g9H6#cNK(00000NkvXXu0mjfcRJF< literal 0 HcmV?d00001 diff --git a/assets/run_test_annotation.html b/assets/run_test_annotation.html new file mode 100644 index 0000000..f2ba09a --- /dev/null +++ b/assets/run_test_annotation.html @@ -0,0 +1,35 @@ + + + + + + +

Run as test

+
+ + diff --git a/assets/test_output.sublime-syntax b/assets/test_output.sublime-syntax new file mode 100644 index 0000000..241ddc4 --- /dev/null +++ b/assets/test_output.sublime-syntax @@ -0,0 +1,86 @@ +%YAML 1.2 +--- +name: PyRock Test Result +scope: text.test-result +hidden: true +contexts: + main: + - match: 'cd' + captures: + 1: support.command.shell + scope: source.shell + + # verbosity <= 1 + - match: '(?=^[\\.sEF]+$)' + push: + - match: $ + pop: true + - match: s + scope: markup.changed + - match: E|F + scope: markup.deleted + + # verbosity >= 2 + - match: .+(\.\.\.) + captures: + 1: markup.ignored + push: + - match: $ + pop: true + - match: ok + scope: markup.inserted + - match: ERROR|FAIL + scope: markup.deleted + - match: "skipped.*" + scope: markup.changed + + - match: ^OK.* + scope: markup.inserted + + - match: ^FAILED.* + scope: markup.deleted + + - match: '======================================================================' + scope: markup.ignored + push: + - match: '----------------------------------------------------------------------' + scope: markup.ignored + pop: true + - match: ^ERROR|FAIL + scope: markup.deleted + + - match: ^----------------------------------------------------------------------$ + scope: markup.ignored + + - include: scope:source.diff + + - match: 'File "(.*)"(?:, line ([0-9]+)(?:, in (.*))?)?' + captures: + 1: markup.underline.link + 2: constant.numeric + 3: entity.name + + - match: 'Running Command:' + scope: markup.heading + + # New patterns for datetime and logger level + - match: '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}' + scope: markup.changed + + - match: 'INFO|DEBUG|WARN|WARNING|ERROR|CRITICAL' + scope: keyword.control.logger-level + + # New patterns for Python errors + - match: 'Traceback \(most recent call last\):' + scope: keyword.control.python-error + push: + - match: 'File "(.*)"(?:, line ([0-9]+)(?:, in (.*))?)?' + captures: + 1: markup.underline.link + 2: constant.numeric + 3: entity.name + - match: '^([^:]+): (.+)$' + captures: + 1: variable.language.python + 2: string.quoted.single.python + scope: keyword.control.python-error diff --git a/assets/test_output_color_scheme.tmTheme b/assets/test_output_color_scheme.tmTheme new file mode 100644 index 0000000..6a83954 --- /dev/null +++ b/assets/test_output_color_scheme.tmTheme @@ -0,0 +1,58 @@ + + + + + name + Custom Theme + settings + + + + settings + + background + #000000 + foreground + #FFFFFF + + + + + + name + Constant - Datetime + scope + constant.other.datetime + settings + + foreground + #FF0000 + + + + name + Constant - Logger Level + scope + keyword.control.logger-level + settings + + foreground + #FF0000 + + + + name + Constant - Python Error + scope + keyword.control.python-error + settings + + foreground + #FF0000 + underline + + + + + + diff --git a/package-metadata.json b/package-metadata.json new file mode 100644 index 0000000..24e4f7d --- /dev/null +++ b/package-metadata.json @@ -0,0 +1 @@ +{"sublime_text": ">=3000", "description": "Sublime plugin to generate python import statements", "platforms": ["*"], "version": "1.1.0", "url": "https://github.com/abhishek72850/pyrock", "dependencies": []} \ No newline at end of file diff --git a/py_rock.py b/py_rock.py index ee36d8c..6e566ae 100644 --- a/py_rock.py +++ b/py_rock.py @@ -1,10 +1,10 @@ -import importlib import sublime import sublime_plugin from sublime import Edit from typing import Optional # Reloads the submodules +# import importlib # from .src import reloader # importlib.reload(reloader) # reloader.reload() @@ -15,9 +15,13 @@ from .src.commands.admin import AdminManager from .src.logger import Logger from .src.constants import PyRockConstants +from .src.commands.copy_test_path import CopyTestPathCommand +from .src.commands.annotate_and_test_runner import AnnotateAndTestRunnerCommand + logger = Logger(__name__) admin = AdminManager(window=sublime.active_window()) +test_runner_cmd = AnnotateAndTestRunnerCommand() def plugin_loaded(): @@ -25,6 +29,9 @@ def plugin_loaded(): admin.initialize() admin.run() + settings = sublime.load_settings(PyRockConstants.PACKAGE_SETTING_NAME) + logger.debug(f"[{PyRockConstants.PACKAGE_NAME}] Settings: {settings}") + def plugin_unloaded(): logger.debug(f"[{PyRockConstants.PACKAGE_NAME}]..........unloaded") @@ -44,9 +51,25 @@ def run(self, edit: Edit, action: str, test: bool = False): test=test, ) cmd.run() - if action == "re_index_imports": + elif action == "copy_import_symbol": + cmd = ImportSymbolCommand( + window=sublime.active_window(), + edit=edit, + view=self.view, + test=test, + ) + cmd.run(copy=True) + elif action == "re_index_imports": cmd = ReIndexImportsCommand(test=test) cmd.run(sublime.active_window()) + elif action == "copy_test_path": + cmd = CopyTestPathCommand( + view=self.view, + test=test, + ) + cmd.run() + else: + logger.debug("Inavlid command recieved") def is_enabled(self, action: str, test: bool = False): """ @@ -92,3 +115,17 @@ def run(self, edit: Edit, start: int, end: int, text: str): """ region = sublime.Region(start, end) self.view.replace(edit, region, text) + + +class PyRockAnnotateAndTestRunnerCommand(sublime_plugin.ViewEventListener): + def on_load_async(self): + logger.debug("View loaded") + test_runner_cmd.run(self.view) + + def on_activated_async(self): + logger.debug("View reloaded") + test_runner_cmd.run(self.view) + + def on_post_save_async(self): + logger.debug("View saved") + test_runner_cmd.run(self.view) diff --git a/src/commands/annotate_and_test_runner.py b/src/commands/annotate_and_test_runner.py new file mode 100644 index 0000000..5d842a9 --- /dev/null +++ b/src/commands/annotate_and_test_runner.py @@ -0,0 +1,368 @@ +import json +import re +import os +import signal +import subprocess +import traceback +from string import Template +from typing import List, Tuple, Union + +import sublime +from sublime import FindFlags, Region, RegionFlags, View + +from ..constants import PyRockConstants +from ..logger import Logger +from ..settings import PyRockSettings +from ..utils import is_test_file +from .output_panel import OutputPanel +from .unittest_path_generator import TestPathGenerator + +logger = Logger(__name__) + + +CLASS_NAME_ONLY_REGEX = r'^(?:class)\s+([a-zA-Z_0-9]\w*)\s*\((?:[^\)]*\):)?' +TEST_METHOD_START_REGEX = r'^ *def\s+(test_[a-zA-Z_0-9]\w*)\s*\((?:[^\)]*\):)?' + + +class CustomTemplate(Template): + delimiter = '$' + + +class AnnotateAndTestRunnerCommand: + + def __init__(self, test: bool = False): + self._test = test + self._process = None + self._command_error_evidence = [] + self._test_process_file_path = os.path.join( + PyRockConstants.INDEX_CACHE_DIRECTORY, 'test_process.json' + ) + self._initilize_test_process_pid_storage() + + def _initilize_test_process_pid_storage(self): + if not os.path.exists(self._test_process_file_path): + logger.debug("Creating empty process list file") + + with open(self._test_process_file_path, 'w') as f: + json.dump([], f) + + def _get_test_command(self, test_path: str) -> str: + unix_env_bash = """ + set -e + . "{venv_path}" + cd "{working_directory}" + {run_test_command} {test_path} + deactivate + """ + unix_without_env_bash = """ + set -e + cd "{working_directory}" + {run_test_command} {test_path} + """ + + if sublime.platform() in [ + PyRockConstants.PLATFORM_LINUX, + PyRockConstants.PLATFORM_OSX + ]: + if PyRockSettings().PYTHON_VIRTUAL_ENV_PATH.value: + test_command = unix_env_bash.format( + venv_path=PyRockSettings().PYTHON_VIRTUAL_ENV_PATH.value, + working_directory=PyRockSettings().TEST_CONFIG.WORKING_DIR, + run_test_command=" ".join( + PyRockSettings().TEST_CONFIG.TEST_RUNNER_COMMAND), + test_path=test_path, + ) + else: + test_command = unix_without_env_bash.format( + working_directory=PyRockSettings().TEST_CONFIG.WORKING_DIR, + run_test_command=" ".join( + PyRockSettings().TEST_CONFIG.TEST_RUNNER_COMMAND), + test_path=test_path, + ) + else: + if PyRockSettings().PYTHON_VIRTUAL_ENV_PATH.value: + venv_path = PyRockSettings().PYTHON_VIRTUAL_ENV_PATH.value.replace( + '\\', '\\\\' + ) + working_directory = PyRockSettings().TEST_CONFIG.WORKING_DIR.replace( + '\\', '\\\\' + ) + run_test_command = " ".join( + PyRockSettings().TEST_CONFIG.TEST_RUNNER_COMMAND + ).replace('\\', '\\\\') + + test_command = [ + venv_path, '&&', 'cd', working_directory, '&&', run_test_command, test_path, 'deactivate' + ] + else: + run_test_command = " ".join( + PyRockSettings().TEST_CONFIG.TEST_RUNNER_COMMAND + ).replace('\\', '\\\\') + + test_command = ['cd', working_directory, '&&', run_test_command, test_path] + + return test_command + + def _kill_existing_running_tests(self): + existing_process = [] + + with open(self._test_process_file_path, 'r') as f: + existing_process = json.load(f) + + logger.debug(f"existing process {existing_process}") + + for process_pid in existing_process: + if sublime.platform() in [ + PyRockConstants.PLATFORM_LINUX, + PyRockConstants.PLATFORM_OSX + ]: + try: + os.kill(process_pid, signal.SIGKILL) + except Exception: + logger.debug(f"unable to kill {process_pid}") + else: + # Windows + os.system(f"taskkill /F /PID {process_pid} > nul 2>&1") + + with open(self._test_process_file_path, 'w') as f: + json.dump([], f) + + def _get_test_process_pids(self) -> List[int]: + test_framework = PyRockSettings().TEST_CONFIG.TEST_FRAMEWORK + + if sublime.platform() in [ + PyRockConstants.PLATFORM_LINUX, + PyRockConstants.PLATFORM_OSX + ]: + if test_framework == PyRockConstants.DJANGO_TEST_FRAMEWORK: + command = 'pgrep -f "manage.py test"' + elif test_framework == PyRockConstants.PYTEST_TEST_FRAMEWORK: + command = 'pgrep -f "pytest"' + else: + # Windows + if test_framework == PyRockConstants.DJANGO_TEST_FRAMEWORK: + command = """wmic process where "name='python.exe' or name='pythonw.exe'" get commandline,processid | find "manage.py test" """ + elif test_framework == PyRockConstants.PYTEST_TEST_FRAMEWORK: + command = """wmic process where "name='python.exe' or name='pythonw.exe'" get commandline,processid | find "pytest.exe" """ + + output = None + pid_list = [] + + try: + output = subprocess.check_output(command, shell=True, text=True) + except subprocess.CalledProcessError: + logger.debug("No process found") + + if output: + if sublime.platform() in [ + PyRockConstants.PLATFORM_LINUX, + PyRockConstants.PLATFORM_OSX + ]: + pid_list = list(map(int, output.strip().split("\n"))) + else: + pid_list = [] + for pid_line in output.split('\n'): + if matchd_pid := re.search(r'\d+', pid_line): + pid_list.append( + int(matchd_pid.group()) + ) + logger.debug(f"PID list: {pid_list}") + + return pid_list + + def _show_test_output_panel(self, test_command: str) -> OutputPanel: + output_panel = OutputPanel( + name=PyRockConstants.PACKAGE_TEST_RUNNER_OUTPUT_PANEL, + word_wrap=True + ) + + if PyRockSettings().LOG_LEVEL.value == "DEBUG": + output_panel.writeln("Running Command: ") + output_panel.writeln(test_command) + output_panel.flush() + output_panel.show() + return output_panel + + def _register_existing_test_process(self): + # Fetch existing running process id's, if any + existing_process = [] + with open(self._test_process_file_path, 'r') as f: + existing_process = json.load(f) + + existing_process = existing_process + self._get_test_process_pids() + logger.debug(f"All test process running: {existing_process}") + + with open(self._test_process_file_path, 'w') as f: + json.dump(existing_process, f) + + def _track_test_progress_on_output_panel(self, test_command: str): + output_panel = self._show_test_output_panel(test_command) + + for index, line in enumerate(self._process.stdout): + if index == 0: + # Latest process will start running by now, and + # we will get all those process ids and register it + self._register_existing_test_process() + + output = line.decode('utf-8').strip() + if output == "" or output is None: + continue + + # output test result + output_panel.writeln(output) + output_panel.flush() + + def _run_test_command( + self, test_command: Union[str, List] + ) -> Tuple[bool, str]: + message: str = "" + script_success: bool = False + + try: + self._process = subprocess.Popen( + test_command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr with stdout + ) + except Exception as e: + logger.error(traceback.format_exc()) + message = str(e) + return script_success, message + + self._track_test_progress_on_output_panel(test_command) + self._process.wait() + + if self._process.returncode == 0: + script_success = True + else: + message = str(self._process.returncode) + + self._process = None + logger.debug("Finished process") + + return script_success, message + + def _execute_test(self, href: str): + regions = self.view.get_regions(self.region_key) + + selected_region_index = int(href) + logger.debug(f"Selected region index: {selected_region_index}") + + for region in regions: + logger.debug( + f"View Region {region.to_tuple()}: {self.view.substr(self.view.full_line(region))}" + ) + + selected_region = regions[selected_region_index] + logger.debug( + f"Selected test region: {selected_region.to_tuple()}: {self.view.substr(self.view.full_line(selected_region))}" + ) + + # prepare test path + test_path = TestPathGenerator.generate( + selected_region, self.view, PyRockSettings().TEST_CONFIG.TEST_FRAMEWORK + ) + logger.debug(f"Test path: {test_path}") + + # get test command + test_command = self._get_test_command(test_path) + logger.debug(f"Test command: {test_command}") + + self._kill_existing_running_tests() + + self.view.window().destroy_output_panel(PyRockConstants.PACKAGE_TEST_RUNNER_OUTPUT_PANEL) + + if self._test: + self._run_test_command(test_command) + else: + # invoke command to run test + sublime.set_timeout_async( + lambda: self._run_test_command(test_command), + 0 + ) + + def _generate_run_test_annotated_html( + self, + matched_regions: List[Region] + ) -> List[str]: + run_test_icon_path = os.path.join( + PyRockConstants.ABSOLUTE_PACKAGE_ASSETS_DIR, 'debug-start.png' + ).replace('\\', '/') + + annotations_html_path = os.path.join( + PyRockConstants.ABSOLUTE_PACKAGE_ASSETS_DIR, 'run_test_annotation.html' + ) + + annotations_html_template = CustomTemplate( + open(annotations_html_path, 'r').read() + ) + + annotations_html_list: List[str] = [] + + for index, region in enumerate(matched_regions): + logger.debug(f"{index} Test regions {region.to_tuple()} : {self.view.substr(self.view.full_line(region))}") + annotations_html_list.append( + annotations_html_template.substitute( + run_test_region_index=index, + image_file=run_test_icon_path + ) + ) + return annotations_html_list + + def run(self, view: View): + self.view = view + if not PyRockSettings().TEST_CONFIG.ENABLED: + logger.info("Test config not enabled") + return + + if not is_test_file(self.view.file_name()): + logger.info("Not a test file, returning") + return + + class_matched_regions: List[Region] = self.view.find_all( + pattern=CLASS_NAME_ONLY_REGEX, + flags=FindFlags.IGNORECASE + ) + logger.debug(f"Class Matched regions list: {class_matched_regions}") + + test_method_matched_regions: List[Region] = self.view.find_all( + pattern=TEST_METHOD_START_REGEX, + flags=FindFlags.IGNORECASE + ) + logger.debug(f"Test Matched regions list: {test_method_matched_regions}") + + matched_regions = list( + sorted( + class_matched_regions + test_method_matched_regions, + key=lambda region: region.to_tuple()[0] + ) + ) + + if len(matched_regions) == 0: + logger.debug( + "No matching regions found for class or test method, returning") + return + + # Prepare run test annotated htmls for matched regions + annotations_html_list = self._generate_run_test_annotated_html(matched_regions) + + test_gutter_icon_path = os.path.join( + PyRockConstants.RELATIVE_PACKAGE_ASSETS_DIR, + "beaker.png" + ) + + self.region_key = f"pyrock-gutter-icon-{self.view.id()}" + + logger.debug(f"Region key: {self.region_key}") + + self.view.add_regions( + key=self.region_key, + regions=matched_regions, + scope='icon', + icon=test_gutter_icon_path, + flags=RegionFlags.HIDDEN, + annotations=annotations_html_list, + annotation_color='green', + on_navigate=self._execute_test + ) diff --git a/src/commands/copy_test_path.py b/src/commands/copy_test_path.py new file mode 100644 index 0000000..20cd3bd --- /dev/null +++ b/src/commands/copy_test_path.py @@ -0,0 +1,38 @@ +import sublime +from typing import Optional +from sublime import View +from ..logger import Logger +from .unittest_path_generator import TestPathGenerator +from ..settings import PyRockSettings +from ..utils import is_test_file + + +logger = Logger(__name__) + + +class CopyTestPathCommand: + def __init__(self, view: View, test: bool = False): + self.view = view + self.test = test + + def run(self): + if not is_test_file(self.view.file_name()): + logger.info("Not a test file, returning") + return + + selected_view = self.view.sel()[0] + selected_text: Optional[str] = self.view.substr(selected_view) + logger.debug(f"Selected test method: {selected_text}") + + test_path = TestPathGenerator.generate( + selected_view, self.view, PyRockSettings().TEST_CONFIG.TEST_FRAMEWORK + ) + logger.debug(f"Generated test path: {test_path}") + + if test_path: + sublime.set_clipboard(test_path) + else: + sublime.status_message( + "Could not generate test path" + ) + logger.info("Couldn't find path") diff --git a/src/commands/import_symbol.py b/src/commands/import_symbol.py index 84231f9..46a2aa1 100644 --- a/src/commands/import_symbol.py +++ b/src/commands/import_symbol.py @@ -123,6 +123,11 @@ def _add_import_to_view(self, index: int): logger.debug(f"Selected option {selected_option} and symbol {selected_symbol}") + if self.copy: + logger.debug(f"Copying import statement {import_option_list[index]}") + sublime.set_clipboard(import_option_list[index]) + return + # Match import statement region like # 1. from foo.bar import foo # 2. from foo.bar import ( @@ -206,7 +211,8 @@ def generate_imports_from_user_python_imports( } return import_statements - def run(self): + def run(self, copy: bool = False): + self.copy = copy selected_view = self.view.sel()[0] selected_text: Optional[str] = self.view.substr(selected_view) diff --git a/src/commands/output_panel.py b/src/commands/output_panel.py new file mode 100644 index 0000000..032fe47 --- /dev/null +++ b/src/commands/output_panel.py @@ -0,0 +1,58 @@ +import collections +import threading + +import sublime + + +class OutputPanel: + + def __init__( + self, name, word_wrap=False, line_numbers=False, gutter=False, + scroll_past_end=False + ): + self.name = name + self.window = sublime.active_window() + self.output_view = self.window.create_output_panel(name) + + settings = self.output_view.settings() + settings.set("word_wrap", word_wrap) + settings.set("line_numbers", line_numbers) + settings.set("gutter", gutter) + settings.set("scroll_past_end", scroll_past_end) + # settings.set("color_scheme", "Packages/PyRock/assets/test_output_color_scheme.tmTheme") + + self.output_view.assign_syntax("Packages/PyRock/assets/test_output.sublime-syntax") + self.output_view.set_read_only(True) + self.closed = False + + self.text_queue_lock = threading.Lock() + self.text_queue = collections.deque() + + def write(self, s): + with self.text_queue_lock: + self.text_queue.append(s) + + def writeln(self, s): + self.write(s + "\n") + + def _write(self): + with self.text_queue_lock: + text = '' + while self.text_queue: + text += self.text_queue.popleft() + + self.output_view.run_command( + 'append', + {'characters': text, 'force': True} + ) + self.output_view.show(self.output_view.size()) + + def flush(self): + self._write() + + def show(self): + self.window.run_command("show_panel", {"panel": "output." + self.name}) + + def close(self): + self.flush() + self.closed = True diff --git a/src/commands/unittest_path_generator.py b/src/commands/unittest_path_generator.py new file mode 100644 index 0000000..038f0ef --- /dev/null +++ b/src/commands/unittest_path_generator.py @@ -0,0 +1,259 @@ +import re +from typing import Optional, List, Tuple +from sublime import Region, View, FindFlags +from ..logger import Logger +from ..exceptions import InvalidTestFramework +from ..constants import PyRockConstants + + +logger = Logger(__name__) + + +# matches class name +CLASS_NAME_START_REGEX = r'^(?:class)\s+([a-zA-Z_0-9]\w*)\s*\((?:[^\)]*\):)?' +# matches test method with params +TEST_METHOD_FULL_REGEX = r'^\s*def\s+(test_[a-zA-Z_0-9]\w*)\s*\([^\)]*\):' +# matches class test method +CLASS_TEST_METHOD_FULL_REGEX = r'^(?: )+def\s+(test_[a-zA-Z_0-9]\w*)\s*\(\s*(?:cls|self)\s*,?[^\)]*\):' + + +class TestPathGenerator: + @staticmethod + def _get_django_test_path( + relative_path: str, + class_name: Optional[str] = None, + method_name: Optional[str] = None, + ) -> str: + test_path = f"{relative_path.replace('.py', '').replace('/', '.')}" + + if class_name: + test_path = f"{test_path}.{class_name}" + + if method_name: + test_path = f"{test_path}.{method_name}" + + return test_path + + @staticmethod + def _get_pytest_test_path( + relative_path: str, + class_name: Optional[str] = None, + method_name: Optional[str] = None, + ) -> str: + test_path = relative_path + + if class_name: + test_path = f"{test_path}::{class_name}" + + if method_name: + test_path = f"{test_path}::{method_name}" + + return test_path + + + @staticmethod + def _get_test_path( + testing_framework: str, + relative_path: str, + class_name: Optional[str] = None, + method_name: Optional[str] = None, + ) -> str: + + if testing_framework == PyRockConstants.DJANGO_TEST_FRAMEWORK: + test_path = TestPathGenerator._get_django_test_path( + relative_path=relative_path, + class_name=class_name, + method_name=method_name, + ) + + elif testing_framework == PyRockConstants.PYTEST_TEST_FRAMEWORK: + test_path = TestPathGenerator._get_pytest_test_path( + relative_path=relative_path, + class_name=class_name, + method_name=method_name, + ) + else: + raise InvalidTestFramework() + + return test_path + + @staticmethod + def _get_class_and_its_method_name( + view: View, + full_line_region: Region + ) -> Tuple[Optional[str], Optional[str]]: + class_name = None + method_name = None + + test_method_region: Region = view.find( + pattern=TEST_METHOD_FULL_REGEX, + start_pt=full_line_region.begin(), + flags=FindFlags.IGNORECASE + ) + + test_method_text = view.substr( + test_method_region + ) + logger.debug(f"Full test method text: {test_method_text}") + logger.debug(f"Test method region point: {test_method_region.to_tuple()}") + + class_test_function_matches = re.findall( + CLASS_TEST_METHOD_FULL_REGEX, + test_method_text, + re.MULTILINE | re.DOTALL + ) + + # Check its a class test method + if len(class_test_function_matches) > 0: + # Its a class test methods + method_name = class_test_function_matches[0] + logger.debug(f"Class first test method name: {method_name}") + + # Find every class name region + class_name_only_regex = r'^(?:class)\s+([a-zA-Z_0-9]\w*)(?:\([^\)]*\):|:)' + + matched_regions: List[Region] = view.find_all( + pattern=class_name_only_regex, + flags=FindFlags.IGNORECASE + ) + logger.debug(f"Matched regions list: {matched_regions}") + + # Finding the matched test method belongs to which class + # its done by first getting all the classes in that view + # then check which class position is closest to that test method + closest_class_region = None + for region in matched_regions: + logger.info(f"{view.substr(region)}, {region.to_tuple()}") + + if test_method_region.begin() > region.begin(): + closest_class_region = region + + logger.debug(f"Closest class {view.substr(closest_class_region)}") + if closest_class_region: + match_result = re.match( + class_name_only_regex, view.substr(closest_class_region) + ) + class_name = match_result.group(1) + + # Check its a individual test method + elif test_function_match := re.match( + TEST_METHOD_FULL_REGEX, + test_method_text, + ): + # Its not a class method + method_name = test_function_match.group(1) + + else: + logger.debug( + "Unable to identify whether its a class or non-class method" + ) + + return (class_name, method_name) + + @staticmethod + def _generate_class_based_test_path( + view: View, + class_name: str, + testing_framework: str, + method_name: Optional[str] = None, + ) -> Optional[str]: + test_path: Optional[str] = None + relative_path = None + + class_symbol_locations = view.window().symbol_locations(class_name) + + # Finding the relative path for self view + for symbol_loc in class_symbol_locations: + # symbol_loc.path has file name + if symbol_loc.path == view.file_name(): + # symbol_loc.display_name has relative path + relative_path = symbol_loc.display_name + break + + if relative_path: + test_path = TestPathGenerator._get_test_path( + testing_framework=testing_framework, + relative_path=relative_path, + class_name=class_name, + method_name=method_name, + ) + logger.debug(f"class based test path: {test_path}") + else: + logger.debug("Could not resolve class relative path") + + return test_path + + @staticmethod + def _generate_method_based_test_path( + view: View, + method_name: str, + testing_framework: str, + ): + test_path: Optional[str] = None + relative_path = None + + method_symbol_locations = view.window().symbol_locations(method_name) + + for symbol_loc in method_symbol_locations: + if symbol_loc.path == view.file_name(): + relative_path = symbol_loc.display_name + break + + if relative_path: + test_path = TestPathGenerator._get_test_path( + testing_framework=testing_framework, + relative_path=relative_path, + class_name=None, + method_name=method_name, + ) + logger.debug(f"method based test path: {test_path}") + else: + logger.debug("Could not resolve method relative path") + + return test_path + + @staticmethod + def generate(region, view: View, testing_framework: str) -> Optional[str]: + logger.debug(f"Testing framework {testing_framework}") + + full_line_region = view.full_line(region) + full_line_text: Optional[str] = view.substr(full_line_region) + logger.debug(f"Selected full line text: {full_line_text}") + + class_name = None + method_name = None + + if class_match := re.match(CLASS_NAME_START_REGEX, full_line_text): + # Its a class name + class_name = class_match.group(1) + + # If its not class name now check + # whether its class test method or indvidual test method + if class_name is None: + class_name, method_name = TestPathGenerator._get_class_and_its_method_name( + view, full_line_region + ) + + logger.debug( + f"Extracted class and test method name: {class_name} {method_name}" + ) + + test_path: Optional[str] = None + + if class_name: + test_path = TestPathGenerator._generate_class_based_test_path( + view, + class_name, + testing_framework, + method_name, + ) + elif method_name: + test_path = TestPathGenerator._generate_method_based_test_path( + view, method_name, testing_framework + ) + else: + logger.debug("Could not find class or method name to generate test path") + + logger.debug(f"Generated test path: {test_path}") + + return test_path diff --git a/src/constants.py b/src/constants.py index 7f2a688..7f7f852 100644 --- a/src/constants.py +++ b/src/constants.py @@ -6,6 +6,17 @@ class PyRockConstants: PACKAGE_SETTING_NAME = 'pyrock.sublime-settings' INDEX_CACHE_DIRECTORY = os.path.join(sublime.cache_path(), PACKAGE_NAME) IMPORT_INDEX_FILE_NAME = 'py_rock_imports.json' + ABSOLUTE_PACKAGE_ASSETS_DIR = os.path.join( + sublime.packages_path(), PACKAGE_NAME, 'assets' + ) + RELATIVE_PACKAGE_ASSETS_DIR = os.path.join( + 'Packages', PACKAGE_NAME, 'assets' + ) + PACKAGE_TEST_FIXTURES_DIR = os.path.join( + sublime.packages_path(), PACKAGE_NAME, 'tests', 'fixtures' + ) + + PACKAGE_TEST_RUNNER_OUTPUT_PANEL = "pyrock_test_runner" DEFAULT_IMPORT_SCAN_DEPTH = 4 MIN_IMPORT_SCAN_DEPTH = 1 @@ -13,4 +24,7 @@ class PyRockConstants: PLATFORM_OSX = "osx" PLATFORM_LINUX = "linux" - PLATFORM_WINDOWS = "windows" \ No newline at end of file + PLATFORM_WINDOWS = "windows" + + DJANGO_TEST_FRAMEWORK = "django" + PYTEST_TEST_FRAMEWORK = "pytest" diff --git a/src/exceptions.py b/src/exceptions.py index 6ce6500..f4fa907 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -78,4 +78,22 @@ def __init__( message: str = "API returned with invalid status", error_code: str = "PR0008", ): - super().__init__(error_code, message) \ No newline at end of file + super().__init__(error_code, message) + + +class InvalidTestConfig(PyRockBaseException): + def __init__( + self, + message: str = "Provided test config is invalid", + error_code: str = "PR0009", + ): + super().__init__(error_code, message) + + +class InvalidTestFramework(PyRockBaseException): + def __init__( + self, + message: str = "Given test framework is invalid", + error_code: str = "PR0010", + ): + super().__init__(error_code, message) diff --git a/src/serialized_settings.json b/src/serialized_settings.json new file mode 100644 index 0000000..cb1bc1f --- /dev/null +++ b/src/serialized_settings.json @@ -0,0 +1 @@ +{"IMPORT_SCAN_DEPTH": 4, "INDEX_CACHE_DIRECTORY": "/Users/abhishek/Library/Caches/Sublime Text/Cache/PyRock", "IMPORT_INDEX_FILE_NAME": "py_rock_imports.json"} \ No newline at end of file diff --git a/src/settings.py b/src/settings.py index 417cdab..3249dac 100644 --- a/src/settings.py +++ b/src/settings.py @@ -3,7 +3,12 @@ import sublime from sublime import Settings from .constants import PyRockConstants -from .exceptions import InvalidImportDepthScan, InvalidPythonVirtualEnvPath, InvalidLogLevel +from .exceptions import ( + InvalidImportDepthScan, + InvalidPythonVirtualEnvPath, + InvalidLogLevel, + InvalidTestConfig +) class PyRockSettingsFieldBase: @@ -88,29 +93,63 @@ def _validate(self): if log_level_map.get(self._field_value) is None: raise InvalidLogLevel +class SettingsTestConfigField(PyRockSettingsFieldBase): + def _get_value(self) -> Any: + return self._settings.get( + self._field_name + ) or self._default_value + + def _validate(self): + if not isinstance(self._field_value, dict): + raise InvalidTestConfig + + self.ENABLED = self._field_value.get('enabled', False) + self.TEST_FRAMEWORK = None + self.WORKING_DIR = None + self.TEST_RUNNER_COMMAND = None + + if self.ENABLED: + self.TEST_FRAMEWORK = self._field_value.get("test_framework") + if self.TEST_FRAMEWORK not in [PyRockConstants.DJANGO_TEST_FRAMEWORK, PyRockConstants.PYTEST_TEST_FRAMEWORK]: + raise InvalidTestConfig(f"Invalid test framework {self.TEST_FRAMEWORK}") + + self.WORKING_DIR = self._field_value.get("working_directory") + if self.WORKING_DIR is None or (self.WORKING_DIR and not os.path.exists(self.WORKING_DIR)): + raise InvalidTestConfig( + f"Invalid or not existing working directory {self.TEST_FRAMEWORK}" + ) + + self.TEST_RUNNER_COMMAND = self._field_value.get("test_runner_command") + if not isinstance(self.TEST_RUNNER_COMMAND, list): + raise InvalidTestConfig("Invalid runner command format") class PyRockSettings: def __init__(self): - PyRockSettings.parse() + self.parse() - @classmethod - def parse(cls): + def parse(self): settings = sublime.load_settings(PyRockConstants.PACKAGE_SETTING_NAME) - cls.IMPORT_SCAN_DEPTH = SettingsImportScanDepthField( + self.IMPORT_SCAN_DEPTH = SettingsImportScanDepthField( "import_scan_depth", settings, default_value=PyRockConstants.DEFAULT_IMPORT_SCAN_DEPTH, ) - cls.PYTHON_VIRTUAL_ENV_PATH = SettingsPythonVirtualEnvPathField( + self.PYTHON_VIRTUAL_ENV_PATH = SettingsPythonVirtualEnvPathField( "python_venv_path", settings, ) - cls.PYTHON_INTERPRETER_PATH = SettingsPythonInterpreterPathField( + self.PYTHON_INTERPRETER_PATH = SettingsPythonInterpreterPathField( "python_interpreter_path", settings, ) - cls.LOG_LEVEL = SettingsPythonLogLevel("log_level", settings, default_value="INFO") + self.LOG_LEVEL = SettingsPythonLogLevel( + "log_level", settings, default_value="INFO" + ) + + self.TEST_CONFIG = SettingsTestConfigField( + "test_config", settings, default_value={} + ) diff --git a/src/utils.py b/src/utils.py index 974971f..b83f826 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,3 +1,5 @@ +import os +import re import urllib.request import json import traceback @@ -74,3 +76,12 @@ def post(url, data, **kwargs): response = Network._make_request(url, data=data, **kwargs) return Network._parse_response(response) + + +def is_test_file(file_name: str): + test_file_name_regex = r'^test_[a-zA-Z_0-9]\w*\.py' + file_name = os.path.basename(file_name) + + if re.match(test_file_name_regex, file_name) is None: + return False + return True diff --git a/tests/fixtures/test_fixture.py b/tests/fixtures/test_fixture.py new file mode 100644 index 0000000..962e9a6 --- /dev/null +++ b/tests/fixtures/test_fixture.py @@ -0,0 +1,14 @@ +import time +from django.test import TestCase + + +class MyTestCase(TestCase): + def test_long_running_task(self): + # Simulate a long-running task + time.sleep(20) + # Add your actual test assertions here (if any) + self.assertEqual(1 + 1, 2) + + +def test_iam_alone(): + assert 1 + 1 == 2 diff --git a/tests/src/commands/test_annotate_and_test_runner.py b/tests/src/commands/test_annotate_and_test_runner.py new file mode 100644 index 0000000..6c06a78 --- /dev/null +++ b/tests/src/commands/test_annotate_and_test_runner.py @@ -0,0 +1,166 @@ +import os +from unittest.mock import patch + +import sublime +from PyRock.src.constants import PyRockConstants + +from tests.base import PyRockTestBase +from PyRock.src.commands.annotate_and_test_runner import AnnotateAndTestRunnerCommand + + +class TestAnnotateAndTestRunnerCommand(PyRockTestBase): + def setUp(self): + self.maxDiff = None + self.test_runner_cmd = AnnotateAndTestRunnerCommand(test=True) + + def tearDown(self): + pass + + def _open_test_fixture_file(self): + test_file_view = sublime.active_window().open_file( + fname=os.path.join( + PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, 'test_fixture.py' + ) + ) + + # wait for view to open + while test_file_view.is_loading(): + pass + + return test_file_view + + + @patch("PyRock.src.settings.SettingsTestConfigField._get_value") + def test_run(self, mocked_get_test_config): + mocked_get_test_config.return_value = { + "enabled": True, + "test_framework": "pytest", + "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, + "test_runner_command": ["pytest"] + } + test_file_view = self._open_test_fixture_file() + + region_key = f"pyrock-gutter-icon-{test_file_view.id()}" + + annotated_regions = test_file_view.get_regions(region_key) + + self.assertEqual(len(annotated_regions), 3) + + @patch("os.path.exists") + @patch("PyRock.src.commands.annotate_and_test_runner.AnnotateAndTestRunnerCommand._run_test_command") + @patch("sublime.load_settings") + def test_click_on_annotated_html( + self, + mocked_load_settings, + mocked_run_test_command, + mocked_os_path_exists, + ): + mocked_load_settings.return_value = { + "python_venv_path": "/Users/abhishek/venv/bin/activate", + "log_level": "debug", + "test_config": { + "enabled": True, + "test_framework": "pytest", + "working_directory": "/Users/abhishek/", + "test_runner_command": ["pytest"], + } + } + mocked_os_path_exists.return_value = True + + test_file_view = self._open_test_fixture_file() + + self.test_runner_cmd.run(test_file_view) + self.test_runner_cmd._execute_test("0") + + test_command = """ + set -e + . "/Users/abhishek/venv/bin/activate" + cd "/Users/abhishek/" + pytest tests/fixtures/test_fixture.py::MyTestCase + deactivate + """ + + mocked_run_test_command.assert_called_once_with( + test_command + ) + + @patch("PyRock.src.commands.output_panel.OutputPanel.show") + @patch("PyRock.src.settings.SettingsTestConfigField._get_value") + def test_run_test_command( + self, + mocked_get_test_config, + mocked_panel_show, + ): + mocked_get_test_config.return_value = { + "enabled": True, + "test_framework": "pytest", + "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, + "test_runner_command": ["pytest"] + } + + test_file_view = self._open_test_fixture_file() + + self.test_runner_cmd.run(test_file_view) + self.test_runner_cmd._execute_test("0") + + test_command = """ + set -e + . "/Users/abhishek/venv/bin/activate" + cd "/Users/abhishek/Library/Application Support/Sublime Text/Packages/PyRock/tests/fixtures" + pytest tests/fixtures/test_fixture.py::MyTestCase + deactivate + """ + + script_success, message = self.test_runner_cmd._run_test_command(test_command) + + self.assertFalse(script_success) + self.assertIsNotNone(message) + + output_panel_view = test_file_view.window().find_output_panel( + name=PyRockConstants.PACKAGE_TEST_RUNNER_OUTPUT_PANEL + ) + output_text = output_panel_view.substr( + sublime.Region(0, output_panel_view.size()) + ) + self.assertTrue(test_command in output_text) + + @patch("os.path.exists") + @patch("sublime.platform") + @patch("sublime.load_settings") + def test_get_test_command( + self, + mocked_load_settings, + mocked_platform, + mocked_os_path_exists, + ): + mocked_load_settings.return_value = { + "python_venv_path": "/Users/abhishek/venv/bin/activate", + "log_level": "debug", + "test_config": { + "enabled": True, + "test_framework": "pytest", + "working_directory": "/Users/abhishek/", + "test_runner_command": ["pytest"], + } + } + + mocked_platform.return_value = "windows" + + mocked_os_path_exists.return_value = True + + result = self.test_runner_cmd._get_test_command( + test_path="tests/fixtures/test_fixture.py::MyTestCase" + ) + + expected_test_command = [ + "/Users/abhishek/venv/bin/activate", + '&&', + 'cd', + "/Users/abhishek/", + '&&', + 'pytest', + 'tests/fixtures/test_fixture.py::MyTestCase', + 'deactivate' + ] + + self.assertEqual(result, expected_test_command) diff --git a/tests/src/commands/test_base_indexer.py b/tests/src/commands/test_base_indexer.py index dce2ac1..0642c9c 100644 --- a/tests/src/commands/test_base_indexer.py +++ b/tests/src/commands/test_base_indexer.py @@ -1,4 +1,3 @@ -import sublime from unittest import mock from tests.base import PyRockTestBase from PyRock.src.commands.base_indexer import BaseIndexer diff --git a/tests/src/commands/test_copy_test_path.py b/tests/src/commands/test_copy_test_path.py new file mode 100644 index 0000000..438fda2 --- /dev/null +++ b/tests/src/commands/test_copy_test_path.py @@ -0,0 +1,202 @@ +import os +from unittest.mock import patch + +import sublime +from PyRock.src.constants import PyRockConstants + +from tests.base import PyRockTestBase + + +class TestCopyTestPathCommand(PyRockTestBase): + def test_copy_django_class_test_path( + self, + ): + test_file_view = sublime.active_window().open_file( + fname=os.path.join( + PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, 'test_fixture.py' + ) + ) + + # wait for view to open + while test_file_view.is_loading(): + pass + + test_file_view.sel().clear() + test_file_view.sel().add(sublime.Region(53, 63)) + + selected_text = test_file_view.substr(test_file_view.sel()[0]) + self.assertEqual(selected_text, "MyTestCase") + + test_file_view.run_command( + "py_rock", args={"action": "copy_test_path", "test": True}) + + expected_import_statement = sublime.get_clipboard() + self.assertEqual( + expected_import_statement, "tests.fixtures.test_fixture.MyTestCase" + ) + + def test_copy_django_class_method_test_path( + self, + ): + + test_file_view = sublime.active_window().open_file( + fname=os.path.join( + PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, 'test_fixture.py' + ) + ) + + # wait for view to open + while test_file_view.is_loading(): + pass + + test_file_view.sel().clear() + test_file_view.sel().add(sublime.Region(83, 105)) + + selected_text = test_file_view.substr(test_file_view.sel()[0]) + self.assertEqual(selected_text, "test_long_running_task") + + test_file_view.run_command( + "py_rock", args={"action": "copy_test_path", "test": True}) + + expected_import_statement = sublime.get_clipboard() + self.assertEqual( + expected_import_statement, + "tests.fixtures.test_fixture.MyTestCase.test_long_running_task" + ) + + def test_copy_django_individual_method_test_path( + self, + ): + test_file_view = sublime.active_window().open_file( + fname=os.path.join( + PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, 'test_fixture.py' + ) + ) + + # wait for view to open + while test_file_view.is_loading(): + pass + + test_file_view.sel().clear() + test_file_view.sel().add(sublime.Region(272, 286)) + + selected_text = test_file_view.substr(test_file_view.sel()[0]) + self.assertEqual(selected_text, "test_iam_alone") + + test_file_view.run_command( + "py_rock", args={"action": "copy_test_path", "test": True}) + + expected_import_statement = sublime.get_clipboard() + self.assertEqual( + expected_import_statement, "tests.fixtures.test_fixture.test_iam_alone" + ) + + @patch("PyRock.src.settings.SettingsTestConfigField._get_value") + def test_copy_pytest_class_test_path( + self, + mocked_get_test_config, + ): + test_file_view = sublime.active_window().open_file( + fname=os.path.join( + PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, 'test_fixture.py' + ) + ) + + # wait for view to open + while test_file_view.is_loading(): + pass + + mocked_get_test_config.return_value = { + "enabled": True, + "test_framework": "pytest", + "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, + "test_runner_command": ["pytest"] + } + + test_file_view.sel().clear() + test_file_view.sel().add(sublime.Region(53, 63)) + + selected_text = test_file_view.substr(test_file_view.sel()[0]) + self.assertEqual(selected_text, "MyTestCase") + + test_file_view.run_command( + "py_rock", args={"action": "copy_test_path", "test": True}) + + expected_import_statement = sublime.get_clipboard() + self.assertEqual( + expected_import_statement, "tests/fixtures/test_fixture.py::MyTestCase" + ) + + @patch("PyRock.src.settings.SettingsTestConfigField._get_value") + def test_copy_pytest_class_method_test_path( + self, + mocked_get_test_config, + ): + test_file_view = sublime.active_window().open_file( + fname=os.path.join( + PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, 'test_fixture.py' + ) + ) + + # wait for view to open + while test_file_view.is_loading(): + pass + + mocked_get_test_config.return_value = { + "enabled": True, + "test_framework": "pytest", + "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, + "test_runner_command": ["pytest"] + } + + test_file_view.sel().clear() + test_file_view.sel().add(sublime.Region(83, 105)) + + selected_text = test_file_view.substr(test_file_view.sel()[0]) + self.assertEqual(selected_text, "test_long_running_task") + + test_file_view.run_command( + "py_rock", args={"action": "copy_test_path", "test": True}) + + expected_import_statement = sublime.get_clipboard() + self.assertEqual( + expected_import_statement, + "tests/fixtures/test_fixture.py::MyTestCase::test_long_running_task" + ) + + @patch("PyRock.src.settings.SettingsTestConfigField._get_value") + def test_copy_pytest_individual_method_test_path( + self, + mocked_get_test_config, + ): + test_file_view = sublime.active_window().open_file( + fname=os.path.join( + PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, 'test_fixture.py' + ) + ) + + # wait for view to open + while test_file_view.is_loading(): + pass + + mocked_get_test_config.return_value = { + "enabled": True, + "test_framework": "pytest", + "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, + "test_runner_command": ["pytest"] + } + + test_file_view.sel().clear() + test_file_view.sel().add(sublime.Region(272, 286)) + + selected_text = test_file_view.substr(test_file_view.sel()[0]) + self.assertEqual(selected_text, "test_iam_alone") + + test_file_view.run_command( + "py_rock", args={"action": "copy_test_path", "test": True}) + + expected_import_statement = sublime.get_clipboard() + self.assertEqual( + expected_import_statement, + "tests/fixtures/test_fixture.py::test_iam_alone" + ) diff --git a/tests/src/commands/test_import_symbol.py b/tests/src/commands/test_import_symbol.py index 6d84341..508b6db 100644 --- a/tests/src/commands/test_import_symbol.py +++ b/tests/src/commands/test_import_symbol.py @@ -108,3 +108,32 @@ def test_add_module_import_in_existing_import( ) ) self.assertEqual(expected_import_statement, "from cmath import sin, log10") + + @patch("PyRock.src.commands.import_symbol.ImportSymbolCommand.load_user_python_imports") + def test_copy_import_symbol( + self, + mocked_load_user_python_imports, + ): + mocked_load_user_python_imports.return_value = { + "c": { + "h": [ + "cmath" + ] + } + } + + insert_text = "cmath" + self.setText(insert_text) + + self.view.sel().clear() + self.view.sel().add(sublime.Region(0, len(insert_text))) + + selected_text = self.view.substr(self.view.sel()[0]) + self.assertEqual(selected_text, "cmath") + + self.view.run_command( + "py_rock", args={"action": "copy_import_symbol", "test": True} + ) + + expected_import_statement = sublime.get_clipboard() + self.assertEqual(expected_import_statement, "import cmath") diff --git a/tests/src/commands/test_re_index_imports.py b/tests/src/commands/test_re_index_imports.py new file mode 100644 index 0000000..ee78393 --- /dev/null +++ b/tests/src/commands/test_re_index_imports.py @@ -0,0 +1,29 @@ +from unittest.mock import patch + +from tests.base import PyRockTestBase + + +class TestReIndexImportsCommand(PyRockTestBase): + def setUp(self): + super().setUp() + + def setText(self, string): + self.view.run_command("insert", {"characters": string}) + + @patch("sublime.set_timeout_async") + @patch("sublime.ok_cancel_dialog") + def test_command( + self, + mocked_ok_cancel_dialog, + mocked_set_timeout_async, + ): + mocked_ok_cancel_dialog.return_value = True + self.view.run_command("py_rock", args={"action": "re_index_imports"}) + + mocked_ok_cancel_dialog.assert_called_once_with( + msg="Are you sure to re-index imports?", + ok_title='Yes', + title='Re-Index Imports' + ) + + self.assertEqual(mocked_set_timeout_async.call_count, 1)