mgedmin/py-test-runner.vim

Run (Python) unit tests under cursor

★ 1Forks 0PythonGitHub ↗Compare

README

Vim script to run the tests for the current file/class/function.

Supports various Python test runners including

  • pytest
  • unittest autodiscovery
  • nose
  • django test
  • zope.testrunner

Demo:

asciicast

I recommend vim-plug:

Plug 'mgedmin/taghelper.vim'
Plug 'mgedmin/py-test-runner.vim'

You'll also need taghelper.vim (or the older pythonhelper.vim), and I recommend asyncrun.vim too.

Needs Vim built with Python 3 support. (Python 2 might work too, but I make no promises about it staying working.)

The plugin defines the following commands:

:RunTestUnderCursor

Runs a single test function/method/class/module, depending on where your cursor is. Can run the test asynchronously if you have asyncrun.vim installed and have defined the :Make command as documented in the wiki.

:RunTest name-of-test

Runs a single test function/method/class/module.

:RunLastTestAgain

Repeats the last test run.

:CopyTestUnderCursor

Copies the command to run a single test function/method/class/module into the system clipboard, so you can paste it into a terminal window and not block your Vim instance.

If you want some, copy and paste this into your .vimrc:

map <C-F9> :RunTestUnderCursor<CR>
map <C-S-F9> :RunLastTestAgain<CR>

If you're running the tests asynchronously it can be helpful to indicate the test status (running/pass/fail) by changing the status line color.

You can define the higlight groups like

hi StatusLineNeutral
            \ term=bold,reverse cterm=bold,reverse gui=bold,reverse
hi StatusLineRunning ctermfg=53 guifg=#5f005f
            \ term=bold,reverse cterm=bold,reverse gui=bold,reverse
hi StatusLineSuccess ctermfg=22 guifg=#005f00
            \ term=bold,reverse cterm=bold,reverse gui=bold,reverse
hi StatusLineFailure ctermfg=52 guifg=#5f0000
            \ term=bold,reverse cterm=bold,reverse gui=bold,reverse
hi! link StatusLine StatusLineNeutral

and then :RunTestUnderCursor will detect this and link StatusLine to StatusLineRunning when the test process starts. It's up to you to define an asyncrun exit callback to link it to success/failure as appropriate:

fun! OnAsyncRunExit()
  if g:asyncrun_status == 'success'
    hi! link StatusLine StatusLineSuccess
  elseif g:asyncrun_status == 'failure'
    hi! link StatusLine StatusLineFailure
  endif
  redrawstatus!
endf
let g:asyncrun_exit = "call OnAsyncRunExit()"

I'm considering removing the half-finished integration and suggesting you do the initial linking via

augroup AsyncRun
  au!
  au User AsyncRunStart hi! link StatusLine StatusLineRunning | redrawstatus!
augroup END

This plugin reads ~/.vim/py-test-runner.cfg (default filename, can be overridden by setting g:pyTestRunnerConfigFile) if it exists. It should be an INI file like this:

[default]
runner = pytest

[runner:mytestrunner]
command = mytestrunner
filter_for_file = {filename}
filter_for_function = {filename}::{function}
filter_for_class = {filename}::{class}
filter_for_method = {filename}::{class}::{method}
filter_for_doctest = -k {function}

[path:~/src/myproject]
runner = mytestrunner
command = venv/bin/mytestrunner

The [default] section has the following settings:

runner

Specifies the default test runner. If omitted, the default is pytest. You can use any of the predefined test runners (pytest, unittest, nose, zope, and django), or any custom test runner if you have a corresponding [runner:foo] section.

This setting can be overridden by [path:...] sections and manually, if you :call pytestrunner#use(runner) or set g:pyTestRunner.

ignore_functions_and_methods

Specifies a whitespace-separated list of function/method names that should not be considered to be tests.

For example, this is the default ignore list:

[default]
ignore_functions_and_methods =
    __init__
    setUp
    tearDown
    test_suite

When the cursor is inside a function/method with one of these names, it will be ignored (and the scope of the test will be the entire module/class).

This setting can be overridden by [runner:...] sections, and by [path:...] sections.

The [runner:NAME] sections define/override test runners and have the following settings:

command

Specifies the main test runner command. This can contain arguments. No shell escaping is done, so be careful!

Examples:

[runner:pytest]
command = pytest -ra

[runner:tox]
command = tox -e py27,py37 --

This setting can be overridden by [path:...] sections and manually, by calling pytestrunner#use(runner, command) or by setting g:pyTestRunnerCommand.

