├── .gitignore ├── Default.sublime-commands ├── Default.sublime-keymap ├── Main.sublime-menu ├── Readme.md ├── TestRSpec.sublime-settings ├── messages.json ├── messages ├── 1.0.1.txt ├── 1.0.10.txt ├── 1.0.2.txt ├── 1.0.3.txt ├── 1.0.4.txt ├── 1.0.5.txt ├── 1.0.6.txt ├── 1.0.7.txt ├── 1.0.8.txt ├── 1.0.9.txt ├── 2.0.0.txt └── install.txt ├── plugin_helpers ├── __init__.py ├── open_file.py ├── project_files.py └── utils.py ├── recordings └── features.gif ├── rspec ├── __init__.py ├── create_spec_file.py ├── execute_spec.py ├── files │ ├── __init__.py │ ├── opposite.py │ ├── save.py │ ├── source.py │ └── spec.py ├── last_copy.py ├── last_run.py ├── output.py ├── package_root.py ├── project_root.py ├── rspec_print.py ├── spec_command.py ├── spec_commands │ ├── __init__.py │ ├── bin_rspec.py │ ├── bundle.py │ ├── rbenv.py │ ├── ruby_rspec.py │ ├── rvm.py │ ├── spring.py │ └── system_ruby.py ├── switch_between_code_and_test.py └── task_context.py ├── test_rspec.py └── themes ├── RSpecConsole.sublime-syntax └── syntax_test_rspecconsole.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "TestRSpec: Test Current Line", "command": "test_current_line" }, 3 | { "caption": "TestRSpec: Test Current File", "command": "test_current_file" }, 4 | { "caption": "TestRSpec: Run last spec", "command": "run_last_spec" }, 5 | { "caption": "TestRSpec: Copy last command", "command": "copy_last" }, 6 | { "caption": "TestRSpec: Display output panel", "command": "display_output_panel" }, 7 | { "caption": "TestRSpec: Switch between code and test", "command": "switch_between_code_and_test" }, 8 | { "caption": "TestRSpec: Create spec file", "command": "create_spec_file" }, 9 | ] 10 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | // This is the keymap used by TestRSpec by default before version 2.0. 3 | // 4 | // Some of these clash with the default Sublime key bindings, and are now 5 | // disabled by default because of that. 6 | 7 | 8 | 9 | // // test current line 10 | // { 11 | // "keys": ["super+shift+r"], 12 | // "command": "test_current_line", 13 | // "context": [ 14 | // { 15 | // "key": "selector", 16 | // "operator": "equal", 17 | // "operand": "source.ruby, source.rspec" 18 | // } 19 | // ] 20 | // }, 21 | 22 | // // test current file 23 | // { 24 | // "keys": ["super+shift+t"], 25 | // "command": "test_current_file", 26 | // "context": [ 27 | // { 28 | // "key": "selector", 29 | // "operator": "equal", 30 | // "operand": "source.ruby, source.rspec" 31 | // } 32 | // ] 33 | // }, 34 | 35 | // // run last spec 36 | // { "keys": ["super+shift+e"], "command": "run_last_spec" }, 37 | 38 | // // copy last command 39 | // { "keys": ["super+shift+,"], "command": "copy_last" }, 40 | 41 | // // display output panel 42 | // { "keys": ["super+shift+x"], "command": "display_output_panel" }, 43 | 44 | // // switch between code and test 45 | // { 46 | // "keys": ["super+period"], 47 | // "command": "switch_between_code_and_test", 48 | // "context": [ 49 | // { 50 | // "key": "selector", "operator": "equal", 51 | // "operand": "source.ruby, source.rspec" 52 | // } 53 | // ] 54 | // }, 55 | 56 | // // create spec file 57 | // { "keys": ["super+shift+c"], "command": "create_spec_file" }, 58 | ] 59 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "tools", 4 | "children": [ 5 | { 6 | "id": "TestRSpec", 7 | "caption": "TestRSpec", 8 | "children": [ 9 | { 10 | "caption": "Test Current Line", 11 | "command": "test_current_line" 12 | }, 13 | { 14 | "caption": "Test Current File", 15 | "command": "test_current_file" 16 | }, 17 | { 18 | "caption": "Run last spec", 19 | "command": "run_last_spec" 20 | }, 21 | { 22 | "caption": "Copy last command", 23 | "command": "copy_last" 24 | }, 25 | { 26 | "caption": "Display output panel", 27 | "command": "display_output_panel" 28 | }, 29 | { 30 | "caption": "Switch between code and test", 31 | "command": "switch_between_code_and_test" 32 | }, 33 | { 34 | "caption": "Create spec file", 35 | "command": "create_spec_file" 36 | }, 37 | ] 38 | } 39 | ] 40 | }, 41 | { 42 | "id": "preferences", 43 | "children": [ 44 | { 45 | "id": "package-settings", 46 | "children": [ 47 | { 48 | "caption": "TestRSpec", 49 | "id": "sublime-rspec", 50 | "children": [ 51 | { 52 | "caption": "Settings", 53 | "command": "edit_settings", 54 | "args": { 55 | "base_file": "${packages}/TestRSpec/TestRSpec.sublime-settings", 56 | "user_file": "${packages}/User/TestRSpec.sublime-settings", 57 | "default": "// User overrides go here\n{\n\t$0\n}\n" 58 | } 59 | }, 60 | { 61 | "caption": "Key Bindings", 62 | "command": "edit_settings", 63 | "args": { 64 | "base_file": "${packages}/TestRSpec/Default.sublime-keymap", 65 | "user_file": "${packages}/User/Default (${platform}).sublime-keymap", 66 | "default": "" 67 | } 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Sublime TestRSpec 2 | 3 | RSpec plugin for Sublime Text 3. 4 | 5 | Run, navigate and create specs from Sublime Text. 6 | 7 | ![Features](recordings/features.gif) 8 | 9 | ## Installation 10 | 11 | Using [Package Control](http://wbond.net/sublime_packages/package_control): 12 | 13 | 1. Run “Package Control: Install Package” command, find and install `TestRspec`. 14 | 2. Define key bindings (see Configuration section below). 15 | 3. Restart Sublime Text. 16 | 17 | Manually: 18 | 19 | 1. Clone this repository into your packages folder (in Sublime Text: Preferences -> Browse Packages). 20 | 2. Define key bindings (see Configuration section below). 21 | 3. Restart Sublime Text. 22 | 23 | ## Configuration 24 | 25 | TestRSpec tries its best to autodetect how to run RSpec. However, you might need to make adjustments to plugin's 26 | configuration if you have an uncommon setup. 27 | 28 | There are no key bindings enabled by default. Go to Preferences -> Package Settings -> TestRSpec -> Key Bindings to define key bindings. 29 | 30 | Find settings in Preferences -> Package Settings -> TestRSpec. 31 | 32 | [Default settings](https://github.com/astrauka/TestRSpec/blob/master/TestRSpec.sublime-settings) 33 | 34 | ## Features 35 | 36 | ### Run RSpec 37 | 38 | Launch RSpec for: 39 | 40 | * Current file 41 | * Current line 42 | * Rerun last run spec 43 | 44 | ### Switch between code and spec 45 | 46 | Jumps from code to spec and vice versa. If there multiple matches, it shows a list with matches. 47 | 48 | ### Create a spec file 49 | 50 | Creates a spec file when run in a source file. 51 | 52 | Uses code snippet defined in settings (`create_spec_snippet`). 53 | 54 | ### Copy last ran RSpec command 55 | 56 | Copies the command of the last run spec. 57 | It can be useful e.g. when you want to debug your application within a 'real' terminal. 58 | 59 | ## Tips 60 | 61 | ### Ignore binding.pry when running specs 62 | 63 | Sublime does not allow input in the output panel, so if you add `binding.pry`, tests get stuck 64 | waiting on input. 65 | 66 | To work around this, you can disable the debugger by modifying TestRSpec configuration: 67 | 68 | ```json 69 | { 70 | "env": { 71 | "DISABLE_PRY": "true" 72 | } 73 | } 74 | ``` 75 | 76 | Alternatively, use [pry-remote](https://github.com/Mon-Ouie/pry-remote). 77 | 78 | ## Troubleshooting 79 | 80 | ### Ruby not found or wrong ruby version used 81 | 82 | Example error: 83 | 84 | ``` 85 | /usr/bin/env: ruby: No such file or directory 86 | ``` 87 | 88 | Override `PATH` variable in your shell configuration (`~/.bashrc` or `~/.bash_profile`). 89 | Make sure `ruby` command runs the right Ruby version in `bash`. 90 | 91 | Alternatively, update package settings with path to ruby, e.g.: 92 | 93 | ```json 94 | { 95 | "rspec_add_to_path": "$HOME/.rbenv/shims" 96 | } 97 | ``` 98 | 99 | ### Spring is not used 100 | 101 | Make sure you have both `spring` and `spring-commands-rspec` in your Gemfile. 102 | 103 | If you use binstubs, you also need to run 104 | 105 | ```bash 106 | bundle exec spring binstub rspec 107 | ``` 108 | 109 | ## Acknowledgments 110 | 111 | Inspired by and uses code from https://github.com/maltize/sublime-text-2-ruby-tests 112 | 113 | ## Contribution 114 | 115 | Help is always welcome. Create an issue if you need help. 116 | 117 | ## Copyright and license 118 | 119 | Copyright © 2016 [@astrauka](https://twitter.com/astrauka) 120 | 121 | Licensed under the [**MIT**](https://mit-license.org/) license. 122 | -------------------------------------------------------------------------------- /TestRSpec.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "target": "exec", 3 | // spec output panel settings 4 | "panel_settings": { 5 | "encoding": "utf-8", 6 | "syntax": "Packages/TestRSpec/themes/RSpecConsole.sublime-syntax", 7 | "scroll_past_end": false, 8 | "word_wrap": true, 9 | "line_numbers": false, 10 | "gutter": false, 11 | "rulers": [], 12 | }, 13 | // rspec command 14 | "rspec_command": "rspec", 15 | // environment variables for rspec command 16 | "env": { 17 | }, 18 | "rspec_add_to_path": "", 19 | // rspec runner paths 20 | "paths_bin_rspec": "./bin/rspec", 21 | "paths_rbenv": "~/.rbenv/bin/rbenv", 22 | "paths_rvm": "~/.rvm/bin/rvm-auto-ruby", 23 | "paths_system_ruby": "/usr/bin/env ruby", 24 | // which rubies to check for when running rspec 25 | "check_for_rbenv": true, 26 | "check_for_rvm": true, 27 | "check_for_system_ruby": true, 28 | "check_for_bundler": true, 29 | "check_for_spring": true, 30 | // switch spec/code directories to skip when searching by file name 31 | "ignored_directories": [".git", "vendor", "tmp", "migrate"], 32 | // specs root folder 33 | "spec_folder": "spec", 34 | // app/models/user.rb spec should be found in spec/model/user_spec.rb, omitting 'app' folder 35 | "ignored_spec_path_building_directories": ["app"], 36 | // auto save current file on spec run 37 | "save_current_file_on_run": true, 38 | // auto save all files on spec run 39 | "save_all_files_on_run": false, 40 | // create spec snippet defined line by line 41 | "create_spec_snippet": [ 42 | "RSpec.describe ${class_name} do", 43 | "", 44 | "end", 45 | ], 46 | // create spec place cursor line number 47 | "create_spec_cursor_line": 2, 48 | // source files extension 49 | "source_file_extension": ".rb", 50 | // spec files extension 51 | "spec_file_extension": "_spec.rb", 52 | // regexp used to determine class name once creating new spec file 53 | "create_spec_class_name_regexp": "(class|module)[ ]+([a-zA-Z0-9:]*)", 54 | // if nested classes should be ignored when creating a new spec file 55 | "ignore_nested_classes": true, 56 | // if there is an exact match then directly jump to that one, show dropdown otherwise 57 | "switch_code_test_immediately_on_direct_match": true, 58 | // can be ERROR, WARNING or INFO 59 | "log_level": "INFO", 60 | // write logs to the output panel or the console 61 | "log_to_console": false, 62 | } 63 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "1.0.1": "messages/1.0.1.txt", 4 | "1.0.2": "messages/1.0.2.txt", 5 | "1.0.3": "messages/1.0.3.txt", 6 | "1.0.4": "messages/1.0.4.txt", 7 | "1.0.5": "messages/1.0.5.txt", 8 | "1.0.6": "messages/1.0.6.txt", 9 | "1.0.7": "messages/1.0.7.txt", 10 | "1.0.8": "messages/1.0.8.txt", 11 | "1.0.9": "messages/1.0.9.txt", 12 | "1.0.10": "messages/1.0.10.txt", 13 | "2.0.0": "messages/2.0.0.txt" 14 | } 15 | -------------------------------------------------------------------------------- /messages/1.0.1.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | Fixes 4 | ===== 5 | 6 | * fix plugin file loading 7 | -------------------------------------------------------------------------------- /messages/1.0.10.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | New features 4 | ============ 5 | 6 | * Press cmd+shift+, or super+shift+, to copy the last RSpec command into clipboard so you can 7 | run the command in your terminal. 8 | -------------------------------------------------------------------------------- /messages/1.0.2.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | Improvements 4 | ===== 5 | 6 | * allow adding to path for spec command run 7 | -------------------------------------------------------------------------------- /messages/1.0.3.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | Improvements 4 | ===== 5 | 6 | * make spec/file switch configurable on direct match found 7 | -------------------------------------------------------------------------------- /messages/1.0.4.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | Fixes 4 | ===== 5 | 6 | * fix default settings file path 7 | 8 | Improvements 9 | ===== 10 | 11 | * add commands to launch panel 12 | -------------------------------------------------------------------------------- /messages/1.0.5.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | Fixes 4 | ===== 5 | 6 | * fix create spec file directory 7 | 8 | Notes 9 | ===== 10 | 11 | Please restart Sublime in case you encounter any issues 12 | -------------------------------------------------------------------------------- /messages/1.0.6.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | Improvements 4 | ===== 5 | 6 | * allow defining rspec command to run. Zeus support. 7 | 8 | Notes 9 | ===== 10 | 11 | Please restart Sublime in case you encounter any issues 12 | -------------------------------------------------------------------------------- /messages/1.0.7.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | Improvements 4 | ===== 5 | 6 | * Add "target" option 7 | 8 | Notes 9 | ===== 10 | 11 | Please restart Sublime in case you encounter any issues 12 | -------------------------------------------------------------------------------- /messages/1.0.8.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | Improvements 4 | ===== 5 | 6 | * Improve spring detection, support spaces in paths 7 | 8 | Notes 9 | ===== 10 | 11 | Please restart Sublime in case you encounter any issues 12 | -------------------------------------------------------------------------------- /messages/1.0.9.txt: -------------------------------------------------------------------------------- 1 | TestRSpec: 2 | 3 | Improvements 4 | ===== 5 | 6 | * [Fix space in path issues on windows](https://github.com/astrauka/TestRSpec/pull/37) 7 | 8 | Notes 9 | ===== 10 | 11 | Please restart Sublime in case you encounter any issues 12 | -------------------------------------------------------------------------------- /messages/2.0.0.txt: -------------------------------------------------------------------------------- 1 | ⚠️ Please restart Sublime Text. 2 | 3 | Changes: 4 | 5 | * ⚠️ BREAKING CHANGE: TestRSpec 2.0 no longer ships with default key bindings. 6 | 7 | To keep the old key bindings, go to Preferences -> Package Settings -> 8 | TestRSpec -> Key Bindings, then copy the parts you need to your key binding 9 | configuration file. 10 | 11 | * Use the default color scheme for RSpec output panel. If you use a light 12 | theme, you'll notice the output is now light as well. 13 | 14 | You might need to reset `panel_settings` override in TestRSpec settings if 15 | you've changed those. 16 | 17 | * Improve failed spec detection in output panel. If you've previously set 18 | `show_errors_inline` to false in your settings because of this plugin, you 19 | might want to reenable it. 20 | 21 | * Refuse to run files that are not specs or when spec folder cannot be found. 22 | 23 | * Make the plugin compatible with future automatic upgrades. 24 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | TestRSpec allows you to run RSpec from within Sublime Text. 5 | 6 | TestRSpec tries its best to autodetect how to run RSpec. However, you might need 7 | to make adjustments to plugin's configuration if you have an uncommon setup. 8 | 9 | There are no key bindings enabled by default. Go to Preferences -> 10 | Package Settings -> TestRSpec -> Key Bindings to define key bindings. 11 | 12 | Documentation: https://github.com/astrauka/TestRSpec 13 | 14 | Having an Issue? 15 | ================ 16 | 17 | Refer to the troubleshooting section at https://github.com/astrauka/TestRSpec 18 | 19 | Please report issues and suggestions there. 20 | -------------------------------------------------------------------------------- /plugin_helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrauka/TestRSpec/233560038fdfc7d9e261e31829d49d2b54583c67/plugin_helpers/__init__.py -------------------------------------------------------------------------------- /plugin_helpers/open_file.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import sublime 3 | 4 | 5 | class OpenFile: 6 | def __init__(self, window, files): 7 | self.window = window 8 | self.files = files if isinstance(files, list) else [files] 9 | 10 | def run(self): 11 | if self._single_file(): 12 | self.window.open_file(self.files[0], sublime.ENCODED_POSITION) 13 | else: 14 | self.window.show_quick_panel(self.files, self._callback()) 15 | 16 | def _single_file(self): 17 | return len(self.files) == 1 18 | 19 | def _callback(self): 20 | return functools.partial(self._on_selected, self.files) 21 | 22 | def _on_selected(self, files, index): 23 | if index == -1: 24 | return 25 | 26 | self.window.open_file(files[index], sublime.ENCODED_POSITION) 27 | -------------------------------------------------------------------------------- /plugin_helpers/project_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class ProjectFiles: 5 | def __init__(self, project_root, file_matcher, ignored_directories): 6 | self.project_root = project_root 7 | self.file_matcher = file_matcher 8 | self.ignored_directories = ignored_directories 9 | 10 | def filter(self): 11 | if not self.project_root: 12 | return 13 | 14 | matches = [] 15 | for dirname, _, files in self._walk(self.project_root): 16 | for file in filter(self.file_matcher, files): 17 | matches.append(os.path.join(dirname, file)) 18 | 19 | return matches 20 | 21 | def _walk(self, directory): 22 | for dir, dirnames, files in os.walk(directory): 23 | dirnames[:] = [ 24 | dirname 25 | for dirname in dirnames 26 | if dirname not in self.ignored_directories 27 | ] 28 | yield dir, dirnames, files 29 | -------------------------------------------------------------------------------- /plugin_helpers/utils.py: -------------------------------------------------------------------------------- 1 | # from http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ 2 | 3 | import platform 4 | import shlex 5 | 6 | from functools import partial 7 | 8 | 9 | class memoize: 10 | """cache the return value of a method 11 | 12 | This class is meant to be used as a decorator of methods. The return value 13 | from a given method invocation will be cached on the instance whose method 14 | was invoked. All arguments passed to a method decorated with memoize must 15 | be hashable. 16 | 17 | If a memoized method is invoked directly on its class the result will not 18 | be cached. Instead the method will be invoked like a static method: 19 | class Obj: 20 | @memoize 21 | def add_to(self, arg): 22 | return self + arg 23 | Obj.add_to(1) # not enough arguments 24 | Obj.add_to(1, 2) # returns 3, result is not cached 25 | """ 26 | 27 | def __init__(self, func): 28 | self.func = func 29 | 30 | def __get__(self, obj, objtype=None): 31 | if obj is None: 32 | return self.func 33 | return partial(self, obj) 34 | 35 | def __call__(self, *args, **kw): 36 | obj = args[0] 37 | try: 38 | cache = obj.__cache 39 | except AttributeError: 40 | cache = obj.__cache = {} 41 | key = (self.func, args[1:], frozenset(kw.items())) 42 | try: 43 | res = cache[key] 44 | except KeyError: 45 | res = cache[key] = self.func(*args, **kw) 46 | return res 47 | 48 | 49 | # from http://stackoverflow.com/a/480227 50 | def unique(seq): 51 | seen = set() 52 | seen_add = seen.add 53 | return [x for x in seq if not (x in seen or seen_add(x))] 54 | 55 | 56 | # from http://stackoverflow.com/a/2556252 57 | def rreplace(s, old, new, occurrence): 58 | li = s.rsplit(old, occurrence) 59 | return new.join(li) 60 | 61 | 62 | def quote(s): 63 | if "windows" in platform.system().lower(): 64 | return _quote_windows_string(s) 65 | else: 66 | return shlex.quote(s) 67 | 68 | 69 | def _quote_windows_string(s): 70 | if not s: 71 | return '""' 72 | return '"' + s.replace('"', '\\"') + '"' 73 | -------------------------------------------------------------------------------- /recordings/features.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrauka/TestRSpec/233560038fdfc7d9e261e31829d49d2b54583c67/recordings/features.gif -------------------------------------------------------------------------------- /rspec/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrauka/TestRSpec/233560038fdfc7d9e261e31829d49d2b54583c67/rspec/__init__.py -------------------------------------------------------------------------------- /rspec/create_spec_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sublime 4 | 5 | from ..plugin_helpers.utils import memoize 6 | from ..plugin_helpers.open_file import OpenFile 7 | from .files.opposite import OppositeFile 8 | 9 | 10 | class CreateSpecFile: 11 | CLASS_DESCRIPTOR = "class" 12 | 13 | def __init__(self, context): 14 | self.context = context 15 | 16 | def run(self): 17 | if self.context.is_test_file(): 18 | return 19 | 20 | self._create_directories() 21 | self._write_template() 22 | self._open() 23 | 24 | def _create_directories(self): 25 | os.makedirs(os.path.dirname(self._file_name()), exist_ok=True) 26 | 27 | def _write_template(self): 28 | if os.path.isfile(self._file_name()): 29 | return 30 | 31 | handler = open(self._file_name(), "w+") 32 | handler.write(self._spec_template()) 33 | handler.close() 34 | 35 | def _open(self): 36 | OpenFile( 37 | self.context.window(), 38 | "{}:{}".format(self._file_name(), self._create_spec_cursor_line()), 39 | ).run() 40 | 41 | @memoize 42 | def _file_name(self): 43 | relative_name = os.path.join( 44 | self._spec_folder(), OppositeFile(self.context).relative_name() 45 | ) 46 | for ignored_directory in OppositeFile(self.context).ignored_directories(): 47 | relative_name = self._spec_name_with_ignore( 48 | ignored_directory, relative_name 49 | ) 50 | 51 | return os.path.join(self.context.package_root(), relative_name) 52 | 53 | def _spec_name_with_ignore(self, ignored_directory, relative_name): 54 | ignore = os.path.join(self._spec_folder(), ignored_directory) 55 | if ignore in relative_name: 56 | return os.path.join( 57 | self._spec_folder(), relative_name.replace(ignore, "", 1) 58 | ) 59 | else: 60 | return relative_name 61 | 62 | @memoize 63 | def _spec_folder(self): 64 | return self.context.from_settings("spec_folder") 65 | 66 | @memoize 67 | def _create_spec_cursor_line(self): 68 | return self.context.from_settings("create_spec_cursor_line") 69 | 70 | @memoize 71 | def _spec_template(self): 72 | return sublime.expand_variables( 73 | "\n".join(self.context.from_settings("create_spec_snippet", [""])), 74 | {"class_name": self._class_name()}, 75 | ) 76 | 77 | def _class_name(self): 78 | body = self.context.view().substr(sublime.Region(0, self.context.view().size())) 79 | matches = re.findall( 80 | self.context.from_settings("create_spec_class_name_regexp"), body 81 | ) 82 | names = [] 83 | for descriptor, name in matches: 84 | names.append(name) 85 | if self._ignore_nested_classes() and descriptor == self.CLASS_DESCRIPTOR: 86 | break 87 | 88 | return "::".join(names) 89 | 90 | @memoize 91 | def _ignore_nested_classes(self): 92 | return self.context.from_settings("ignore_nested_classes") 93 | -------------------------------------------------------------------------------- /rspec/execute_spec.py: -------------------------------------------------------------------------------- 1 | from ..plugin_helpers.utils import quote 2 | from ..plugin_helpers.utils import memoize 3 | from .output import Output 4 | from .spec_command import SpecCommand 5 | from .last_run import LastRun 6 | from .files.save import SaveFiles 7 | 8 | 9 | class ExecuteSpec: 10 | def __init__(self, context): 11 | self.context = context 12 | 13 | def current(self): 14 | if not self._validate_can_run_spec(): 15 | return 16 | 17 | self._prepare_output_panel() 18 | self._execute(self._command_hash()) 19 | 20 | def last_run(self): 21 | self._execute(LastRun.command_hash()) 22 | 23 | def _validate_can_run_spec(self): 24 | if not self.context.project_root(): 25 | self._notify_missing_project_root() 26 | return False 27 | 28 | if not self.context.is_test_file(): 29 | self._notify_not_test_file() 30 | return False 31 | 32 | return True 33 | 34 | def _prepare_output_panel(self): 35 | self.context.log("Error occurred, see more in 'View -> Show Console'") 36 | self.context.log("Project root {0}".format(self.context.project_root())) 37 | self.context.log("Spec target {0}".format(self.context.spec_target())) 38 | self.context.display_output_panel() 39 | 40 | def _execute(self, command_hash): 41 | self._before_execute() 42 | self.context.log("Executing {0}\n".format(command_hash.get("shell_cmd"))) 43 | self.context.window().run_command( 44 | self.context.from_settings("target"), command_hash 45 | ) 46 | LastRun.save(command_hash) 47 | 48 | def _before_execute(self): 49 | SaveFiles(self.context).run() 50 | 51 | def _notify_missing_project_root(self): 52 | self.context.log( 53 | "Could not find '{0}/' folder traversing back from {1}".format( 54 | self.context.from_settings("spec_folder"), self.context.file_name() 55 | ), 56 | level=Output.Levels.ERROR, 57 | ) 58 | self.context.display_output_panel() 59 | 60 | def _notify_not_test_file(self): 61 | self.context.log( 62 | "Not an RSpec file: {0}".format(self.context.file_name()), 63 | level=Output.Levels.ERROR, 64 | ) 65 | self.context.display_output_panel() 66 | 67 | @memoize 68 | def _command_hash(self): 69 | add_to_path = self.context.from_settings("rspec_add_to_path", "") 70 | append_path = ( 71 | "export PATH={0}:$PATH;".format(add_to_path) if add_to_path else "" 72 | ) 73 | 74 | command = ( 75 | "({append_path} cd {project_root} && {rspec_command} {target})".format( 76 | append_path=append_path, 77 | project_root=quote(self.context.project_root()), 78 | rspec_command=SpecCommand(self.context).result(), 79 | target=quote(self.context.spec_target()), 80 | ) 81 | ) 82 | panel_settings = self.context.from_settings("panel_settings", {}) 83 | env = self.context.from_settings("env", {}) 84 | 85 | return { 86 | "shell_cmd": command, 87 | "working_dir": self.context.project_root(), 88 | "env": env, 89 | "file_regex": r"^rspec ([^ ]*\.rb):(\d+)() # (.+)$", 90 | "syntax": panel_settings.get("syntax"), 91 | "encoding": panel_settings.get("encoding", "utf-8"), 92 | } 93 | -------------------------------------------------------------------------------- /rspec/files/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrauka/TestRSpec/233560038fdfc7d9e261e31829d49d2b54583c67/rspec/files/__init__.py -------------------------------------------------------------------------------- /rspec/files/opposite.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ...plugin_helpers.utils import rreplace 4 | 5 | 6 | class OppositeFile: 7 | def __init__(self, context): 8 | self.context = context 9 | 10 | def relative_name(self): 11 | return self.opposite(self.context.file_package_relative_name()) 12 | 13 | def base_name(self): 14 | return self.opposite(self.context.file_base_name()) 15 | 16 | def opposite(self, name): 17 | if self.context.is_test_file(): 18 | return rreplace(name, self._spec_extension(), self._source_extension(), 1) 19 | else: 20 | return rreplace(name, self._source_extension(), self._spec_extension(), 1) 21 | 22 | def _spec_extension(self): 23 | return self.context.from_settings("spec_file_extension") 24 | 25 | def _source_extension(self): 26 | return self.context.from_settings("source_file_extension") 27 | 28 | def ignored_directories(self): 29 | directories = ( 30 | self.context.from_settings("ignored_spec_path_building_directories") or [] 31 | ) 32 | return self._direct_match() + [directory + os.sep for directory in directories] 33 | 34 | def _direct_match(self): 35 | return [""] 36 | -------------------------------------------------------------------------------- /rspec/files/save.py: -------------------------------------------------------------------------------- 1 | class SaveFiles: 2 | def __init__(self, context): 3 | self.context = context 4 | 5 | def run(self): 6 | self._save_current_file_on_run() 7 | self._save_all_files_on_run() 8 | 9 | def _save_current_file_on_run(self): 10 | if self.context.from_settings("save_current_file_on_run"): 11 | self.context.window().run_command("save") 12 | 13 | def _save_all_files_on_run(self): 14 | if self.context.from_settings("save_all_files_on_run"): 15 | self.context.window().run_command("save_all") 16 | -------------------------------------------------------------------------------- /rspec/files/source.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ...plugin_helpers.utils import memoize 4 | from .opposite import OppositeFile 5 | 6 | 7 | class SourceFile: 8 | def __init__(self, context, append_directory): 9 | self.context = context 10 | self.append_directory = append_directory 11 | 12 | def result(self): 13 | source_name = self._source_name() 14 | if os.path.isfile(source_name): 15 | return source_name 16 | 17 | def _source_name(self): 18 | return os.path.join( 19 | self.context.package_root(), 20 | self.append_directory, 21 | self._name_without_spec_directory(), 22 | ) 23 | 24 | def _name_without_spec_directory(self): 25 | return self._relative_name().replace( 26 | self.context.from_settings("spec_folder") + os.sep, "", 1 27 | ) 28 | 29 | @memoize 30 | def _relative_name(self): 31 | return OppositeFile(self.context).relative_name() 32 | -------------------------------------------------------------------------------- /rspec/files/spec.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .opposite import OppositeFile 4 | from ...plugin_helpers.utils import memoize 5 | 6 | 7 | class SpecFile: 8 | def __init__(self, context, ignored_directory): 9 | self.context = context 10 | self.ignored_directory = ignored_directory 11 | 12 | def result(self): 13 | if not self._relative_name().startswith(self.ignored_directory): 14 | return 15 | 16 | spec_name = self.spec_name() 17 | if os.path.isfile(spec_name): 18 | return spec_name 19 | 20 | def spec_name(self): 21 | return os.path.join( 22 | self.context.package_root(), 23 | self.context.from_settings("spec_folder"), 24 | self._name_without_ignored_directory(), 25 | ) 26 | 27 | def _name_without_ignored_directory(self): 28 | return self._relative_name().replace(self.ignored_directory, "", 1) 29 | 30 | @memoize 31 | def _relative_name(self): 32 | return OppositeFile(self.context).relative_name() 33 | -------------------------------------------------------------------------------- /rspec/last_copy.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from .last_run import LastRun 4 | from .rspec_print import rspec_print 5 | 6 | 7 | class LastCopy: 8 | @classmethod 9 | def run(klass): 10 | command = LastRun.command_hash().get("shell_cmd") 11 | sublime.set_clipboard(command) 12 | rspec_print("Copied to clipboard: {0}".format(command)) 13 | sublime.active_window().status_message( 14 | "TestRspec: Copied last spec command to clipboard" 15 | ) 16 | -------------------------------------------------------------------------------- /rspec/last_run.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | 4 | class LastRun: 5 | SETTINGS_FILE = "TestRSpec.last-run" 6 | 7 | @classmethod 8 | def save(klass, command_hash): 9 | settings = sublime.load_settings(klass.SETTINGS_FILE) 10 | settings.set("command_hash", command_hash) 11 | sublime.save_settings(klass.SETTINGS_FILE) 12 | 13 | @classmethod 14 | def command_hash(klass): 15 | settings = sublime.load_settings(klass.SETTINGS_FILE) 16 | return settings.get("command_hash") 17 | -------------------------------------------------------------------------------- /rspec/output.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from ..plugin_helpers.utils import memoize 4 | 5 | 6 | class Output: 7 | class Levels: 8 | ERROR = "ERROR" 9 | WARNING = "WARNING" 10 | INFO = "INFO" 11 | 12 | PANEL_NAME = "exec" # must be same as sublime command exec output panel name 13 | 14 | DEFAULT_SYNTAX = "Packages/TestRSpec/themes/RSpecConsole.sublime-syntax" 15 | PLAIN_TEXT_SYNTAX = "Packages/Text/Plain text.tmLanguage" 16 | 17 | @classmethod 18 | def destroy(cls): 19 | for window in sublime.windows(): 20 | cls.destroy_window_panels(window) 21 | 22 | @classmethod 23 | def destroy_window_panels(cls, window): 24 | panel = window.find_output_panel(cls.PANEL_NAME) 25 | if not panel: 26 | return 27 | if panel.settings().get("syntax") != cls.DEFAULT_SYNTAX: 28 | return 29 | 30 | panel.settings().set("syntax", cls.PLAIN_TEXT_SYNTAX) 31 | window.destroy_output_panel(cls.PANEL_NAME) 32 | 33 | def __init__(self, window, edit, panel_settings={}): 34 | self.window = window 35 | self.edit = edit 36 | self.panel_settings = panel_settings 37 | 38 | def log(self, message): 39 | self.panel().insert(self.edit, self.panel().size(), "{0}\n".format(message)) 40 | 41 | @memoize 42 | def panel(self): 43 | panel = self.window.get_output_panel(Output.PANEL_NAME) 44 | for key, value in self.panel_settings.items(): 45 | panel.settings().set(key, value) 46 | 47 | return panel 48 | 49 | @memoize 50 | def panel_name(self): 51 | return "output.{0}".format(Output.PANEL_NAME) 52 | 53 | def show_panel(self): 54 | self.window.run_command("show_panel", {"panel": self.panel_name()}) 55 | -------------------------------------------------------------------------------- /rspec/package_root.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class PackageRoot: 5 | def __init__(self, file_name, spec_folder_name): 6 | self.file_name = file_name 7 | self.spec_folder_name = spec_folder_name 8 | 9 | def result(self): 10 | if not self.file_name: 11 | return 12 | if not self.spec_folder_name: 13 | return 14 | 15 | return self._via_inclusion() or self._via_upwards_search() 16 | 17 | def _via_inclusion(self): 18 | wrapped_folder_name = "/{0}/".format(self.spec_folder_name) 19 | if not wrapped_folder_name in self.file_name: 20 | return 21 | 22 | return self.file_name[: self.file_name.rindex(wrapped_folder_name)] 23 | 24 | def _via_upwards_search(self): 25 | path = self.file_name 26 | 27 | while True: 28 | (path, current_dir_name) = os.path.split(path) 29 | if not current_dir_name: 30 | return 31 | 32 | spec_folder_path = os.path.join(path, self.spec_folder_name) 33 | if os.path.isdir(spec_folder_path): 34 | return path 35 | -------------------------------------------------------------------------------- /rspec/project_root.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .package_root import PackageRoot 4 | 5 | 6 | class ProjectRoot: 7 | def __init__(self, file_name, spec_folder_name, sublime_command): 8 | self.file_name = file_name 9 | self.spec_folder_name = spec_folder_name 10 | self.sublime_command = sublime_command 11 | 12 | def result(self): 13 | if not self.file_name: 14 | return 15 | if not self.spec_folder_name: 16 | return 17 | 18 | return self._via_open_folders() or self._package_root() 19 | 20 | def _via_open_folders(self): 21 | view = self.sublime_command.view 22 | if not view: 23 | return 24 | 25 | window = view.window() 26 | if not window: 27 | return 28 | 29 | for folder in window.folders(): 30 | if not self.file_name.startswith(folder): 31 | continue 32 | 33 | spec_folder_path = os.path.join(folder, self.spec_folder_name) 34 | if os.path.isdir(spec_folder_path): 35 | return folder 36 | 37 | def _package_root(self): 38 | return PackageRoot(self.file_name, self.spec_folder_name).result() 39 | -------------------------------------------------------------------------------- /rspec/rspec_print.py: -------------------------------------------------------------------------------- 1 | def rspec_print(*args): 2 | print("TestRSpec:", *args) 3 | -------------------------------------------------------------------------------- /rspec/spec_command.py: -------------------------------------------------------------------------------- 1 | from .spec_commands.bin_rspec import BinRspec 2 | from .spec_commands.ruby_rspec import RubyRspec 3 | 4 | 5 | class SpecCommand: 6 | def __init__(self, context): 7 | self.context = context 8 | 9 | def result(self): 10 | if not self.context: 11 | return 12 | 13 | return BinRspec(self.context).result() or RubyRspec(self.context).result() 14 | -------------------------------------------------------------------------------- /rspec/spec_commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrauka/TestRSpec/233560038fdfc7d9e261e31829d49d2b54583c67/rspec/spec_commands/__init__.py -------------------------------------------------------------------------------- /rspec/spec_commands/bin_rspec.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ...plugin_helpers.utils import memoize 3 | 4 | 5 | class BinRspec: 6 | def __init__(self, context): 7 | self.context = context 8 | 9 | def result(self): 10 | if not self._file_exists(): 11 | return 12 | 13 | return self._bin_path() 14 | 15 | def _file_exists(self): 16 | if not self._bin_url(): 17 | return 18 | 19 | return os.path.exists(self._bin_url()) 20 | 21 | @memoize 22 | def _bin_path(self): 23 | return self.context.from_settings("paths_bin_rspec") 24 | 25 | @memoize 26 | def _bin_url(self): 27 | if not self._bin_path(): 28 | return 29 | if self._is_full_path(): 30 | return self._bin_path() 31 | 32 | return os.path.join(self.context.project_root(), self._bin_path()) 33 | 34 | def _is_full_path(self): 35 | return not self._bin_path().startswith("./") 36 | -------------------------------------------------------------------------------- /rspec/spec_commands/bundle.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Bundle: 5 | def __init__(self, context): 6 | self.context = context 7 | 8 | def result(self): 9 | if not self.context.from_settings("check_for_bundler"): 10 | return 11 | if self.gemfile_exists(): 12 | return "bundle exec" 13 | 14 | def gemfile_exists(self): 15 | return self.context.gemfile_path() 16 | -------------------------------------------------------------------------------- /rspec/spec_commands/rbenv.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ...plugin_helpers.utils import memoize 3 | 4 | 5 | class Rbenv: 6 | def __init__(self, context): 7 | self.context = context 8 | 9 | def result(self): 10 | if not self.context.from_settings("check_for_rbenv"): 11 | return 12 | if self._from_settings(): 13 | return "{0} exec".format(self._from_settings()) 14 | 15 | @memoize 16 | def _from_settings(self): 17 | file = os.path.expanduser(self.context.from_settings("paths_rbenv")) 18 | if file and os.path.isfile(file): 19 | return file 20 | -------------------------------------------------------------------------------- /rspec/spec_commands/ruby_rspec.py: -------------------------------------------------------------------------------- 1 | from .bundle import Bundle 2 | from .spring import Spring 3 | from .rbenv import Rbenv 4 | from .rvm import Rvm 5 | from .system_ruby import SystemRuby 6 | 7 | 8 | class RubyRspec: 9 | def __init__(self, context): 10 | self.context = context 11 | 12 | def result(self): 13 | if not self.context: 14 | return 15 | 16 | return " ".join( 17 | filter( 18 | None, 19 | [ 20 | self._ruby(), 21 | Bundle(self.context).result(), 22 | Spring(self.context).result(), 23 | self.context.from_settings("rspec_command"), 24 | ], 25 | ) 26 | ) 27 | 28 | def _ruby(self): 29 | return ( 30 | Rbenv(self.context).result() 31 | or Rvm(self.context).result() 32 | or SystemRuby(self.context).result() 33 | ) 34 | -------------------------------------------------------------------------------- /rspec/spec_commands/rvm.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ...plugin_helpers.utils import memoize 3 | 4 | 5 | class Rvm: 6 | def __init__(self, context): 7 | self.context = context 8 | 9 | def result(self): 10 | if not self.context.from_settings("check_for_rvm"): 11 | return 12 | if self._from_settings(): 13 | return "{0} -S".format(self._from_settings()) 14 | 15 | @memoize 16 | def _from_settings(self): 17 | file = os.path.expanduser(self.context.from_settings("paths_rvm")) 18 | if file and os.path.isfile(file): 19 | return file 20 | -------------------------------------------------------------------------------- /rspec/spec_commands/spring.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Spring: 5 | def __init__(self, context): 6 | self.context = context 7 | 8 | def result(self): 9 | if not self.context.from_settings("check_for_spring"): 10 | return 11 | if self.spring_included(): 12 | return "spring" 13 | 14 | def spring_included(self): 15 | gemfile_path = self.context.gemfile_path() 16 | if not gemfile_path: 17 | return 18 | 19 | with open(gemfile_path, "r") as f: 20 | return f.read().find("spring-commands-rspec") > 0 21 | -------------------------------------------------------------------------------- /rspec/spec_commands/system_ruby.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ...plugin_helpers.utils import memoize 3 | 4 | 5 | class SystemRuby: 6 | def __init__(self, context): 7 | self.context = context 8 | 9 | def result(self): 10 | if not self.context.from_settings("check_for_system_ruby"): 11 | return 12 | if self._from_settings(): 13 | return "{0} -S".format(self._from_settings()) 14 | 15 | @memoize 16 | def _from_settings(self): 17 | command = os.path.expanduser(self.context.from_settings("paths_system_ruby")) 18 | if not command: 19 | return 20 | 21 | file = command.split()[0] 22 | if os.path.isfile(file): 23 | return command 24 | -------------------------------------------------------------------------------- /rspec/switch_between_code_and_test.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from ..plugin_helpers.utils import memoize, unique 4 | from ..plugin_helpers.open_file import OpenFile 5 | from .files.opposite import OppositeFile 6 | from .files.spec import SpecFile 7 | from .files.source import SourceFile 8 | from .rspec_print import rspec_print 9 | from .create_spec_file import CreateSpecFile 10 | 11 | 12 | class SwitchBetweenCodeAndTest: 13 | def __init__(self, context): 14 | self.context = context 15 | 16 | def run(self): 17 | files = self._direct_match() or self._prioritize_by_path() 18 | 19 | if files: 20 | OpenFile(self.context.window(), files).run() 21 | elif self._fall_back_to_create_spec(): 22 | CreateSpecFile(self.context).run() 23 | else: 24 | rspec_print( 25 | "No files found, searched for {0}".format(self._file_base_name()) 26 | ) 27 | 28 | def _fall_back_to_create_spec(self): 29 | if self.context.is_test_file(): 30 | return False 31 | 32 | return sublime.ok_cancel_dialog( 33 | "Spec file not found.\n" "Do you want to create it?", ok_title="Create" 34 | ) 35 | 36 | def _direct_match(self): 37 | direct_match = self.context.from_settings( 38 | "switch_code_test_immediately_on_direct_match" 39 | ) 40 | return direct_match and self._files_by_path() 41 | 42 | @memoize 43 | def _files_by_path(self): 44 | if self._searching_for_spec_file(): 45 | return self._ignoring_spec_path_building_directories() 46 | else: 47 | return self._appending_spec_path_building_directories() 48 | 49 | def _prioritize_by_path(self): 50 | return unique(self._files_by_path() + self._files_by_name()) 51 | 52 | @memoize 53 | def _ignoring_spec_path_building_directories(self): 54 | # by spec/rel_path 55 | # by spec/rel_path-ignored_dir 56 | ignored_directories = OppositeFile(self.context).ignored_directories() 57 | files = [ 58 | SpecFile(self.context, directory).result() 59 | for directory in ignored_directories 60 | ] 61 | return list(filter(None, files)) 62 | 63 | @memoize 64 | def _appending_spec_path_building_directories(self): 65 | # by rel_path-spec 66 | # by rel_path-spec+ignored_dir 67 | appended_directories = OppositeFile(self.context).ignored_directories() 68 | files = [ 69 | SourceFile(self.context, directory).result() 70 | for directory in appended_directories 71 | ] 72 | return list(filter(None, files)) 73 | 74 | @memoize 75 | def _files_by_name(self): 76 | file_matcher = lambda file: file == self._file_base_name() 77 | return self.context.project_files(file_matcher) 78 | 79 | @memoize 80 | def _file_base_name(self): 81 | return OppositeFile(self.context).base_name() 82 | 83 | @memoize 84 | def _searching_for_spec_file(self): 85 | return not self.context.is_test_file() 86 | -------------------------------------------------------------------------------- /rspec/task_context.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sublime 3 | 4 | from ..plugin_helpers.utils import memoize 5 | from ..plugin_helpers.project_files import ProjectFiles 6 | from .package_root import PackageRoot 7 | from .project_root import ProjectRoot 8 | from .output import Output 9 | from .rspec_print import rspec_print 10 | 11 | 12 | class TaskContext: 13 | GEMFILE_NAME = "Gemfile" 14 | LOG_LEVELS = [Output.Levels.ERROR, Output.Levels.WARNING, Output.Levels.INFO] 15 | 16 | def __init__(self, sublime_command, edit, spec_target_is_file=False): 17 | self.sublime_command = sublime_command 18 | self.edit = edit 19 | self.spec_target_is_file = spec_target_is_file 20 | 21 | @memoize 22 | def view(self): 23 | return self.sublime_command.view 24 | 25 | @memoize 26 | def file_name(self): 27 | return self.view().file_name() 28 | 29 | @memoize 30 | def file_base_name(self): 31 | return os.path.basename(self.file_name()) 32 | 33 | @memoize 34 | def file_relative_name(self): 35 | return os.path.relpath(self.file_name(), self.project_root()) 36 | 37 | @memoize 38 | def file_package_relative_name(self): 39 | return os.path.relpath(self.file_name(), self.package_root()) 40 | 41 | @memoize 42 | def spec_file_extension(self): 43 | return self.from_settings("spec_file_extension") 44 | 45 | # from https://github.com/theskyliner/CopyFilepathWithLineNumbers/blob/master/CopyFilepathWithLineNumbers.py 46 | @memoize 47 | def line_number(self): 48 | (rowStart, colStart) = self.view().rowcol(self.view().sel()[0].begin()) 49 | (rowEnd, colEnd) = self.view().rowcol(self.view().sel()[0].end()) 50 | lines = (str)(rowStart + 1) 51 | 52 | if rowStart != rowEnd: 53 | # multiple selection 54 | lines += "-" + (str)(rowEnd + 1) 55 | 56 | return lines 57 | 58 | @memoize 59 | def spec_target(self): 60 | file_relative_name = self.file_relative_name() 61 | if self.spec_target_is_file: 62 | return file_relative_name 63 | else: 64 | return ":".join([file_relative_name, self.line_number()]) 65 | 66 | @memoize 67 | def project_root(self): 68 | return ProjectRoot( 69 | self.file_name(), 70 | self.from_settings("spec_folder"), 71 | self.sublime_command, 72 | ).result() 73 | 74 | @memoize 75 | def package_root(self): 76 | return PackageRoot( 77 | self.file_name(), 78 | self.from_settings("spec_folder"), 79 | ).result() 80 | 81 | def window(self): 82 | return self.view().window() 83 | 84 | @memoize 85 | def output_buffer(self): 86 | return Output( 87 | self.view().window(), self.edit, self.from_settings("panel_settings") 88 | ) 89 | 90 | def output_panel(self): 91 | return self.output_buffer().panel() 92 | 93 | def log(self, message, level=Output.Levels.INFO): 94 | if not self._can_log(level): 95 | return 96 | 97 | formatted_message = "{0}: {1}".format(level, message) 98 | if self.from_settings("log_to_console"): 99 | rspec_print(formatted_message) 100 | else: 101 | self.output_buffer().log(formatted_message) 102 | 103 | def display_output_panel(self): 104 | self.output_buffer().show_panel() 105 | 106 | @memoize 107 | def _settings(self): 108 | return sublime.load_settings("TestRSpec.sublime-settings") 109 | 110 | def from_settings(self, key, default_value=None): 111 | return self._settings().get(key, default_value) 112 | 113 | def is_test_file(self): 114 | return self.file_name().endswith(self.spec_file_extension()) 115 | 116 | @memoize 117 | def gemfile_path(self): 118 | path = os.path.join(self.project_root(), TaskContext.GEMFILE_NAME) 119 | if not os.path.isfile(path): 120 | return 121 | 122 | return path 123 | 124 | def project_files(self, file_matcher): 125 | return ProjectFiles( 126 | self.project_root(), file_matcher, self.from_settings("ignored_directories") 127 | ).filter() 128 | 129 | def _get_log_level(self, log_level, default=0): 130 | try: 131 | return self.LOG_LEVELS.index(str(log_level).upper()) 132 | except ValueError: 133 | rspec_print("Invalid log level: {0}".format(log_level)) 134 | return default 135 | 136 | def _can_log(self, log_level): 137 | return self._get_log_level(log_level, 0) <= self._get_log_level(self.from_settings("log_level", 2)) 138 | -------------------------------------------------------------------------------- /test_rspec.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | # Clear module cache to force reloading all modules of this package. 5 | # See https://github.com/emmetio/sublime-text-plugin/issues/35 6 | prefix = __package__ + "." # don't clear the base package 7 | for module_name in [ 8 | module_name 9 | for module_name in sys.modules 10 | if module_name.startswith(prefix) and module_name != __name__ 11 | ]: 12 | del sys.modules[module_name] 13 | 14 | 15 | import sublime_plugin 16 | 17 | from .rspec.rspec_print import rspec_print 18 | from .rspec.execute_spec import ExecuteSpec 19 | from .rspec.task_context import TaskContext 20 | from .rspec.switch_between_code_and_test import SwitchBetweenCodeAndTest 21 | from .rspec.last_copy import LastCopy 22 | from .rspec.create_spec_file import CreateSpecFile 23 | from .rspec.output import Output 24 | 25 | 26 | class TestCurrentLineCommand(sublime_plugin.TextCommand): 27 | def run(self, edit): 28 | rspec_print("Running rspec") 29 | context = TaskContext(self, edit) 30 | ExecuteSpec(context).current() 31 | 32 | 33 | class TestCurrentFileCommand(sublime_plugin.TextCommand): 34 | def run(self, edit): 35 | rspec_print("Running rspec") 36 | context = TaskContext(self, edit, spec_target_is_file=True) 37 | ExecuteSpec(context).current() 38 | 39 | 40 | class RunLastSpecCommand(sublime_plugin.TextCommand): 41 | def run(self, edit): 42 | rspec_print("Running last rspec command") 43 | context = TaskContext(self, edit) 44 | ExecuteSpec(context).last_run() 45 | 46 | 47 | class CopyLastCommand(sublime_plugin.TextCommand): 48 | def run(self, edit): 49 | rspec_print("Running copy last rspec command") 50 | LastCopy.run() 51 | 52 | 53 | class DisplayOutputPanelCommand(sublime_plugin.TextCommand): 54 | def run(self, edit): 55 | rspec_print("Displaying output panel") 56 | context = TaskContext(self, edit) 57 | context.display_output_panel() 58 | 59 | 60 | class SwitchBetweenCodeAndTestCommand(sublime_plugin.TextCommand): 61 | def run(self, edit): 62 | rspec_print("Switching between code and test") 63 | context = TaskContext(self, edit) 64 | SwitchBetweenCodeAndTest(context).run() 65 | 66 | 67 | class CreateSpecFileCommand(sublime_plugin.TextCommand): 68 | def run(self, edit): 69 | rspec_print("Creating spec file") 70 | context = TaskContext(self, edit) 71 | CreateSpecFile(context).run() 72 | 73 | 74 | def plugin_unloaded(): 75 | # Destroy output panels because the default syntax is going to disappear. 76 | # This prevents error messages on plugin upgrade. 77 | Output.destroy() 78 | -------------------------------------------------------------------------------- /themes/RSpecConsole.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | # http://www.sublimetext.com/docs/syntax.html 4 | 5 | name: RSpecConsole 6 | hidden: true 7 | scope: tests.ruby 8 | 9 | contexts: 10 | main: 11 | - match: '^(?=[\.\*F]+$)' 12 | push: results-line 13 | - match: \b\d+ examples?|\b0 failures 14 | scope: markup.inserted.testrspec 15 | - match: \b\d+ failures? 16 | scope: markup.deleted.testrspec 17 | - match: \b\d+ pending 18 | scope: markup.changed.testrspec 19 | - match: '^(Failures|ERROR):' 20 | scope: markup.deleted.testrspec 21 | - match: '^(Pending|WARNING):' 22 | scope: markup.changed.testrspec 23 | - match: '^ Failure/Error: .+$' 24 | scope: markup.deleted.testrspec 25 | - match: \s#\s.+ 26 | scope: comment.line.placeholder.testrspec 27 | 28 | results-line: 29 | - match: $ 30 | pop: true 31 | - match: \.+ 32 | scope: markup.inserted.testrspec 33 | - match: \*+ 34 | scope: markup.changed.testrspec 35 | - match: F+ 36 | scope: markup.deleted.testrspec 37 | -------------------------------------------------------------------------------- /themes/syntax_test_rspecconsole.txt: -------------------------------------------------------------------------------- 1 | # SYNTAX TEST "Packages/TestRSpec/themes/RSpecConsole.sublime-syntax" 2 | 3 | . 4 | # <- markup.inserted.testrspec 5 | 6 | ..... 7 | # <- markup.inserted.testrspec 8 | #^^^^ markup.inserted.testrspec 9 | 10 | Hello. World. 11 | # ^ - markup.inserted.testrspec 12 | # ^ - markup.inserted.testrspec 13 | 14 | Fail 15 | # <- - markup.deleted.testrspec 16 | 17 | F 18 | # <- markup.deleted.testrspec 19 | 20 | FFFFF 21 | # <- markup.deleted.testrspec 22 | #^^^^ markup.deleted.testrspec 23 | 24 | * 25 | # <- markup.changed.testrspec 26 | 27 | ***** 28 | # <- markup.changed.testrspec 29 | #^^^^ markup.changed.testrspec 30 | 31 | .F.*. 32 | # <- markup.inserted.testrspec 33 | #^ markup.deleted.testrspec 34 | # ^ markup.inserted.testrspec 35 | # ^ markup.changed.testrspec 36 | # ^ markup.inserted.testrspec 37 | 38 | fF 39 | # <- - markup.deleted.testrspec 40 | #^ - markup.deleted.testrspec 41 | 42 | 6 examples, 1 failure, 4 pending 43 | # <- markup.inserted.testrspec 44 | #^^^^^^^^^ markup.inserted.testrspec 45 | # ^^^^^^^^^ markup.deleted.testrspec 46 | # ^^^^^^^^^ markup.changed.testrspec 47 | 48 | 0 examples, 0 failures, 0 pending 49 | # <- markup.inserted.testrspec 50 | #^^^^^^^^^ markup.inserted.testrspec 51 | # ^^^^^^^^^^ markup.inserted.testrspec 52 | # ^^^^^^^^^ markup.changed.testrspec 53 | 54 | 5 failures 55 | # <- markup.deleted.testrspec 56 | #^^^^^^^^^ markup.deleted.testrspec 57 | 58 | # comment 59 | #^^^^^^^^^ comment.line.placeholder.testrspec 60 | 61 | # comment 62 | # <- - comment.line.placeholder.testrspec 63 | #^^^^^^^^ - comment.line.placeholder.testrspec 64 | 65 | 1) Class#method failed 66 | # ^^^^^^^ - comment.line.placeholder.testrspec 67 | 68 | Failure/Error: raise 'error' 69 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ markup.deleted.testrspec 70 | 71 | Failures: 72 | # <- markup.deleted.testrspec 73 | #^^^^^^^^ markup.deleted.testrspec 74 | 75 | Pending: (Failures listed here are expected and do not affect your suite's status) 76 | # <- markup.changed.testrspec 77 | #^^^^^^^ markup.changed.testrspec 78 | # ^^^^^^ - markup.changed.testrspec 79 | --------------------------------------------------------------------------------