nvim.lua 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. local pretty = require 'pl.pretty'
  2. local t_global = require('test.testutil')
  3. local colors = setmetatable({}, {
  4. __index = function()
  5. return function(s)
  6. return s == nil and '' or tostring(s)
  7. end
  8. end,
  9. })
  10. local enable_colors = true
  11. if os.getenv 'TEST_COLORS' then
  12. local test_colors = os.getenv('TEST_COLORS'):lower()
  13. local disable_colors = test_colors == 'false'
  14. or test_colors == '0'
  15. or test_colors == 'no'
  16. or test_colors == 'off'
  17. enable_colors = not disable_colors
  18. end
  19. if enable_colors then
  20. colors = require 'term.colors'
  21. end
  22. return function(options)
  23. local busted = require 'busted'
  24. local handler = require 'busted.outputHandlers.base'()
  25. local c = {
  26. succ = function(s)
  27. return colors.bright(colors.green(s))
  28. end,
  29. skip = function(s)
  30. return colors.bright(colors.yellow(s))
  31. end,
  32. fail = function(s)
  33. return colors.bright(colors.magenta(s))
  34. end,
  35. errr = function(s)
  36. return colors.bright(colors.red(s))
  37. end,
  38. test = tostring,
  39. file = colors.cyan,
  40. time = colors.dim,
  41. note = colors.yellow,
  42. sect = function(s)
  43. return colors.green(colors.dim(s))
  44. end,
  45. nmbr = colors.bright,
  46. }
  47. local repeatSuiteString = '\nRepeating all tests (run %d of %d) . . .\n\n'
  48. local randomizeString = c.note('Note: Randomizing test order with a seed of %d.\n')
  49. local globalSetup = c.sect('--------') .. ' Global test environment setup.\n'
  50. local fileStartString = c.sect('--------') .. ' Running tests from ' .. c.file('%s') .. '\n'
  51. local runString = c.sect('RUN ') .. ' ' .. c.test('%s') .. ': '
  52. local successString = c.succ('OK') .. '\n'
  53. local skippedString = c.skip('SKIP') .. '\n'
  54. local failureString = c.fail('FAIL') .. '\n'
  55. local errorString = c.errr('ERR') .. '\n'
  56. local fileEndString = c.sect('--------')
  57. .. ' '
  58. .. c.nmbr('%d')
  59. .. ' %s from '
  60. .. c.file('%s')
  61. .. ' '
  62. .. c.time('(%.2f ms total)')
  63. .. '\n\n'
  64. local globalTeardown = c.sect('--------') .. ' Global test environment teardown.\n'
  65. local suiteEndString = c.sect('========')
  66. .. ' '
  67. .. c.nmbr('%d')
  68. .. ' %s from '
  69. .. c.nmbr('%d')
  70. .. ' test %s ran. '
  71. .. c.time('(%.2f ms total)')
  72. .. '\n'
  73. local successStatus = c.succ('PASSED ') .. ' ' .. c.nmbr('%d') .. ' %s.\n'
  74. local timeString = c.time('%.2f ms')
  75. local summaryStrings = {
  76. skipped = {
  77. header = c.skip('SKIPPED ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n',
  78. test = c.skip('SKIPPED ') .. ' %s\n',
  79. footer = ' ' .. c.nmbr('%d') .. ' SKIPPED %s\n',
  80. },
  81. failure = {
  82. header = c.fail('FAILED ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n',
  83. test = c.fail('FAILED ') .. ' %s\n',
  84. footer = ' ' .. c.nmbr('%d') .. ' FAILED %s\n',
  85. },
  86. error = {
  87. header = c.errr('ERROR ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n',
  88. test = c.errr('ERROR ') .. ' %s\n',
  89. footer = ' ' .. c.nmbr('%d') .. ' %s\n',
  90. },
  91. }
  92. local fileCount = 0
  93. local fileTestCount = 0
  94. local testCount = 0
  95. local successCount = 0
  96. local skippedCount = 0
  97. local failureCount = 0
  98. local errorCount = 0
  99. local pendingDescription = function(pending)
  100. local string = ''
  101. if type(pending.message) == 'string' then
  102. string = string .. pending.message .. '\n'
  103. elseif pending.message ~= nil then
  104. string = string .. pretty.write(pending.message) .. '\n'
  105. end
  106. return string
  107. end
  108. local failureDescription = function(failure)
  109. local string = failure.randomseed and ('Random seed: ' .. failure.randomseed .. '\n') or ''
  110. if type(failure.message) == 'string' then
  111. string = string .. failure.message
  112. elseif failure.message == nil then
  113. string = string .. 'Nil error'
  114. else
  115. string = string .. pretty.write(failure.message)
  116. end
  117. string = string .. '\n'
  118. if options.verbose and failure.trace and failure.trace.traceback then
  119. string = string .. failure.trace.traceback .. '\n'
  120. end
  121. return string
  122. end
  123. local getFileLine = function(element)
  124. local fileline = ''
  125. if element.trace or element.trace.short_src then
  126. fileline = colors.cyan(element.trace.short_src)
  127. .. ' @ '
  128. .. colors.cyan(element.trace.currentline)
  129. .. ': '
  130. end
  131. return fileline
  132. end
  133. local getTestList = function(status, count, list, getDescription)
  134. local string = ''
  135. local header = summaryStrings[status].header
  136. if count > 0 and header then
  137. local tests = (count == 1 and 'test' or 'tests')
  138. local errors = (count == 1 and 'error' or 'errors')
  139. string = header:format(count, status == 'error' and errors or tests)
  140. local testString = summaryStrings[status].test
  141. if testString then
  142. for _, t in ipairs(list) do
  143. local fullname = getFileLine(t.element) .. colors.bright(t.name)
  144. string = string .. testString:format(fullname)
  145. string = string .. getDescription(t)
  146. end
  147. end
  148. end
  149. return string
  150. end
  151. local getSummary = function(status, count)
  152. local string = ''
  153. local footer = summaryStrings[status].footer
  154. if count > 0 and footer then
  155. local tests = (count == 1 and 'TEST' or 'TESTS')
  156. local errors = (count == 1 and 'ERROR' or 'ERRORS')
  157. string = footer:format(count, status == 'error' and errors or tests)
  158. end
  159. return string
  160. end
  161. local getSummaryString = function()
  162. local tests = (successCount == 1 and 'test' or 'tests')
  163. local string = successStatus:format(successCount, tests)
  164. string = string .. getTestList('skipped', skippedCount, handler.pendings, pendingDescription)
  165. string = string .. getTestList('failure', failureCount, handler.failures, failureDescription)
  166. string = string .. getTestList('error', errorCount, handler.errors, failureDescription)
  167. string = string .. ((skippedCount + failureCount + errorCount) > 0 and '\n' or '')
  168. string = string .. getSummary('skipped', skippedCount)
  169. string = string .. getSummary('failure', failureCount)
  170. string = string .. getSummary('error', errorCount)
  171. return string
  172. end
  173. handler.suiteReset = function()
  174. fileCount = 0
  175. fileTestCount = 0
  176. testCount = 0
  177. successCount = 0
  178. skippedCount = 0
  179. failureCount = 0
  180. errorCount = 0
  181. return nil, true
  182. end
  183. handler.suiteStart = function(_suite, count, total, randomseed)
  184. if total > 1 then
  185. io.write(repeatSuiteString:format(count, total))
  186. end
  187. if randomseed then
  188. io.write(randomizeString:format(randomseed))
  189. end
  190. io.write(globalSetup)
  191. io.flush()
  192. return nil, true
  193. end
  194. local function getElapsedTime(tbl)
  195. if tbl.duration then
  196. return tbl.duration * 1000
  197. else
  198. return tonumber('nan')
  199. end
  200. end
  201. handler.suiteEnd = function(suite, _count, _total)
  202. local elapsedTime_ms = getElapsedTime(suite)
  203. local tests = (testCount == 1 and 'test' or 'tests')
  204. local files = (fileCount == 1 and 'file' or 'files')
  205. io.write(globalTeardown)
  206. io.write(suiteEndString:format(testCount, tests, fileCount, files, elapsedTime_ms))
  207. io.write(getSummaryString())
  208. if failureCount > 0 or errorCount > 0 then
  209. io.write(t_global.read_nvim_log(nil, true))
  210. end
  211. io.flush()
  212. return nil, true
  213. end
  214. handler.fileStart = function(file)
  215. fileTestCount = 0
  216. io.write(fileStartString:format(vim.fs.normalize(file.name)))
  217. io.flush()
  218. return nil, true
  219. end
  220. handler.fileEnd = function(file)
  221. local elapsedTime_ms = getElapsedTime(file)
  222. local tests = (fileTestCount == 1 and 'test' or 'tests')
  223. fileCount = fileCount + 1
  224. io.write(
  225. fileEndString:format(fileTestCount, tests, vim.fs.normalize(file.name), elapsedTime_ms)
  226. )
  227. io.flush()
  228. return nil, true
  229. end
  230. handler.testStart = function(element, _parent)
  231. local testid = _G._nvim_test_id or ''
  232. local desc = ('%s %s'):format(testid, handler.getFullName(element))
  233. io.write(runString:format(desc))
  234. io.flush()
  235. return nil, true
  236. end
  237. local function write_status(element, string)
  238. io.write(timeString:format(getElapsedTime(element)) .. ' ' .. string)
  239. io.flush()
  240. end
  241. handler.testEnd = function(element, _parent, status, _debug)
  242. local string
  243. fileTestCount = fileTestCount + 1
  244. testCount = testCount + 1
  245. if status == 'success' then
  246. successCount = successCount + 1
  247. string = successString
  248. elseif status == 'pending' then
  249. skippedCount = skippedCount + 1
  250. string = skippedString
  251. elseif status == 'failure' then
  252. failureCount = failureCount + 1
  253. string = failureString .. failureDescription(handler.failures[#handler.failures])
  254. elseif status == 'error' then
  255. errorCount = errorCount + 1
  256. string = errorString .. failureDescription(handler.errors[#handler.errors])
  257. else
  258. string = 'unexpected test status! (' .. status .. ')'
  259. end
  260. write_status(element, string)
  261. return nil, true
  262. end
  263. handler.error = function(element, _parent, _message, _debug)
  264. if element.descriptor ~= 'it' then
  265. write_status(element, failureDescription(handler.errors[#handler.errors]))
  266. errorCount = errorCount + 1
  267. end
  268. return nil, true
  269. end
  270. busted.subscribe({ 'suite', 'reset' }, handler.suiteReset)
  271. busted.subscribe({ 'suite', 'start' }, handler.suiteStart)
  272. busted.subscribe({ 'suite', 'end' }, handler.suiteEnd)
  273. busted.subscribe({ 'file', 'start' }, handler.fileStart)
  274. busted.subscribe({ 'file', 'end' }, handler.fileEnd)
  275. busted.subscribe({ 'test', 'start' }, handler.testStart, { predicate = handler.cancelOnPending })
  276. busted.subscribe({ 'test', 'end' }, handler.testEnd, { predicate = handler.cancelOnPending })
  277. busted.subscribe({ 'failure' }, handler.error)
  278. busted.subscribe({ 'error' }, handler.error)
  279. return handler
  280. end