test.vim 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. " Test runs `go test` in the current directory. If compile is true, it'll
  2. " compile the tests instead of running them (useful to catch errors in the
  3. " test files). Any other argument is appended to the final `go test` command.
  4. function! go#test#Test(bang, compile, ...) abort
  5. let args = ["test"]
  6. " don't run the test, only compile it. Useful to capture and fix errors.
  7. if a:compile
  8. let testfile = tempname() . ".vim-go.test"
  9. call extend(args, ["-c", "-o", testfile])
  10. endif
  11. if exists('g:go_build_tags')
  12. let tags = get(g:, 'go_build_tags')
  13. call extend(args, ["-tags", tags])
  14. endif
  15. if a:0
  16. let goargs = a:000
  17. " do not expand for coverage mode as we're passing the arg ourself
  18. if a:1 != '-coverprofile'
  19. " expand all wildcards(i.e: '%' to the current file name)
  20. let goargs = map(copy(a:000), "expand(v:val)")
  21. endif
  22. if !(has('nvim') || go#util#has_job())
  23. let goargs = go#util#Shelllist(goargs, 1)
  24. endif
  25. call extend(args, goargs, 1)
  26. else
  27. " only add this if no custom flags are passed
  28. let timeout = get(g:, 'go_test_timeout', '10s')
  29. call add(args, printf("-timeout=%s", timeout))
  30. endif
  31. if get(g:, 'go_echo_command_info', 1)
  32. if a:compile
  33. call go#util#EchoProgress("compiling tests ...")
  34. else
  35. call go#util#EchoProgress("testing...")
  36. endif
  37. endif
  38. if go#util#has_job()
  39. " use vim's job functionality to call it asynchronously
  40. let job_args = {
  41. \ 'cmd': ['go'] + args,
  42. \ 'bang': a:bang,
  43. \ 'winnr': winnr(),
  44. \ 'dir': getcwd(),
  45. \ 'compile_test': a:compile,
  46. \ 'jobdir': fnameescape(expand("%:p:h")),
  47. \ }
  48. call s:test_job(job_args)
  49. return
  50. elseif has('nvim')
  51. " use nvims's job functionality
  52. if get(g:, 'go_term_enabled', 0)
  53. let id = go#term#new(a:bang, ["go"] + args)
  54. else
  55. let id = go#jobcontrol#Spawn(a:bang, "test", "GoTest", args)
  56. endif
  57. return id
  58. endif
  59. call go#cmd#autowrite()
  60. redraw
  61. let command = "go " . join(args, ' ')
  62. let out = go#tool#ExecuteInDir(command)
  63. " TODO(bc): When the output is JSON, the JSON should be run through a
  64. " filter to produce lines that are more easily described by errorformat.
  65. let l:listtype = go#list#Type("GoTest")
  66. let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd '
  67. let dir = getcwd()
  68. execute cd fnameescape(expand("%:p:h"))
  69. if go#util#ShellError() != 0
  70. call go#list#ParseFormat(l:listtype, s:errorformat(), split(out, '\n'), command)
  71. let errors = go#list#Get(l:listtype)
  72. call go#list#Window(l:listtype, len(errors))
  73. if !empty(errors) && !a:bang
  74. call go#list#JumpToFirst(l:listtype)
  75. elseif empty(errors)
  76. " failed to parse errors, output the original content
  77. call go#util#EchoError(out)
  78. endif
  79. call go#util#EchoError("[test] FAIL")
  80. else
  81. call go#list#Clean(l:listtype)
  82. if a:compile
  83. call go#util#EchoSuccess("[test] SUCCESS")
  84. else
  85. call go#util#EchoSuccess("[test] PASS")
  86. endif
  87. endif
  88. execute cd . fnameescape(dir)
  89. endfunction
  90. " Testfunc runs a single test that surrounds the current cursor position.
  91. " Arguments are passed to the `go test` command.
  92. function! go#test#Func(bang, ...) abort
  93. " search flags legend (used only)
  94. " 'b' search backward instead of forward
  95. " 'c' accept a match at the cursor position
  96. " 'n' do Not move the cursor
  97. " 'W' don't wrap around the end of the file
  98. "
  99. " for the full list
  100. " :help search
  101. let test = search('func \(Test\|Example\)', "bcnW")
  102. if test == 0
  103. echo "vim-go: [test] no test found immediate to cursor"
  104. return
  105. end
  106. let line = getline(test)
  107. let name = split(split(line, " ")[1], "(")[0]
  108. let args = [a:bang, 0, "-run", name . "$"]
  109. if a:0
  110. call extend(args, a:000)
  111. else
  112. " only add this if no custom flags are passed
  113. let timeout = get(g:, 'go_test_timeout', '10s')
  114. call add(args, printf("-timeout=%s", timeout))
  115. endif
  116. call call('go#test#Test', args)
  117. endfunction
  118. function! s:test_job(args) abort
  119. let status = {
  120. \ 'desc': 'current status',
  121. \ 'type': "test",
  122. \ 'state': "started",
  123. \ }
  124. if a:args.compile_test
  125. let status.state = "compiling"
  126. endif
  127. " autowrite is not enabled for jobs
  128. call go#cmd#autowrite()
  129. let state = {
  130. \ 'exited': 0,
  131. \ 'closed': 0,
  132. \ 'exitval': 0,
  133. \ 'messages': [],
  134. \ 'args': a:args,
  135. \ 'compile_test': a:args.compile_test,
  136. \ 'status_dir': expand('%:p:h'),
  137. \ 'started_at': reltime()
  138. \ }
  139. call go#statusline#Update(state.status_dir, status)
  140. function! s:callback(chan, msg) dict
  141. call add(self.messages, a:msg)
  142. endfunction
  143. function! s:exit_cb(job, exitval) dict
  144. let self.exited = 1
  145. let self.exitval = a:exitval
  146. let status = {
  147. \ 'desc': 'last status',
  148. \ 'type': "test",
  149. \ 'state': "pass",
  150. \ }
  151. if self.compile_test
  152. let status.state = "success"
  153. endif
  154. if a:exitval
  155. let status.state = "failed"
  156. endif
  157. if get(g:, 'go_echo_command_info', 1)
  158. if a:exitval == 0
  159. if self.compile_test
  160. call go#util#EchoSuccess("[test] SUCCESS")
  161. else
  162. call go#util#EchoSuccess("[test] PASS")
  163. endif
  164. else
  165. call go#util#EchoError("[test] FAIL")
  166. endif
  167. endif
  168. let elapsed_time = reltimestr(reltime(self.started_at))
  169. " strip whitespace
  170. let elapsed_time = substitute(elapsed_time, '^\s*\(.\{-}\)\s*$', '\1', '')
  171. let status.state .= printf(" (%ss)", elapsed_time)
  172. call go#statusline#Update(self.status_dir, status)
  173. if self.closed
  174. call s:show_errors(self.args, self.exitval, self.messages)
  175. endif
  176. endfunction
  177. function! s:close_cb(ch) dict
  178. let self.closed = 1
  179. if self.exited
  180. call s:show_errors(self.args, self.exitval, self.messages)
  181. endif
  182. endfunction
  183. " explicitly bind the callbacks to state so that self within them always
  184. " refers to state. See :help Partial for more information.
  185. let start_options = {
  186. \ 'callback': funcref("s:callback", [], state),
  187. \ 'exit_cb': funcref("s:exit_cb", [], state),
  188. \ 'close_cb': funcref("s:close_cb", [], state)
  189. \ }
  190. " pre start
  191. let dir = getcwd()
  192. let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd '
  193. let jobdir = fnameescape(expand("%:p:h"))
  194. execute cd . jobdir
  195. call job_start(a:args.cmd, start_options)
  196. " post start
  197. execute cd . fnameescape(dir)
  198. endfunction
  199. " show_errors parses the given list of lines of a 'go test' output and returns
  200. " a quickfix compatible list of errors. It's intended to be used only for go
  201. " test output.
  202. function! s:show_errors(args, exit_val, messages) abort
  203. let l:listtype = go#list#Type("GoTest")
  204. if a:exit_val == 0
  205. call go#list#Clean(l:listtype)
  206. return
  207. endif
  208. " TODO(bc): When messages is JSON, the JSON should be run through a
  209. " filter to produce lines that are more easily described by errorformat.
  210. let l:listtype = go#list#Type("GoTest")
  211. let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd '
  212. try
  213. execute cd a:args.jobdir
  214. call go#list#ParseFormat(l:listtype, s:errorformat(), a:messages, join(a:args.cmd))
  215. let errors = go#list#Get(l:listtype)
  216. finally
  217. execute cd . fnameescape(a:args.dir)
  218. endtry
  219. if !len(errors)
  220. " failed to parse errors, output the original content
  221. call go#util#EchoError(a:messages)
  222. call go#util#EchoError(a:args.dir)
  223. return
  224. endif
  225. if a:args.winnr == winnr()
  226. call go#list#Window(l:listtype, len(errors))
  227. if !empty(errors) && !a:args.bang
  228. call go#list#JumpToFirst(l:listtype)
  229. endif
  230. endif
  231. endfunction
  232. let s:efm= ""
  233. let s:go_test_show_name=0
  234. function! s:errorformat() abort
  235. " NOTE(arslan): once we get JSON output everything will be easier :).
  236. " TODO(bc): When the output is JSON, the JSON should be run through a
  237. " filter to produce lines that are more easily described by errorformat.
  238. " https://github.com/golang/go/issues/2981.
  239. let goroot = go#util#goroot()
  240. let show_name=get(g:, 'go_test_show_name', 0)
  241. if s:efm != "" && s:go_test_show_name == show_name
  242. return s:efm
  243. endif
  244. let s:go_test_show_name = show_name
  245. " each level of test indents the test output 4 spaces. Capturing groups
  246. " (e.g. \(\)) cannot be used in an errorformat, but non-capturing groups can
  247. " (e.g. \%(\)).
  248. let indent = '%\\%( %\\)%#'
  249. " match compiler errors
  250. let format = "%f:%l:%c: %m"
  251. " ignore `go test -v` output for starting tests
  252. let format .= ",%-G=== RUN %.%#"
  253. " ignore `go test -v` output for passing tests
  254. let format .= ",%-G" . indent . "--- PASS: %.%#"
  255. " Match failure lines.
  256. "
  257. " Test failures start with '--- FAIL: ', followed by the test name followed
  258. " by a space the duration of the test in parentheses
  259. "
  260. " e.g.:
  261. " '--- FAIL: TestSomething (0.00s)'
  262. if show_name
  263. let format .= ",%G" . indent . "--- FAIL: %m (%.%#)"
  264. else
  265. let format .= ",%-G" . indent . "--- FAIL: %.%#"
  266. endif
  267. " Matches test output lines.
  268. "
  269. " All test output lines start with the test indentation and a tab, followed
  270. " by the filename, a colon, the line number, another colon, a space, and the
  271. " message. e.g.:
  272. " '\ttime_test.go:30: Likely problem: the time zone files have not been installed.'
  273. let format .= ",%A" . indent . "%\\t%\\+%f:%l: %m"
  274. " also match lines that don't have a message (i.e. the message begins with a
  275. " newline or is the empty string):
  276. " e.g.:
  277. " t.Errorf("\ngot %v; want %v", actual, expected)
  278. " t.Error("")
  279. let format .= ",%A" . indent . "%\\t%\\+%f:%l: "
  280. " Match the 2nd and later lines of multi-line output. These lines are
  281. " indented the number of spaces for the level of nesting of the test,
  282. " followed by two tabs, followed by the message.
  283. "
  284. " Treat these lines as if they are stand-alone lines of output by using %G.
  285. " It would also be valid to treat these lines as if they were the
  286. " continuation of a multi-line error by using %C instead of %G, but that
  287. " would also require that all test errors using a %A or %E modifier to
  288. " indicate that they're multiple lines of output, but in that case the lines
  289. " get concatenated in the quickfix list, which is not what users typically
  290. " want when writing a newline into their test output.
  291. let format .= ",%G" . indent . "%\\t%\\{2}%m"
  292. " set the format for panics.
  293. " handle panics from test timeouts
  294. let format .= ",%+Gpanic: test timed out after %.%\\+"
  295. " handle non-timeout panics
  296. " In addition to 'panic', check for 'fatal error' to support older versions
  297. " of Go that used 'fatal error'.
  298. "
  299. " Panics come in two flavors. When the goroutine running the tests panics,
  300. " `go test` recovers and tries to exit more cleanly. In that case, the panic
  301. " message is suffixed with ' [recovered]'. If the panic occurs in a
  302. " different goroutine, it will not be suffixed with ' [recovered]'.
  303. let format .= ",%+Afatal error: %.%# [recovered]"
  304. let format .= ",%+Apanic: %.%# [recovered]"
  305. let format .= ",%+Afatal error: %.%#"
  306. let format .= ",%+Apanic: %.%#"
  307. " Match address lines in stacktraces produced by panic.
  308. "
  309. " Address lines in the stack trace have leading tabs, followed by the path
  310. " to the file. The file path is followed by a colon and then the line number
  311. " within the file where the panic occurred. After that there's a space and
  312. " hexadecimal number.
  313. "
  314. " e.g.:
  315. " '\t/usr/local/go/src/time.go:1313 +0x5d'
  316. " panicaddress, and readyaddress are identical except for
  317. " panicaddress sets the filename and line number.
  318. let panicaddress = "%\\t%f:%l +0x%[0-9A-Fa-f]%\\+"
  319. let readyaddress = "%\\t%\\f%\\+:%\\d%\\+ +0x%[0-9A-Fa-f]%\\+"
  320. " stdlib address is identical to readyaddress, except it matches files
  321. " inside GOROOT.
  322. let stdlibaddress = "%\\t" . goroot . "%\\f%\\+:%\\d%\\+ +0x%[0-9A-Fa-f]%\\+"
  323. " Match and ignore the running goroutine line.
  324. let format .= ",%-Cgoroutine %\\d%\\+ [running]:"
  325. " Match address lines that refer to stdlib, but consider them informational
  326. " only. This is to catch the lines after the first address line in the
  327. " running goroutine of a panic stack trace. Ideally, this wouldn't be
  328. " necessary, but when a panic happens in the goroutine running a test, it's
  329. " recovered and another panic is created, so the stack trace actually has
  330. " the line that caused the original panic a couple of addresses down the
  331. " stack.
  332. let format .= ",%-C" . stdlibaddress
  333. " Match address lines in the first matching goroutine. This means the panic
  334. " message will only be shown as the error message in the first address of
  335. " the running goroutine's stack.
  336. let format .= ",%Z" . panicaddress
  337. " Match and ignore panic address without being part of a multi-line message.
  338. " This is to catch those lines that come after the top most non-standard
  339. " library line in stack traces.
  340. let format .= ",%-G" . readyaddress
  341. " Match and ignore exit status lines (produced when go test panics) whether
  342. " part of a multi-line message or not, because these lines sometimes come
  343. " before and sometimes after panic stacktraces.
  344. let format .= ",%-Cexit status %[0-9]%\\+"
  345. "let format .= ",exit status %[0-9]%\\+"
  346. " Match and ignore exit failure lines whether part of a multi-line message
  347. " or not, because these lines sometimes come before and sometimes after
  348. " panic stacktraces.
  349. let format .= ",%-CFAIL%\\t%.%#"
  350. "let format .= ",FAIL%\\t%.%#"
  351. " Match and ignore everything else in multi-line messages.
  352. let format .= ",%-C%.%#"
  353. " Match and ignore everything else not in a multi-line message:
  354. let format .= ",%-G%.%#"
  355. let s:efm = format
  356. return s:efm
  357. endfunction
  358. " vim: sw=2 ts=2 et