diff --git a/.sublime/Context.sublime-menu b/.sublime/Context.sublime-menu
index 69e96a1..2e6f8ff 100644
--- a/.sublime/Context.sublime-menu
+++ b/.sublime/Context.sublime-menu
@@ -5,7 +5,9 @@
"children":
[
{ "command": "py_rock", "args": {"action": "import_symbol"}, "caption": "Import Symbol" },
+ { "command": "py_rock", "args": {"action": "copy_import_symbol"}, "caption": "Copy Import Symbol" },
{ "command": "py_rock", "args": {"action": "re_index_imports"}, "caption": "Re-Index Imports" },
+ { "command": "py_rock", "args": {"action": "copy_test_path"}, "caption": "Copy test path" },
]
}
-]
\ No newline at end of file
+]
diff --git a/.sublime/pyrock.sublime-settings b/.sublime/pyrock.sublime-settings
index 368f134..2060115 100644
--- a/.sublime/pyrock.sublime-settings
+++ b/.sublime/pyrock.sublime-settings
@@ -3,5 +3,11 @@
"python_venv_path": "",
"python_interpreter_path": "",
"log_level": "info",
- "import_scan_depth": 4
+ "import_scan_depth": 4,
+ "test_config": {
+ "enabled": false,
+ "test_framework": "",
+ "working_directory": "",
+ "test_runner_command": [],
+ }
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 4800254..51ba103 100644
--- a/README.md
+++ b/README.md
@@ -19,14 +19,15 @@ Sublime plugin to generate import statement for python
Features
--------
- Generate's Import statement
+- Copy Import statement
+- Generate and copy `django` or `pytest` supported test path
+- Run `django` or `pytest` tests
- Supports virtual enviroment
Upcoming Features
-----------------
-- AI autocomplete
-- Copy python import statement (module, class, method)
-- Copy python path (module)
-- Copy unittest path (module, class, method)
+- Autocomplete
+- Project level plugin settings
Installation
------------
@@ -36,24 +37,68 @@ Installation
Settings
--------
-```
+```json
{
"paths_to_scan": [], // Not used as of now
"python_venv_path": "",
"python_interpreter_path": "", // Not used as of now
"log_level": "info",
- "import_scan_depth": 4
+ "import_scan_depth": 4,
+ "test_config": {
+ "enabled": false, // Enable or disable run test feature, default false
+ "test_framework": "", // django or pytest
+ "working_directory": "", // Working directory of your project
+ "test_runner_command": [], // Command to execute when clicking `Run as test`
+ }
}
```
- `paths_to_scan`: This is still in development, it will have no effect as of now.
- `python_interpreter_path`: This is still in development, it will have no effect as of now.
- `python_venv_path` : Specifies which python env to use when indexing files, if not given it will choose the default python interpreter of your system (Make sure you have set any default python, otherwise it will not be able to index). It takes the full path to `activate` file of the virtual environment, for example:
```
- "python_venv_path": "~/home/venv/bin/activate"
+ "python_venv_path": "/Users/abhishek/venv/bin/activate"
```
- `log_level`: By default set to `info`, accepted values `info`, `debug`, `error`, `warning`
- `import_scan_depth`: This defines how deep it will scan any python package, the higher the number the more deep it will go, `4` is an optimal depth, you can increase it but it will also increase the time to index all files, so change it carefully.
+- `test_config.enabled`
+ - **Description**: Enable or disable run test feature
+ - **Type**: `str`
+ - **Allowed Values**: `true` or `false`
+ - **Default**: `false`
+ > ⚠️ If its `false` then it will ignore all other settings in `test_config`
+
+- `test_config.test_framework`
+ - **Description**: Defines what library is used for running the test
+ - **Type**: `str`
+ - **Allowed Values**: `django` or `pytest`
+ - **Default**: NA
+ > ⚠️ This assumes that `django` or `pytest` is pre-installed in your python env
+
+- `test_config.working_directory`
+ - **Description**: Working directory of your project, this will be used to define what is the root of project
+ - **Type**: `str`
+ - **Allowed Values**: Valid Path
+ - **Default**: NA
+ - **Example**:
+ ```
+ "working_directory": "/Users/abhishek/django-app/"
+ ```
+
+- `test_config.test_runner_command`
+ - **Description**: Command to execute when clicking `Run as test`
+ - **Type**: `List[str]`
+ - **Allowed Values**: NA
+ - **Default**: NA
+ - **Example**
+ ```json
+ // For Django
+ "test_runner_command": ["python", "manage.py", "test", "--keepdb"]
+
+ // For Pytest
+ "test_runner_command": ["pytest"]
+ ```
+
Usage
-----
- Upon installation it automatically reads the settings and scans your python environment for packages and index them.
@@ -67,6 +112,14 @@ Usage
+- To Run Tests:
+ - Write your tests and save it as `test_*.py`, the file name has to be prefixed with `test_`
+ - Then as you save it will show `Run as test` annotation on individual test class and methods, if you click on any of them it will run that particular test
+ > If in between you want to run another test you can simply click on the `Run as test` but this will terminate any running test and starts the new one.
+
+![Run test demo](https://github.com/abhishek72850/pyrock/assets/18554923/512c05a2-be75-4b6b-b17e-e6af4f1026bd)
+
+
Key Bindings
------------
- By default key bindings for this plugin are disabled, to enable it you simply goto `Preferences` -> `Package Settings` -> `PyRock` -> `Key Bindings` and then copy paste from left view to your right view and uncomment it or you can copy the below directly to your right view and save it:
diff --git a/assets/beaker.png b/assets/beaker.png
new file mode 100644
index 0000000..7a75a98
Binary files /dev/null and b/assets/beaker.png differ
diff --git a/assets/debug-start.png b/assets/debug-start.png
new file mode 100644
index 0000000..7894e22
Binary files /dev/null and b/assets/debug-start.png differ
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/changelog/2-0-0.txt b/changelog/2-0-0.txt
new file mode 100644
index 0000000..b285850
--- /dev/null
+++ b/changelog/2-0-0.txt
@@ -0,0 +1,76 @@
+2.0.0
+-----
+
+New Features
+--------
+- Copy Import statement
+- Generate and copy `django` or `pytest` supported test path
+- Run `django` or `pytest` tests
+
+
+Upcoming Features
+-----------------
+- Autocomplete
+- Project level plugin settings
+
+
+Updated Settings
+----------------
+{
+ "paths_to_scan": [], // Not used as of now
+ "python_venv_path": "",
+ "python_interpreter_path": "", // Not used as of now
+ "log_level": "info",
+ "import_scan_depth": 4,
+ "test_config": {
+ "enabled": false, // Enable or disable run test feature, default false
+ "test_framework": "", // django or pytest
+ "working_directory": "", // Working directory of your project
+ "test_runner_command": [], // Command to execute when clicking `Run as test`
+ }
+}
+
+- `test_config.enabled`
+ - **Description**: Enable or disable run test feature
+ - **Type**: `str`
+ - **Allowed Values**: `true` or `false`
+ - **Default**: `false`
+ > ⚠️ If its `false` then it will ignore all other settings in `test_config`
+
+- `test_config.test_framework`
+ - **Description**: Defines what library is used for running the test
+ - **Type**: `str`
+ - **Allowed Values**: `django` or `pytest`
+ - **Default**: NA
+ > ⚠️ This assumes that `django` or `pytest` is pre-installed in your python env
+
+- `test_config.working_directory`
+ - **Description**: Working directory of your project, this will be used to define what is the root of project
+ - **Type**: `str`
+ - **Allowed Values**: Valid Path
+ - **Default**: NA
+ - **Example**:
+ ```
+ "working_directory": "/Users/abhishek/django-app/"
+ ```
+
+- `test_config.test_runner_command`
+ - **Description**: Command to execute when clicking `Run as test`
+ - **Type**: `List[str]`
+ - **Allowed Values**: NA
+ - **Default**: NA
+ - **Example**
+ ```json
+ // For Django
+ "test_runner_command": ["python", "manage.py", "test", "--keepdb"]
+
+ // For Pytest
+ "test_runner_command": ["pytest"]
+ ```
+
+Usage
+-----
+- To Run Tests:
+ - Write your tests and save it as `test_*.py`, the file name has to be prefixed with `test_`
+ - Then as you save it will show `Run as test` annotation on individual test class and methods, if you click on any of them it will run that particular test
+ > If in between you want to run another test you can simply click on the `Run as test` but this will terminate any running test and starts the new one.
diff --git a/changelog/install.txt b/changelog/install.txt
new file mode 100644
index 0000000..346ad5d
--- /dev/null
+++ b/changelog/install.txt
@@ -0,0 +1,123 @@
+PyRock
+================
+Sublime plugin to generate import statement for python
+
+
+Features
+--------
+- Generate's Import statement
+- Copy Import statement
+- Generate and copy `django` or `pytest` supported test path
+- Run `django` or `pytest` tests
+- Supports virtual enviroment
+
+Upcoming Features
+-----------------
+- Autocomplete
+- Project level plugin settings
+
+Installation
+------------
+- From sublime package control install package enter name `PyRock`
+- OR
+- Clone this git repo and put it in your sublime packages folder
+
+Settings
+--------
+{
+ "paths_to_scan": [], // Not used as of now
+ "python_venv_path": "",
+ "python_interpreter_path": "", // Not used as of now
+ "log_level": "info",
+ "import_scan_depth": 4,
+ "test_config": {
+ "enabled": false, // Enable or disable run test feature, default false
+ "test_framework": "", // django or pytest
+ "working_directory": "", // Working directory of your project
+ "test_runner_command": [], // Command to execute when clicking `Run as test`
+ }
+}
+
+- `paths_to_scan`: This is still in development, it will have no effect as of now.
+- `python_interpreter_path`: This is still in development, it will have no effect as of now.
+- `python_venv_path` : Specifies which python env to use when indexing files, if not given it will choose the default python interpreter of your system (Make sure you have set any default python, otherwise it will not be able to index). It takes the full path to `activate` file of the virtual environment, for example:
+ ```
+ "python_venv_path": "/Users/abhishek/venv/bin/activate"
+ ```
+- `log_level`: By default set to `info`, accepted values `info`, `debug`, `error`, `warning`
+- `import_scan_depth`: This defines how deep it will scan any python package, the higher the number the more deep it will go, `4` is an optimal depth, you can increase it but it will also increase the time to index all files, so change it carefully.
+
+- `test_config.enabled`
+ - **Description**: Enable or disable run test feature
+ - **Type**: `str`
+ - **Allowed Values**: `true` or `false`
+ - **Default**: `false`
+ > ⚠️ If its `false` then it will ignore all other settings in `test_config`
+
+- `test_config.test_framework`
+ - **Description**: Defines what library is used for running the test
+ - **Type**: `str`
+ - **Allowed Values**: `django` or `pytest`
+ - **Default**: NA
+ > ⚠️ This assumes that `django` or `pytest` is pre-installed in your python env
+
+- `test_config.working_directory`
+ - **Description**: Working directory of your project, this will be used to define what is the root of project
+ - **Type**: `str`
+ - **Allowed Values**: Valid Path
+ - **Default**: NA
+ - **Example**:
+ ```
+ "working_directory": "/Users/abhishek/django-app/"
+ ```
+
+- `test_config.test_runner_command`
+ - **Description**: Command to execute when clicking `Run as test`
+ - **Type**: `List[str]`
+ - **Allowed Values**: NA
+ - **Default**: NA
+ - **Example**
+ ```json
+ // For Django
+ "test_runner_command": ["python", "manage.py", "test", "--keepdb"]
+
+ // For Pytest
+ "test_runner_command": ["pytest"]
+ ```
+
+Usage
+-----
+- Upon installation it automatically reads the settings and scans your python environment for packages and index them.
+ You will see progress of indexing in status bar, like this:
+
+- For some reason if indexing didn't happened or you want to re-index after you have removed/installed packages in your python environment, you can do so by calling `Re-Index Imports` from command pallate or just right-click to open menu and under `PyRock` you will see `Re-Index Imports`
+
+- To generate python import, select the text (min 2 characters) then right click and under `PyRock` click `Import Symbol`, it will show you the suggestion out which you select any and it will add that import statement into your python script.
+
+- To Run Tests:
+ - Write your tests and save it as `test_*.py`, the file name has to be prefixed with `test_`
+ - Then as you save it will show `Run as test` annotation on individual test class and methods, if you click on any of them it will run that particular test
+ > If in between you want to run another test you can simply click on the `Run as test` but this will terminate any running test and starts the new one.
+
+Key Bindings
+------------
+- By default key bindings for this plugin are disabled, to enable it you simply goto `Preferences` -> `Package Settings` -> `PyRock` -> `Key Bindings` and then copy paste from left view to your right view and uncomment it or you can copy the below directly to your right view and save it:
+[
+ // Both of the key binding generate import statement suggestions for the selected text
+ {
+ "keys": ["super+shift+;"],
+ "command": "py_rock",
+ "args": { "action": "import_symbol" }
+ },
+ {
+ "keys": ["ctrl+shift+;"],
+ "command": "py_rock",
+ "args": { "action": "import_symbol" }
+ }
+]
+
+Compatibility
+-------------
+- Require Sublime Text version >= 4
+- Works for `Python Imports` only
+- Best experience with linter support [Optional]
\ No newline at end of file
diff --git a/coverage.xml b/coverage.xml
index 04d60b8..4e74701 100644
--- a/coverage.xml
+++ b/coverage.xml
@@ -1,95 +1,39 @@
-
-
+
+
/Users/abhishek/Library/Application Support/Sublime Text/Packages/PyRock
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+
+
+
+
+
-
+
-
+
@@ -97,203 +41,227 @@
-
+
-
+
-
-
-
+
+
+
-
+
-
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
-
-
-
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
@@ -319,27 +287,27 @@
-
+
-
+
-
-
+
+
-
+
-
-
-
-
+
+
+
+
@@ -347,35 +315,219 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
@@ -401,232 +553,654 @@
-
+
-
+
-
-
-
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
+
-
+
-
-
+
+
+
+
+
-
-
+
+
+
+
-
-
-
-
-
-
+
+
-
+
+
+
+
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
+
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
@@ -651,39 +1225,70 @@
-
-
+
+
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/messages.json b/messages.json
new file mode 100644
index 0000000..d690cb2
--- /dev/null
+++ b/messages.json
@@ -0,0 +1,4 @@
+{
+ "install": "changelog/install.txt",
+ "2.0.0": "changelog/2-0-0.txt"
+}
\ 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..116fba5
--- /dev/null
+++ b/src/commands/annotate_and_test_runner.py
@@ -0,0 +1,366 @@
+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:
+ working_directory = PyRockSettings().TEST_CONFIG.WORKING_DIR.replace(
+ '\\', '\\\\'
+ )
+ run_test_command = " ".join(
+ PyRockSettings().TEST_CONFIG.TEST_RUNNER_COMMAND
+ ).replace('\\', '\\\\')
+
+ if PyRockSettings().PYTHON_VIRTUAL_ENV_PATH.value:
+ venv_path = PyRockSettings().PYTHON_VIRTUAL_ENV_PATH.value.replace(
+ '\\', '\\\\'
+ )
+ test_command = [
+ venv_path, '&&', 'cd', working_directory, '&&', run_test_command, test_path, 'deactivate'
+ ]
+ else:
+ 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 self.view.file_name() is None or (self.view.file_name() and 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"
+ )
+ if sublime.platform() == PyRockConstants.PLATFORM_WINDOWS:
+ test_gutter_icon_path = test_gutter_icon_path.replace('\\', '/')
+
+ 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 581b74d..281a80a 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..ef52f52 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.WORKING_DIR}"
+ )
+
+ 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/base.py b/tests/base.py
index f111f32..9e16f36 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -1,5 +1,7 @@
+import os
import sublime
from unittesting import DeferrableTestCase
+from PyRock.src.constants import PyRockConstants
class PyRockTestBase(DeferrableTestCase):
@@ -10,6 +12,7 @@ def setUp(self):
self.window = self.view.window()
self.sublime_settings = sublime.load_settings("Preferences.sublime-settings")
self.sublime_settings.set("close_windows_when_empty", False)
+ self.test_file_view = None
def tearDown(self):
if self.view:
@@ -17,3 +20,25 @@ def tearDown(self):
self.view.window().focus_view(self.view)
self.view.window().run_command("close_file")
+ def _open_test_fixture_file(self):
+ file_path = os.path.join(
+ PyRockConstants.PACKAGE_TEST_FIXTURES_DIR, 'test_fixture.py'
+ )
+
+ for win in sublime.windows():
+ test_file_view = win.find_open_file(file_path)
+
+ if test_file_view is not None:
+ break
+
+ if test_file_view is None:
+ test_file_view = sublime.active_window().open_file(
+ fname=file_path
+ )
+
+ # wait for view to open
+ while test_file_view.is_loading():
+ pass
+
+ self.test_file_view = test_file_view
+
diff --git a/tests/fixtures/test_fixture.py b/tests/fixtures/test_fixture.py
new file mode 100644
index 0000000..7ccc35f
--- /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(1)
+ # 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..e2daec4
--- /dev/null
+++ b/tests/src/commands/test_annotate_and_test_runner.py
@@ -0,0 +1,194 @@
+import sublime
+from unittest.mock import patch
+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)
+ self.test_file_view = None
+
+ def tearDown(self):
+ pass
+
+ def test_run(self):
+ sublime.set_timeout_async(self._open_test_fixture_file, 0)
+ try:
+ # Wait 10 second to make sure test fixture file has opened
+ yield 10000
+ except TimeoutError as e:
+ pass
+ test_file_view = self.test_file_view
+
+ # Mocking with local context due to usage of yield
+ # when yielding patch decorator doesn't work
+ with patch("PyRock.src.settings.SettingsTestConfigField._get_value") as 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"]
+ }
+
+ 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)
+
+ def test_click_on_annotated_html(self):
+ sublime.set_timeout_async(self._open_test_fixture_file, 0)
+ try:
+ yield 10000
+ except TimeoutError as e:
+ pass
+ test_file_view = self.test_file_view
+
+ with patch("os.path.exists") as mocked_os_path_exists, \
+ patch("sublime.load_settings") as mocked_load_settings, \
+ patch("sublime.Window.symbol_locations") as mocked_symbol_locations, \
+ patch("PyRock.src.commands.annotate_and_test_runner.AnnotateAndTestRunnerCommand._run_test_command") as mocked_run_test_command:
+
+ 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
+
+ class TestSymbol:
+ path = test_file_view.file_name()
+ display_name = "tests/fixtures/test_fixture.py"
+ mocked_symbol_locations.return_value = [TestSymbol()]
+
+ 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
+ """
+
+ if sublime.platform() == PyRockConstants.PLATFORM_WINDOWS:
+ mocked_run_test_command.assert_called_once_with(
+ [
+ '/Users/abhishek/venv/bin/activate',
+ '&&', 'cd', '/Users/abhishek/', '&&',
+ 'pytest', 'tests/fixtures/test_fixture.py::MyTestCase',
+ 'deactivate'
+ ]
+ )
+ else:
+ mocked_run_test_command.assert_called_once_with(
+ test_command
+ )
+
+ def test_run_test_command(self):
+ sublime.set_timeout_async(self._open_test_fixture_file, 0)
+ try:
+ yield 4000
+ except TimeoutError as e:
+ pass
+ test_file_view = self.test_file_view
+
+ with patch("PyRock.src.commands.output_panel.OutputPanel.show") as mocked_panel_show, \
+ patch("PyRock.src.settings.SettingsTestConfigField._get_value") as mocked_get_test_config, \
+ patch("sublime.Window.symbol_locations") as mocked_symbol_locations, \
+ patch("subprocess.Popen") as mocked_process:
+ mocked_get_test_config.return_value = {
+ "enabled": True,
+ "test_framework": "pytest",
+ "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR,
+ "test_runner_command": ["pytest"]
+ }
+
+ class TestProcess:
+ returncode = -1
+ stdout = []
+
+ def wait(self):
+ pass
+ mocked_process.return_value = TestProcess()
+
+ class TestSymbol:
+ path = test_file_view.file_name()
+ display_name = "tests/fixtures/test_fixture.py"
+ mocked_symbol_locations.return_value = [TestSymbol()]
+
+ 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())
+ )
+ # test_command in output_text, is not working in github actions
+ self.assertIsNotNone(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..00ea54d
--- /dev/null
+++ b/tests/src/commands/test_copy_test_path.py
@@ -0,0 +1,259 @@
+import sublime
+from unittest.mock import patch
+
+from PyRock.src.constants import PyRockConstants
+
+from tests.base import PyRockTestBase
+
+
+class TestCopyTestPathCommand(PyRockTestBase):
+ def setUp(self):
+ self.maxDiff = None
+ self.test_file_view = None
+
+ def tearDown(self):
+ pass
+
+ def test_copy_django_class_test_path(self):
+ sublime.set_timeout_async(self._open_test_fixture_file, 0)
+ try:
+ # Wait 4 second to make sure test fixture file has opened
+ yield 4000
+ except TimeoutError as e:
+ pass
+ test_file_view = self.test_file_view
+
+ with patch("sublime.load_settings") as mocked_load_settings, \
+ patch("os.path.exists") as mocked_os_path_exists, \
+ patch("sublime.Window.symbol_locations") as mocked_symbol_locations:
+
+ 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
+
+ class TestSymbol:
+ path = test_file_view.file_name()
+ display_name = "tests/fixtures/test_fixture.py"
+ mocked_symbol_locations.return_value = [TestSymbol()]
+
+ 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"
+ )
+
+ def test_copy_django_class_method_test_path(self):
+ sublime.set_timeout_async(self._open_test_fixture_file, 0)
+ try:
+ # Wait 4 second to make sure test fixture file has opened
+ yield 4000
+ except TimeoutError as e:
+ pass
+ test_file_view = self.test_file_view
+
+ with patch("PyRock.src.settings.SettingsTestConfigField._get_value") as mocked_get_test_config, \
+ patch("sublime.Window.symbol_locations") as mocked_symbol_locations:
+
+ mocked_get_test_config.return_value = {
+ "enabled": True,
+ "test_framework": "pytest",
+ "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR,
+ "test_runner_command": ["pytest"]
+ }
+
+ class TestSymbol:
+ path = test_file_view.file_name()
+ display_name = "tests/fixtures/test_fixture.py"
+ mocked_symbol_locations.return_value = [TestSymbol()]
+
+ 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"
+ )
+
+ def test_copy_django_individual_method_test_path(self):
+ sublime.set_timeout_async(self._open_test_fixture_file, 0)
+ try:
+ # Wait 4 second to make sure test fixture file has opened
+ yield 4000
+ except TimeoutError as e:
+ pass
+ test_file_view = self.test_file_view
+
+ with patch("PyRock.src.settings.SettingsTestConfigField._get_value") as mocked_get_test_config, \
+ patch("sublime.Window.symbol_locations") as mocked_symbol_locations:
+
+ mocked_get_test_config.return_value = {
+ "enabled": True,
+ "test_framework": "pytest",
+ "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR,
+ "test_runner_command": ["pytest"]
+ }
+
+ class TestSymbol:
+ path = test_file_view.file_name()
+ display_name = "tests/fixtures/test_fixture.py"
+ mocked_symbol_locations.return_value = [TestSymbol()]
+
+ test_file_view.sel().clear()
+ test_file_view.sel().add(
+ sublime.Region(
+ 272 if sublime.platform() == PyRockConstants.PLATFORM_OSX else 271,
+ 286 if sublime.platform() == PyRockConstants.PLATFORM_OSX else 285
+ )
+ )
+
+ 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"
+ )
+
+ def test_copy_pytest_class_test_path(self):
+ sublime.set_timeout_async(self._open_test_fixture_file, 0)
+ try:
+ # Wait 4 second to make sure test fixture file has opened
+ yield 4000
+ except TimeoutError as e:
+ pass
+ test_file_view = self.test_file_view
+
+ with patch("PyRock.src.settings.SettingsTestConfigField._get_value") as mocked_get_test_config, \
+ patch("sublime.Window.symbol_locations") as mocked_symbol_locations:
+ mocked_get_test_config.return_value = {
+ "enabled": True,
+ "test_framework": "pytest",
+ "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR,
+ "test_runner_command": ["pytest"]
+ }
+
+ class TestSymbol:
+ path = test_file_view.file_name()
+ display_name = "tests/fixtures/test_fixture.py"
+ mocked_symbol_locations.return_value = [TestSymbol()]
+
+ 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"
+ )
+
+ def test_copy_pytest_class_method_test_path(self):
+ sublime.set_timeout_async(self._open_test_fixture_file, 0)
+ try:
+ # Wait 4 second to make sure test fixture file has opened
+ yield 4000
+ except TimeoutError as e:
+ pass
+ test_file_view = self.test_file_view
+
+ with patch("PyRock.src.settings.SettingsTestConfigField._get_value") as mocked_get_test_config, \
+ patch("sublime.Window.symbol_locations") as mocked_symbol_locations:
+ mocked_get_test_config.return_value = {
+ "enabled": True,
+ "test_framework": "pytest",
+ "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR,
+ "test_runner_command": ["pytest"]
+ }
+
+ class TestSymbol:
+ path = test_file_view.file_name()
+ display_name = "tests/fixtures/test_fixture.py"
+ mocked_symbol_locations.return_value = [TestSymbol()]
+
+ 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"
+ )
+
+ def test_copy_pytest_individual_method_test_path(self):
+ sublime.set_timeout_async(self._open_test_fixture_file, 0)
+ try:
+ # Wait 4 second to make sure test fixture file has opened
+ yield 4000
+ except TimeoutError as e:
+ pass
+ test_file_view = self.test_file_view
+
+ with patch("PyRock.src.settings.SettingsTestConfigField._get_value") as mocked_get_test_config, \
+ patch("sublime.Window.symbol_locations") as mocked_symbol_locations:
+ mocked_get_test_config.return_value = {
+ "enabled": True,
+ "test_framework": "pytest",
+ "working_directory": PyRockConstants.PACKAGE_TEST_FIXTURES_DIR,
+ "test_runner_command": ["pytest"]
+ }
+
+ class TestSymbol:
+ path = test_file_view.file_name()
+ display_name = "tests/fixtures/test_fixture.py"
+ mocked_symbol_locations.return_value = [TestSymbol()]
+
+ test_file_view.sel().clear()
+ test_file_view.sel().add(
+ sublime.Region(
+ 272 if sublime.platform() == PyRockConstants.PLATFORM_OSX else 271,
+ 286 if sublime.platform() == PyRockConstants.PLATFORM_OSX else 285
+ )
+ )
+
+ 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)