The full command is constructed from command and the multiple filter_for_... settings in the following order:

  1. cd workdir, if workdir is not blank
  2. command
  3. filter_for_file, if not blank
  4. filter_for_directory, if not blank
  5. filter_for_package, if not blank
  6. filter_for_module, if not blank
  7. one of filter_for_function, filter_for_doctest, filer_for_doctest_file, filter_for_class, filter_for_method, whichever is applicable

As a special case, if filter_for_function (or filter_for_doctest, or filter_for_doctest_file, or filter_for_class, or filter_for_method, whichever was picked) mentions the {filename} placeholder, filter_for_file, filter_for_directory, filter_for_package and filter_for_module will be skipped.

filter_for_file

Specifies how to tell the test runner which test file is interesting.

Example:

[runner:pytest]
filter_for_file = {filename}

Whether the {filename} placeholder is replaced with a relative or absolute filename depends on the absolute_filenames and relative_filenames settings.

You will want to specify either filter_for_file or filter_for_module, but not both. (I don't know what will happen if you specify both.)

This setting can be overridden by [path:...] sections.

filter_for_directory

Specifies how to tell the test runner which test directory is interesting.

Example:

[runner:...]
filter_for_directory = {directory}

Whether the {directory} placeholder is replaced with a relative or absolute filename depends on the absolute_filenames and relative_filenames settings.

You will want to specify either filter_for_directory or filter_for_filename, but not both. (I don't know what will happen if you specify both.)

You will want to specify either filter_for_directory or filter_for_package, but not both. (I don't know what will happen if you specify both.)

This setting can be overridden by [path:...] sections.

filter_for_package

Specifies how to tell the test runner which test package is interesting.

Example:

[runner:zope]
filter_for_package = -s {package}

The logic that computes Python package names from directory names relies on the presence/absence of __init__.py files and breaks if you use PEP-420 namespace packages.

You will want to specify either filter_for_package or filter_for_filename, but not both. (I don't know what will happen if you specify both.)

You will want to specify either filter_for_package or filter_for_directory, but not both. (I don't know what will happen if you specify both.)

This setting can be overridden by [path:...] sections.

filter_for_module

Specifies how to tell the test runner which test module is interesting.

Example:

[runner:zope]
filter_for_module = -m {module}

The module name is just the filename without the .py extension.

You will want to specify either filter_for_module or filter_for_filename, but not both. (I don't know what will happen if you specify both.)

This setting can be overridden by [path:...] sections.

filter_for_function

Specifies how to tell the test runner which test function is interesting.

Filtering by test function requires taghelper.vim to be installed.

Examples:

[runner:zope]
filter_for_function = -t {function}

[runner:pytest]
filter_for_function = {filename}::{function}

This setting can be overridden by [path:...] sections.

filter_for_doctest

Specifies how to tell the test runner which doctest function is interesting.

Filtering by test function requires taghelper.vim to be installed.

Regular functions from doctest functions are distinguished by name (functions starting with test are assumed to be regular functions).

Examples:

[runner:zope]
filter_for_doctest = -t {function}

[runner:pytest]
filter_for_doctest = -k {function}

[runner:pytest]
filter_for_doctest = {filename}::{full_module}.{function}

If this setting is not specified, filter_for_function is used instead for doctest functions as well.

This setting can be overridden by [path:...] sections.

filter_for_doctest_file

Specifies how to tell the test runner which doctest file is interesting.

Doctest files are recognized by their extension (.txt, .rst, .test).

Examples:

[runner:pytest]
filter_for_doctest_file = -k {function}

The filename (without directory) is used as the "function name" in the filter, hence the {function} in the above example.

If this setting is not specified, filter_for_doctest or filter_for_function is used instead.

This setting can be overridden by [path:...] sections.

filter_for_class

Specifies how to tell the test runner which test class is interesting.

Filtering by test class requires pythonhelper.vim to be installed.

Examples:

[runner:zope]
filter_for_class = -t {class}

[runner:pytest]
filter_for_class = {filename}::{class}

If this setting is not specified, filter_for_function is used instead.

This setting can be overridden by [path:...] sections.

filter_for_method

Specifies how to tell the test runner which test method is interesting.

Filtering by test method requires taghelper.vim to be installed.

Examples:

[runner:zope]
filter_for_method = -t '{method} \(.*\.{class}\)'

[runner:pytest]
filter_for_class = {filename}::{class}::{method}

[runner:nose]
filter_for_class = {filename}::{class}.{method}

If this setting is not specified, filter_for_function is used instead and gets the method name (discarding the class name).

This setting can be overridden by [path:...] sections.

absolute_filenames

Set to a true value (true, yes, 1) if you want {filename} and {directory} placeholders to be absolute.

This is helpful when the test runner script changes its working directory before it starts looking for files.

Set to a false value (false, no, 0) if you want {filename} and {directory} placeholders to be exactly as they appear in Vim (so they could be absolute or relative, depending on how you opened the file), as long as unless relative_filenames is also set to a false value.

Defauls to false. Can be overridden by [path:...] sections.

If both absolute_filenames and relative_filenames are enabled, absolute_filenames take precedence.

relative_filenames

Set to a true value (true, yes, 1) if you want {filename} and {directory} placeholders to be made relative.

The filenames are made relative to the directory named in the relative_to setting, if set, otherwise the workdir setting, if set, otherwise to the current working directory.

This is helpful when the test runner script changes its working directory before it starts looking for files.

Set to a false value (false, no, 0) if you want {filename} and {directory} placeholders to be exactly as they appear in Vim (so they could be absolute or relative, depending on how you opened the file), as long as unless relative_filenames is also set to a false value.

Defauls to false. Can be overridden by [path:...] sections.

If both absolute_filenames and relative_filenames are enabled, absolute_filenames take precedence.

relative_to

Set to a directory name when you want to use filenames relative to somewhere that is not the current working directory.

Example:

[path:~/src/project/alembic]
runner = pytest
command = tox -c alembic/ --
absolute_filenames = false
relative_filenames = true
relative_to = alembic/

Defaults to empty string. Can be overridden by [path:...] sections.

workdir

Set to a directory name when you want to change the working directory before running the test command.

Example:

[path:~/src/project/alembic]
runner = pytest
command = tox --
workdir = alembic/

This will run cd alembic/ && tox -- ...

Defaults to empty string. Can be overridden by [path:...] sections.

clipboard_extras

Extra command-line flags to be added when using :CopyTestUnderCursor.

Use this to add colors or progress bars that would otherwise confuse Vim's :make.

These flags are added to the beginning of the command line.

Example:

[runner:zope]
clipboard_extras = -pvc

This setting can be overridden by [path:...] sections.

clipboard_extras_suffix

Extra command-line flags to be added when using :CopyTestUnderCursor.

Use this to add colors or progress bars that would otherwise confuse Vim's :make.

These flags are added to the end of the command line.

No shell escaping is done so you can in fact do something like

[runner:zope]
clipboard_extras = 2>&1 | less -R

to pipe the test runner's output to a pager.

This setting can be overridden by [path:...] sections.

The [path:PATH] sections define overrides for your projects identified by path names and have the following settings:

runner

Overrides the runner from the [defaults] section.

Example:

[path:~/src/vim-plugins/py-test-checker.vim]
command = coverage run -m pytest

Can be overridden by setting g:pyTestRunner or calling pytestrunner#use(runner).

command

Overrides the command from the [runner:...] section.

Example:

[path:~/src/vim-plugins/py-test-checker.vim]
command = coverage run -m pytest

Can be overridden by setting g:pyTestRunnerCommand or calling pytestrunner#use(runner, command).

filter_for_file, filter_for_directory, filter_for_package, filter_for_module, filter_for_function, filter_for_doctest, filter_for_doctest_file, filter_for_class, filter_for_method ignore_functions_and_methods

Override the corresponding setting from the [runner:...] section.

You're not expected to ever need this.

absolute_filenames, relative_filenames, relative_to, workdir, clipboard_extras, clipboard_extras_suffix

Override the corresponding setting from the [runner:...] section.

These look like settings it can make sense to override on a per-project basis.

The following global variables are used:

g:pyVimRunCommand (default: "")

Vim command to run an external process (after setting &makeprg). If blank, the plugin will use :Make if such a user-defined command exists, otherwise it will use :make.

asyncrun.vim recommends defining

command! -bang -nargs=* -complete=file Make AsyncRun -program=make @ <args>

so you can run commands in the background.

g:pyTestRunnerConfigFile (default: "")

Config file to read. If blank, the plugin reads ~/.vim/py-test-runner.cfg.

It makes sense to set it if you use NeoVim and want your py-test-runner.cfg in ~/.config/nvim/ instead of the default location.

g:pyTestRunner (default: "")

Test runner to use. If not blank, overrides the runner setting in the configuration file.

The :call pytestrunner#use(...) convenience command writes to this variable.

g:pyTestRunnerCommand (default: "")

Test runner command to use. If not blank, overrides the command setting in the configuration file.

The :call pytestrunner#use(...) convenience command writes to this variable.

g:pyTestLastTest (default: "")

This is not a configuration setting, but instead the filter describing the last test executed via :RunTestUnderCursor. It is used by :RunLastTestAgain.

There are several functions that streamline the setup for the most common test runners, left for backwards compatibility:

:call UsePyTestTestRunner("pytest -ra")

Use pytest, which uses commands like

pytest -ra <filename>::<class>::<method>

You can optionally specify the main executable, which is helpful if you use multiple virtualenvs, e.g.

:call UsePyTestTestRunner("tox -e py27,py37 --")

assuming your tox.ini has

[testenv]
commands = pytest {posargs}

:call UsePyTestTestRunner(...) is exactly equivalent to :call pytestrunner#use("pytest", ...) and is provided for backwards compatibility.

:call UseZopeTestRunner("bin/test")

Use the Zope test runner, which uses commands like

bin/test -s <package> -m <module> -t '<method> (class <Class>)'

You can optionally specify the main executable or pass additional arguments, e.g.

:call UseZopeTestRunner("venv/bin/zope-testrunner -vv")

:call UseZopeTestRunner(...) is exactly equivalent to :call pytestrunner#use("zope", ...) and is provided for backwards compatibility.

:call UseNoseTestRunner("nosetests")

Use the nose test runner, which uses commands like

nosetests <filename>:<class>.<method>

:call UseNoseTestRunner(...) is exactly equivalent to :call pytestrunner#use("nose", ...) and is provided for backwards compatibility.

:call UseDjangoTestRunner("bin/django test")

Use the Django test runner. Assumes you're using django-nose, which uses commands like

bin/django test <filename>:<class>.<method>

You can optionally specify the main executable or pass additional arguments, e.g.

:call UseDjangoTestRunner("python manage.py test")
:call UseDjangoTestRunner("venv/bin/django-admin test")

:call UseDjangoTestRunner(...) is exactly equivalent to :call pytestrunner#use("django", ...) and is provided for backwards compatibility.

The following global variables are no longer used:

g:pyTestRunner

This used to define the test runner command, instead of selecting the test runner configuration section. If you keep defining it, you will get errors.

Use g:pyTestRunnerCommand instead.

g:pyTestRunnerTestFiltering

This is now completely ignored.

Define a [runner:...] or [path:...] section with

filter_for_function = -t {function}

instead.

g:pyTestRunnerDoctestFiltering

This is now completely ignored.

Define a [runner:...] or [path:...] section with

filter_for_doctest = -t {function}

instead.

g:pyTestRunnerTestFilteringClassAndMethodFormat

This is now completely ignored.

Define a [runner:...] or [path:...] section with

filter_for_method = -t {class}.{method}

instead.

g:pyTestRunnerTestFilteringBlacklist

This is now completely ignored.

Define a [default] or [runner:...] or [path:...] section with

ignore_functions_and_methods =
    __init__
    setUp
    tearDown
    test_suite

instead.

g:pyTestRunnerDirectoryFiltering

This is now completely ignored.

Define a [runner:...] or [path:...] section with

filter_for_directory = -s {directory}

instead.

g:pyTestRunnerFilenameFiltering

This is now completely ignored.

Define a [runner:...] or [path:...] section with

filter_for_file = {filename}

instead.

g:pyTestRunnerUseAbsoluteFilenames

This is now completely ignored.

Define a [runner:...] or [path:...] section with

absolute_filenames = yes

instead.

g:pyTestRunnerPackageFiltering

This is now completely ignored.

Define a [runner:...] or [path:...] section with

filter_for_package = -s {package}

instead.

g:pyTestRunnerModuleFiltering

This is now completely ignored.

Define a [runner:...] or [path:...] section with

filter_for_module = -m {module}

instead.

g:pyTestRunnerClipboardExtras

This is now completely ignored.

Define a [runner:...] or [path:...] section with

clipboard_extras = -pvc

instead.

g:pyTestRunnerClipboardExtrasSuffix

This is now completely ignored.

Define a [runner:...] or [path:...] section with

clipboard_extras_suffix = 2>&1 | less -R

instead.

  • various corner cases for lesser used test runners might not work
  • [path:...] runner=... probably overrides g:pyTestRunner, contradicting the documentation
  • there's no error if you specify an empty command in a config file (or set g:pyTestRunnerCommand to a bunch of spaces)
  • there's no error if a [path:...] section specifies a bad runner

py-test-runner.vim was written by Marius Gedminas <[email protected]>. Licence: MIT.

Contributors

mgedmin

Issues