local TERM_COLOR_CODE_WHITE = '\27[37;1m' local TERM_COLOR_CODE_GREEN = '\27[32;1m' local TERM_COLOR_CODE_RED = '\27[31;1m' local TERM_COLOR_CODE_GREY = '\27[30;0m' local ASSERT_ERROR_TYPE = [[ Types not compatible: Expected: %s (%s) Gotten: %s (%s) ]] local ASSERT_ERROR_VALUE = [[ Values of type '%s' not equal: Expected: %s Gotten: %s ]] local ASSERT_ERROR_TABLE_VALUE = [[ Values in tables not equal: For key: %s Expected: %s (%s) Gotten: %s (%s) ]] local function assert_equal (expected, gotten) if type(gotten) ~= type(expected) then error(ASSERT_ERROR_TYPE:format(tostring(gotten), type(gotten), tostring(expected), type(expected))) elseif type(expected) == 'table' and gotten ~= expected then for key, expected_value in pairs(expected) do if expected_value ~= gotten[key] then error(ASSERT_ERROR_TABLE_VALUE:format(key, expected_value, type(expected_value), gotten[key], type(gotten[key]))) end end elseif gotten ~= expected then error(ASSERT_ERROR_VALUE:format(type(gotten), gotten, expected)) end end local libraries_tests_and_wrappers = { assert_equal = assert_equal } for k, v in pairs(_G) do libraries_tests_and_wrappers[k] = v end local function indent_string (str, indent) return indent .. str:gsub('\n', '\n'..indent) end local TEST_SUITE_TRACEBACK = "\n\t\t[C]: in function 'xpcall'\n\t\t./TestSuite.lua" local function find_cutoff_index_for_traceback_string (str) local index = str:find(TEST_SUITE_TRACEBACK, 1, true) return index end -------------------------------------------------------------------------------- local TestSuite = {} TestSuite.__index = TestSuite function TestSuite.new (module_name) local new_test_suite = setmetatable({}, TestSuite) new_test_suite.name = module_name new_test_suite.submodules = {} new_test_suite.tests = {} new_test_suite.custom_env = {} return new_test_suite end -------------------------------------------------------------------------------- function TestSuite:setEnviroment (new_enviroment) self.custom_env = new_enviroment end function TestSuite:createUniqueEnviroment () local new_enviroment = {} for k, v in pairs(libraries_tests_and_wrappers) do new_enviroment[k] = v end for k, v in pairs(self.custom_env) do new_enviroment[k] = v end return new_enviroment end -------------------------------------------------------------------------------- function TestSuite:addModules (modules) for _, module in ipairs(modules) do table.insert(self.submodules, module) end end function TestSuite:addTest (test_name, extra_info, test_func) if type(extra_info) == 'function' then extra_info, test_func = test_func, extra_info end table.insert(self.tests, { name = test_name, test = test_func, extra_info = extra_info or {} }) end -------------------------------------------------------------------------------- function TestSuite:runSubmodules (prefix, indent) local total_errors, total_tests = 0, 0 for _, module in ipairs(self.submodules) do local errors, tests = module:runTests(prefix, indent) total_errors, total_tests = total_errors + errors, total_tests + tests end return total_errors, total_tests end local function setup_debug_hooks (extra_info) assert(not (extra_info.max_time and extra_info.max_lines), "TEST CONFIG ERROR: Only one line hook allowed pr. test!") local trace if extra_info.max_time then local stop_time = os.clock() + extra_info.max_time trace = function (l) if os.clock() >= stop_time then debug.sethook(nil, 'l') error("Test timed out! This is usually symptom of an infinite loop!") end end elseif extra_info.max_lines then local line_countdown = 10 + extra_info.max_lines trace = function (l) line_countdown = line_countdown - 1 if line_countdown <= 0 then debug.sethook(nil, 'l') error("Test exteeded allowed number of lines! This is usually symptom of an infinite loop! (Or too low an estimate!)") end end end debug.sethook(trace, 'l') end function TestSuite:runTests (parent_prefix, parent_indent) local prefix, indent = self.name, (parent_indent or 0) + 2 if parent_prefix then prefix = parent_prefix .. '.' .. prefix else io.write('\n## Running tests ##\n\n') end local nr_errors, nr_tests = self:runSubmodules(prefix, indent) local function error_handler (err) return err..debug.traceback('', 4) end local function print_status (left, right, color) local term_width = 80 local color = color or '' io.write(TERM_COLOR_CODE_WHITE..left..': '..string.rep(' ', term_width + 1-#left-#right)..color..right..'\n'..TERM_COLOR_CODE_GREY) end for index, test in ipairs(self.tests) do nr_tests = nr_tests + 1 -- Setup Test Env, and name local ext_name = prefix .. '.' .. test.name local env = self:createUniqueEnviroment() setfenv(test.test, env) setup_debug_hooks(test.extra_info) -- Call tests local success, traceback = xpcall(test.test, error_handler) -- Unset line hook, if set earlier debug.sethook(nil, 'l') -- Write work (or not.) if success then print_status(ext_name, 'SUCCESS!', TERM_COLOR_CODE_GREEN) else print_status(ext_name, 'ERROR!', TERM_COLOR_CODE_RED) traceback = indent_string(traceback, '\t') local stop_index = find_cutoff_index_for_traceback_string(traceback) io.write('\n'..TERM_COLOR_CODE_GREY) io.write(traceback:sub(1, stop_index)) io.write('\n\n') nr_errors = nr_errors + 1 end end if nr_errors == 0 then print_status(prefix, 'NO ERRORS! WELL DONE!', TERM_COLOR_CODE_GREEN) elseif nr_errors == 1 then print_status(prefix, 'A SINGLE ERROR! ALMOST THERE!', TERM_COLOR_CODE_RED) else print_status(prefix, nr_errors..' ERRORS! GET TO WORK!', TERM_COLOR_CODE_RED) end if not parent_prefix then self:writeStatus(nr_errors, nr_tests) end return nr_errors, nr_tests end function TestSuite:writeStatus ( nr_errors, nr_tests ) io.write(TERM_COLOR_CODE_WHITE) io.write('\n## All tests run! ##\n\n') local WIDTH_OR_BAR = 70 local BAR_CHAR = '#' local nr_successes = nr_tests - nr_errors local size_of_green_bar = math.floor( WIDTH_OR_BAR * nr_successes/nr_tests) io.write('Status: ') io.write(nr_errors == 0 and TERM_COLOR_CODE_GREEN or TERM_COLOR_CODE_RED) io.write(BAR_CHAR:rep(WIDTH_OR_BAR)) io.write(TERM_COLOR_CODE_WHITE) local numbers_str = ' ' .. nr_successes .. ' / ' .. nr_tests .. ' ' local half_bar = (WIDTH_OR_BAR-#numbers_str)/2 local green_bar = BAR_CHAR:rep(math.floor(half_bar)) .. numbers_str .. BAR_CHAR:rep(math.ceil(half_bar)) green_bar = TERM_COLOR_CODE_GREEN .. green_bar:sub(1, size_of_green_bar) .. TERM_COLOR_CODE_RED .. green_bar:sub(size_of_green_bar + 1) -- Get io.write('\n\nSummary: '..green_bar..'\n\n') io.write(TERM_COLOR_CODE_WHITE) end -------------------------------------------------------------------------------- return TestSuite