Watched FileType testing in Vim

Posted on
vim

I love vim because it lets me automate my workflow.

Problem

Case in point, at work we use GNU make pretty heavily. Most of our Makefiles have a test target, which usually runs a combination of linting and unit tests in a virtualenv.

My setup is tmux + neovim, and I got tired of the following tedious workflow:

  1. make a change in source or test code
  2. switch to a pane with a terminal
  3. run make test
  4. examine test output
  5. switch back to original pane
  6. GOTO 1

So to avoid pane switching in tmux, I decided to shell out in vim with :make test. This helps, but it’s blocking, so that if the tests take time to run, I’m noticeably underutilized.

Solution

vim-dispatch is a plugin written by prolific tpope that lets you shell out in vim in a non-blocking way.

Using it, I came up with the following (while being reminded of how painful and confusing writing vimscript is).

augroup python
  autocmd!
  " F9 to run current module
  autocmd FileType python nnoremap <buffer> <F9> :w<cr> :!python %<cr>
  autocmd BufWritePost *.py call <SID>TryTests(1)
  autocmd FileType python nnoremap <buffer> <F8> :<C-U>call <SID>TryTests(0)<CR>
  autocmd FileType python nnoremap <buffer> <F7> :<C-U>Cop<CR>
augroup END

function! s:TryTests(bg)
  if !exists('b:has_test_target')
    let b:has_test_target = filereadable('Makefile')
          \ && match(readfile('Makefile'), 'test:') != -1
  endif
  if b:has_test_target
    let request = dispatch#request()
    if get(request, 'background', 0)
      " background tasks need to be explicitly checked against the system
      call system('ps -p ' . get(request, 'pid'))
    endif
    if empty(request) || dispatch#completed(request) || v:shell_error
      let test_command = 'Make'
      if a:bg | let test_command .= '!' | endif
      let test_command .= ' test'
      silent execute test_command
    else
      echom 'Current tests are still running.'
    endif
  endif
endfunction

Ignoring the first autocmd - which already existed and is unrelated to this endeavor - this means that whenever I save a Python file, and the current buffer is part of a project that has a make test target, I execute those tests in a non-blocking way in a separate tmux window.

Note that

  • the check for whether or not make test is executable is buffer-local and cached; this makes sense because you don’t want to scan a Makefile every time you write a buffer
  • we don’t dispatch a new test run if one is already in progress (this can happen when you save a file before a previous run is completed)
  • the latest test results will always be available to view; I just need to press F7 to see it

Now my workflow is only three steps:

  1. make a change in source or test code
  2. examine test output
  3. GOTO 1

Here’s a demo:

make test

Automatically run tests in the background

If I explicitly want to run tests in the foreground (but still in a non-blocking way) I can simply press F8. The test results pop up automatically.

make test

Explicitly running tests in the foreground

Extensions

Note that by using a separate function TryTests that checks for the existence of a test harness, the implementation becomes flexible enough to work outside the context of make, for example, we can support pytest by adding a check to see if py.test is in $PATH.

Also, this is implemented for *.py files, but obviously we can use whatever file type we want, and the approach generalizes.

Alternatives

There are other ways to achieve a similar result, e.g.

  • :Make test, then @:, then @@ (manually run tests once, repeat last Ex command, repeat last @ command, respectively)
  • autocmd FileType python nnoremap <F8> :<C-U>call <SID>TryTests()<CR>

but both of these require more manual steps, which leads to a suboptimal workflow.

Conclusion

Writing tests was never easier, and thus I’m encouraged to write tests, and am one step closer to helping my colleagues avoid the Broken Window effect with respect to the software I write.

My experience has been that others will show a similar level of care for software that was previously given to it. So making it easy for me to do the right thing makes it easier for others to do the right thing as well (in theory).