local pretty = require 'pl.pretty' local t_global = require('test.testutil') local colors = setmetatable({}, { __index = function() return function(s) return s == nil and '' or tostring(s) end end, }) local enable_colors = true if os.getenv 'TEST_COLORS' then local test_colors = os.getenv('TEST_COLORS'):lower() local disable_colors = test_colors == 'false' or test_colors == '0' or test_colors == 'no' or test_colors == 'off' enable_colors = not disable_colors end if enable_colors then colors = require 'term.colors' end return function(options) local busted = require 'busted' local handler = require 'busted.outputHandlers.base'() local c = { succ = function(s) return colors.bright(colors.green(s)) end, skip = function(s) return colors.bright(colors.yellow(s)) end, fail = function(s) return colors.bright(colors.magenta(s)) end, errr = function(s) return colors.bright(colors.red(s)) end, test = tostring, file = colors.cyan, time = colors.dim, note = colors.yellow, sect = function(s) return colors.green(colors.dim(s)) end, nmbr = colors.bright, } local repeatSuiteString = '\nRepeating all tests (run %d of %d) . . .\n\n' local randomizeString = c.note('Note: Randomizing test order with a seed of %d.\n') local globalSetup = c.sect('--------') .. ' Global test environment setup.\n' local fileStartString = c.sect('--------') .. ' Running tests from ' .. c.file('%s') .. '\n' local runString = c.sect('RUN ') .. ' ' .. c.test('%s') .. ': ' local successString = c.succ('OK') .. '\n' local skippedString = c.skip('SKIP') .. '\n' local failureString = c.fail('FAIL') .. '\n' local errorString = c.errr('ERR') .. '\n' local fileEndString = c.sect('--------') .. ' ' .. c.nmbr('%d') .. ' %s from ' .. c.file('%s') .. ' ' .. c.time('(%.2f ms total)') .. '\n\n' local globalTeardown = c.sect('--------') .. ' Global test environment teardown.\n' local suiteEndString = c.sect('========') .. ' ' .. c.nmbr('%d') .. ' %s from ' .. c.nmbr('%d') .. ' test %s ran. ' .. c.time('(%.2f ms total)') .. '\n' local successStatus = c.succ('PASSED ') .. ' ' .. c.nmbr('%d') .. ' %s.\n' local timeString = c.time('%.2f ms') local summaryStrings = { skipped = { header = c.skip('SKIPPED ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n', test = c.skip('SKIPPED ') .. ' %s\n', footer = ' ' .. c.nmbr('%d') .. ' SKIPPED %s\n', }, failure = { header = c.fail('FAILED ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n', test = c.fail('FAILED ') .. ' %s\n', footer = ' ' .. c.nmbr('%d') .. ' FAILED %s\n', }, error = { header = c.errr('ERROR ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n', test = c.errr('ERROR ') .. ' %s\n', footer = ' ' .. c.nmbr('%d') .. ' %s\n', }, } local fileCount = 0 local fileTestCount = 0 local testCount = 0 local successCount = 0 local skippedCount = 0 local failureCount = 0 local errorCount = 0 local pendingDescription = function(pending) local string = '' if type(pending.message) == 'string' then string = string .. pending.message .. '\n' elseif pending.message ~= nil then string = string .. pretty.write(pending.message) .. '\n' end return string end local failureDescription = function(failure) local string = failure.randomseed and ('Random seed: ' .. failure.randomseed .. '\n') or '' if type(failure.message) == 'string' then string = string .. failure.message elseif failure.message == nil then string = string .. 'Nil error' else string = string .. pretty.write(failure.message) end string = string .. '\n' if options.verbose and failure.trace and failure.trace.traceback then string = string .. failure.trace.traceback .. '\n' end return string end local getFileLine = function(element) local fileline = '' if element.trace or element.trace.short_src then fileline = colors.cyan(element.trace.short_src) .. ' @ ' .. colors.cyan(element.trace.currentline) .. ': ' end return fileline end local getTestList = function(status, count, list, getDescription) local string = '' local header = summaryStrings[status].header if count > 0 and header then local tests = (count == 1 and 'test' or 'tests') local errors = (count == 1 and 'error' or 'errors') string = header:format(count, status == 'error' and errors or tests) local testString = summaryStrings[status].test if testString then for _, t in ipairs(list) do local fullname = getFileLine(t.element) .. colors.bright(t.name) string = string .. testString:format(fullname) string = string .. getDescription(t) end end end return string end local getSummary = function(status, count) local string = '' local footer = summaryStrings[status].footer if count > 0 and footer then local tests = (count == 1 and 'TEST' or 'TESTS') local errors = (count == 1 and 'ERROR' or 'ERRORS') string = footer:format(count, status == 'error' and errors or tests) end return string end local getSummaryString = function() local tests = (successCount == 1 and 'test' or 'tests') local string = successStatus:format(successCount, tests) string = string .. getTestList('skipped', skippedCount, handler.pendings, pendingDescription) string = string .. getTestList('failure', failureCount, handler.failures, failureDescription) string = string .. getTestList('error', errorCount, handler.errors, failureDescription) string = string .. ((skippedCount + failureCount + errorCount) > 0 and '\n' or '') string = string .. getSummary('skipped', skippedCount) string = string .. getSummary('failure', failureCount) string = string .. getSummary('error', errorCount) return string end handler.suiteReset = function() fileCount = 0 fileTestCount = 0 testCount = 0 successCount = 0 skippedCount = 0 failureCount = 0 errorCount = 0 return nil, true end handler.suiteStart = function(_suite, count, total, randomseed) if total > 1 then io.write(repeatSuiteString:format(count, total)) end if randomseed then io.write(randomizeString:format(randomseed)) end io.write(globalSetup) io.flush() return nil, true end local function getElapsedTime(tbl) if tbl.duration then return tbl.duration * 1000 else return tonumber('nan') end end handler.suiteEnd = function(suite, _count, _total) local elapsedTime_ms = getElapsedTime(suite) local tests = (testCount == 1 and 'test' or 'tests') local files = (fileCount == 1 and 'file' or 'files') io.write(globalTeardown) io.write(suiteEndString:format(testCount, tests, fileCount, files, elapsedTime_ms)) io.write(getSummaryString()) if failureCount > 0 or errorCount > 0 then io.write(t_global.read_nvim_log(nil, true)) end io.flush() return nil, true end handler.fileStart = function(file) fileTestCount = 0 io.write(fileStartString:format(vim.fs.normalize(file.name))) io.flush() return nil, true end handler.fileEnd = function(file) local elapsedTime_ms = getElapsedTime(file) local tests = (fileTestCount == 1 and 'test' or 'tests') fileCount = fileCount + 1 io.write( fileEndString:format(fileTestCount, tests, vim.fs.normalize(file.name), elapsedTime_ms) ) io.flush() return nil, true end handler.testStart = function(element, _parent) local testid = _G._nvim_test_id or '' local desc = ('%s %s'):format(testid, handler.getFullName(element)) io.write(runString:format(desc)) io.flush() return nil, true end local function write_status(element, string) io.write(timeString:format(getElapsedTime(element)) .. ' ' .. string) io.flush() end handler.testEnd = function(element, _parent, status, _debug) local string fileTestCount = fileTestCount + 1 testCount = testCount + 1 if status == 'success' then successCount = successCount + 1 string = successString elseif status == 'pending' then skippedCount = skippedCount + 1 string = skippedString elseif status == 'failure' then failureCount = failureCount + 1 string = failureString .. failureDescription(handler.failures[#handler.failures]) elseif status == 'error' then errorCount = errorCount + 1 string = errorString .. failureDescription(handler.errors[#handler.errors]) else string = 'unexpected test status! (' .. status .. ')' end write_status(element, string) return nil, true end handler.error = function(element, _parent, _message, _debug) if element.descriptor ~= 'it' then write_status(element, failureDescription(handler.errors[#handler.errors])) errorCount = errorCount + 1 end return nil, true end busted.subscribe({ 'suite', 'reset' }, handler.suiteReset) busted.subscribe({ 'suite', 'start' }, handler.suiteStart) busted.subscribe({ 'suite', 'end' }, handler.suiteEnd) busted.subscribe({ 'file', 'start' }, handler.fileStart) busted.subscribe({ 'file', 'end' }, handler.fileEnd) busted.subscribe({ 'test', 'start' }, handler.testStart, { predicate = handler.cancelOnPending }) busted.subscribe({ 'test', 'end' }, handler.testEnd, { predicate = handler.cancelOnPending }) busted.subscribe({ 'failure' }, handler.error) busted.subscribe({ 'error' }, handler.error) return handler end