├── LICENSE ├── README.md └── plugin ├── phpunit.py └── vim-phpunitqf.vim /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPUnit runner for Vim 2 | 3 | PHPUnitQf is a plugin for Vim that allows you to run PHPUnit tests easily from the Vim window. It then reads the output and puts the errors into the [quickfix][1] list, so you can easily jump to them. It's configurable too, so if you use a PHPUnit wrapper command or have a special set of arguments, then that's no problem. 4 | 5 | ### How to use 6 | 7 | In a Vim window, run: 8 | 9 | ```vim 10 | :Test 11 | ``` 12 | 13 | Where `` are passed directly to the PHPUnit command. To set up a custom PHPUnit command see the configuration section below. You can also set default arguments which will always be passed. 14 | 15 | ### Installation 16 | 17 | Installation is easy-peasy if you're using [Vundle][2]. Just add this to your *.vimrc* file: 18 | 19 | ```vim 20 | Bundle 'joonty/vim-phpunitqf.git' 21 | ``` 22 | and run `vim +BundleInstall +qall` from a terminal. 23 | 24 | If you aren't using vundle, you will have to extract the files in each folder to the correct folder in *.vim/*. 25 | 26 | **Note:** your vim installation must be compiled with *python* for this plugin to work. 27 | 28 | ### Configuration 29 | 30 | By default, the command used to run PHPUnit is `phpunit`, but you can change it in your vimrc file with: 31 | 32 | ```vim 33 | let g:phpunit_cmd = "/usr/bin/mytest" 34 | ``` 35 | 36 | To pass arguments to the command, use: 37 | 38 | ```vim 39 | let g:phpunit_args = "--configuration /path/to/config" 40 | ``` 41 | 42 | You can also specify arguments to be placed after the "dynamic" argument (the argument passed when running from within Vim): 43 | 44 | ```vim 45 | let g:phpunit_args_append = "--repeat" 46 | ``` 47 | 48 | The output is written to a temporary file. You can change the location of this (default value is */tmp/vim_phpunit.out*) with: 49 | 50 | ```vim 51 | let g:phpunit_tmpfile = "/my/new/tmp/file" 52 | ``` 53 | 54 | #### Callback for modifying arguments 55 | 56 | You can do some more in-depth argument handling when running tests, with callback functions. You can define a callback function and tell PHPUnitQf to use that function to parse and potentially modify the arguments passed to PHPUnit when running `:Test`. The callback function takes the arguments as it's parameter, and returns the modified arguments. Think of it as a filter for your test arguments. You can tell PHPUnitQf to use the callback with: 57 | 58 | ```vim 59 | let g:phpunit_callback = "MyCallbackFunction" 60 | ``` 61 | 62 | A callback function looks like this: 63 | 64 | ```vim 65 | function! MyCallbackFunction(args) 66 | let l:args = a:args 67 | " Do something with the arguments 68 | return l:args 69 | endfunction 70 | ``` 71 | 72 | For example, let's say I want `:Test` on it's own (no arguments) to try and find and run a test case for the current file. I would write a callback that accepts the arguments to PHPUnit, and tries to work out a test case from the current filename if the arguments are empty. Here's one that I use that works for CakePHP (I won't explain it, see if you can understand it :D): 73 | 74 | ```vim 75 | " Let PHPUnitQf use the callback function 76 | let g:phpunit_callback = "CakePHPTestCallback" 77 | 78 | function! CakePHPTestCallback(args) 79 | " Trim white space 80 | let l:args = substitute(a:args, '^\s*\(.\{-}\)\s*$', '\1', '') 81 | 82 | " If no arguments are passed to :Test 83 | if len(l:args) is 0 84 | let l:file = expand('%') 85 | if l:file =~ "^app/Test/Case.*" 86 | " If the current file is a unit test 87 | let l:args = substitute(l:file,'^app/Test/Case/\(.\{-}\)Test\.php$','\1','') 88 | else 89 | " Otherwise try and run the test for this file 90 | let l:args = substitute(l:file,'^app/\(.\{-}\)\.php$','\1','') 91 | endif 92 | endif 93 | return l:args 94 | endfunction 95 | ``` 96 | 97 | ### License 98 | 99 | This plugin is released under the [MIT License][3]. 100 | 101 | [1]: http://vimdoc.sourceforge.net/htmldoc/quickfix.html 102 | [2]: https://github.com/gmarik/vundle 103 | [3]: https://raw.github.com/joonty/vim-phpunitqf/master/LICENSE 104 | -------------------------------------------------------------------------------- /plugin/phpunit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import vim 3 | import re 4 | 5 | def print_error(msg): 6 | vim.command("echohl Error | echo \""+msg+"\" | echohl None") 7 | 8 | """ 9 | " Super intelligent debug function 10 | """ 11 | def debug(msg): 12 | if debugOn == 1: 13 | print msg 14 | 15 | def parse_test_output( ): 16 | global debugOn 17 | try: 18 | fname = vim.eval("g:phpunit_tmpfile") 19 | debugOn = int(vim.eval("g:phpunit_debug")) 20 | fd = open(fname) 21 | except IOError as e: 22 | print_error("Failed to find or open the PHPUnit error log - the command may have failed") 23 | 24 | try: 25 | manager = TestErrorManager() 26 | parser = TestOutputParser(manager) 27 | parser.parse(fd) 28 | fd.close() 29 | if manager.hasErrors(): 30 | manager.addToQuickfix() 31 | elif parser.foundTestSummary == False: 32 | vim.command('echohl Error | echo "phpunit failed to run (or so it seems)" | echohl None') 33 | vim.command('cclose') 34 | vim.command('call setqflist([])') 35 | else: 36 | vim.command('echohl WarningMsg | echo "No test errors or failures" | echohl None') 37 | vim.command('cclose') 38 | vim.command('call setqflist([])') 39 | except ParserException, e: 40 | print_error("An error has occured in parsing the PHPUnit error log: " + e.args[0]) 41 | except Exception, e: 42 | print_error("An error has occured: " + str(sys.exc_info())) 43 | 44 | " Holds information about a single error " 45 | class TestError: 46 | message = None 47 | file = None 48 | line = None 49 | 50 | def __init__(self,type): 51 | self.type = type 52 | 53 | def setMessage(self,message): 54 | self.message = message 55 | 56 | def getEscapedMessage(self): 57 | return self._escape(self.message) 58 | 59 | def setFile(self,file): 60 | self.file = file 61 | 62 | def getType(self): 63 | return self.type 64 | 65 | def getEscapedFile(self): 66 | return self._escape(self.file) 67 | 68 | def setLine(self,line): 69 | self.line = line 70 | 71 | def getEscapedLine(self): 72 | return self._escape(self.line) 73 | 74 | def assertComplete(self): 75 | if self.message == None: 76 | return False 77 | elif self.file == None: 78 | return False 79 | elif self.line == None: 80 | return False 81 | else: 82 | return True 83 | 84 | def _escape(self,string): 85 | return string.replace("'","\"") 86 | 87 | 88 | " A wrapper for a list of errors and failures " 89 | class TestErrorManager: 90 | def __init__(self): 91 | self.errors = [] 92 | 93 | def add(self,error): 94 | if error.assertComplete(): 95 | debug("Adding error: \""+ error.message+"\"") 96 | self.errors.append(error) 97 | else: 98 | print_error("Incomplete error object") 99 | 100 | def addToQuickfix(self): 101 | vimstr = "[" 102 | idx = 1 103 | for error in self.errors: 104 | if idx > 1: 105 | vimstr += "," 106 | vimstr += "{" 107 | vimstr += "'filename':'"+error.getEscapedFile()+"'," 108 | vimstr += "'lnum':'"+error.getEscapedLine()+"'," 109 | vimstr += "'text':'"+error.getEscapedMessage()+"'," 110 | vimstr += "'type':'"+error.getType()+"'" 111 | vimstr += "}" 112 | idx += 1 113 | vimstr += "]" 114 | debug("Vim list: "+vimstr) 115 | vim.command('call setqflist('+vimstr+')') 116 | vim.command('copen') 117 | 118 | def hasErrors(self): 119 | return len(self.errors) > 0 120 | 121 | 122 | " A parser for the error log " 123 | class TestOutputParser: 124 | parsingType = False 125 | currentError = None 126 | foundErrors = False 127 | foundTestSummary = False 128 | fileReg = "^([^:]+):([0-9]+)$" 129 | 130 | def __init__(self,manager): 131 | self.errors = manager 132 | 133 | def parse(self,fd): 134 | k = 0 135 | try: 136 | for line in fd: 137 | if self.foundTestSummary == True: 138 | self.parseLine(fd,line) 139 | if "Time: " in line: 140 | self.foundTestSummary = True 141 | k = k + 1 142 | except StopIteration: 143 | pass 144 | 145 | def parseLine(self,fd,line): 146 | matchObj = re.match('There (?:were|was) ([0-9]*) (error|failure|skipped|incomplete)',line,re.M) 147 | if matchObj: 148 | type = matchObj.group(2) 149 | self.foundErrors = True 150 | debug("Parsing "+type) 151 | self.parsingType = type[0].upper() 152 | if self.foundErrors: 153 | self.readError(fd,line) 154 | 155 | def readError(self,fd,line): 156 | matchObj = re.match("^[0-9]\) ([^:]+)::(.+)",line) 157 | if matchObj: 158 | testClass = matchObj.group(1) 159 | testMethod = matchObj.group(2) 160 | 161 | error = TestError(self.parsingType) 162 | 163 | message = "(" + testClass + "::" + testMethod + ")" 164 | 165 | # Get multi-line message 166 | while True: 167 | line = fd.next().strip() 168 | if re.match(self.fileReg,line): 169 | break 170 | 171 | message += "\n" + line 172 | 173 | error.setMessage(message) 174 | 175 | testFile = testClass.replace("Case","") + ".php" 176 | foundFile = False 177 | firstLine = line 178 | 179 | while True: 180 | 181 | fileName = line 182 | if len(fileName) == 0: 183 | if foundFile == False: 184 | print_error("Failed to find the file for test class "+testClass+", using top file") 185 | ret = self.parseFileLine(firstLine,error) 186 | if ret == False: 187 | raise ParserException("Failed to parse the log") 188 | break 189 | elif foundFile == False and testFile in fileName: 190 | foundFile = self.parseFileLine(fileName,error) 191 | if foundFile == False: 192 | print_error("Failed to parse line "+line) 193 | try: 194 | line = fd.next().strip() 195 | except StopIteration: 196 | line = "" 197 | 198 | def parseFileLine(self,line,error): 199 | matchObj = re.match(self.fileReg,line) 200 | if matchObj: 201 | filePath = matchObj.group(1) 202 | lineNo = matchObj.group(2) 203 | debug("File: "+filePath+", "+lineNo) 204 | error.setFile(filePath) 205 | error.setLine(lineNo) 206 | self.errors.add(error) 207 | return True 208 | else: 209 | debug("Failed to parse file from line: "+line) 210 | return False 211 | 212 | 213 | class ParserException(Exception): 214 | pass 215 | -------------------------------------------------------------------------------- /plugin/vim-phpunitqf.vim: -------------------------------------------------------------------------------- 1 | " ------------------------------------------------------------------------------ 2 | " Vim PHPUnitQf {{{ 3 | " 4 | " Author: Jon Cairns 5 | " 6 | " Description: 7 | " Run PHPUnit from within Vim and parse the output into the quickfix list, to 8 | " allow for easy navigation to failed test methods. 9 | " 10 | " Requires: Vim 6.0 or newer, compiled with Python. 11 | " 12 | " Install: 13 | " Put this file and the python file in the vim plugins directory (~/.vim/plugin) 14 | " to load it automatically, or load it manually with :so sauce.vim. 15 | " 16 | " License: MIT 17 | " 18 | " }}} 19 | " ------------------------------------------------------------------------------ 20 | 21 | if filereadable($VIMRUNTIME."/plugin/phpunit.py") 22 | pyfile $VIMRUNTIME/plugin/phpunit.py 23 | elseif filereadable($HOME."/.vim/plugin/phpunit.py") 24 | pyfile $HOME/.vim/plugin/phpunit.py 25 | else 26 | " when we use pathogen for instance 27 | let $CUR_DIRECTORY=expand(":p:h") 28 | 29 | if filereadable($CUR_DIRECTORY."/phpunit.py") 30 | pyfile $CUR_DIRECTORY/phpunit.py 31 | else 32 | call confirm('phpunitqf.vim: Unable to find phpunit.py. Place it in either your home vim directory or in the Vim runtime directory.', 'OK') 33 | finish 34 | endif 35 | endif 36 | 37 | " PHPUnit command 38 | if !exists("g:phpunit_cmd") 39 | let g:phpunit_cmd='phpunit' 40 | endif 41 | 42 | " Static arguments passed to the PHPUnit command 43 | if !exists("g:phpunit_args") 44 | let g:phpunit_args='' 45 | endif 46 | 47 | " Static arguments passed to the PHPUnit command after the dynamic argument 48 | if !exists("g:phpunit_args_append") 49 | let g:phpunit_args_append='' 50 | endif 51 | 52 | " Location of temporary error log 53 | if !exists("g:phpunit_tmpfile") 54 | let g:phpunit_tmpfile="/tmp/vim_phpunit.out" 55 | endif 56 | 57 | " Debug enabled 58 | if !exists("g:phpunit_debug") 59 | let g:phpunit_debug=0 60 | endif 61 | 62 | if !exists("g:phpunit_callback") 63 | let g:phpunit_callback = "" 64 | endif 65 | 66 | command! -nargs=* Test call s:RunPHPUnitTests() 67 | command! TestOutput call s:OpenPHPUnitOutput() 68 | 69 | " Run PHPUnit command and python parser 70 | function! s:RunPHPUnitTests(arg) 71 | let s:args = a:arg 72 | if len(g:phpunit_callback) > 0 73 | exe "let s:args = ".g:phpunit_callback."('".s:args."')" 74 | endif 75 | " Truncate current log file 76 | call system("> ".g:phpunit_tmpfile) 77 | exe "!".g:phpunit_cmd." ".g:phpunit_args." ".s:args." ".g:phpunit_args_append." 2>&1 | tee ".g:phpunit_tmpfile 78 | python parse_test_output() 79 | endfunction 80 | 81 | " Open the test output 82 | function! s:OpenPHPUnitOutput() 83 | exe "sp ".g:phpunit_tmpfile 84 | endfunction 85 | --------------------------------------------------------------------------------