--- -- self-test/test_runner.lua -- -- Execute unit tests and test suites. -- -- Author Jason Perkins -- Copyright (c) 2008-2016 Jason Perkins and the Premake project. --- local p = premake local m = p.modules.self_test local _ = {} function m.runTest(test) local failed = 0 local failedTests = {} local suites = m.getSuites() local suitesKeys, suiteTestsKeys, totalTestCount = _.preprocessTests(suites, test) _.log(term.lightGreen, "[==========]", string.format(" Running %d tests from %d test suites.", totalTestCount, #suitesKeys)) local startTime = os.clock() for index, suiteName in ipairs(suitesKeys) do suite = suites[suiteName] if not m.isSuppressed(suiteName) then local test = { suiteName = suiteName, suite = suite } local suiteFailed, suiteFailedTests = _.runTestSuite(test, suiteTestsKeys[suiteName]) failed = failed + suiteFailed failedTests = table.join(failedTests, suiteFailedTests) end end _.log(term.lightGreen, "[==========]", string.format(" %d tests from %d test suites ran. (%.0f ms total)", totalTestCount, #suitesKeys, (os.clock() - startTime) * 1000)) _.log(term.lightGreen, "[ PASSED ]", string.format(" %d tests.", totalTestCount - failed)) if failed > 0 then _.log(term.lightRed, "[ FAILED ]", string.format(" %d tests, listed below:", failed)) for index, testName in ipairs(failedTests) do _.log(term.lightRed, "[ FAILED ]", " " .. testName) end end return (totalTestCount - failed), failed end function _.runTestSuite(test, keys) local failed = 0 local failedTests = {} _.log(term.lightGreen, "[----------]", string.format(" %d tests from %s", #keys, test.suiteName)) local startTime = os.clock() if test.suite ~= nil then for index, testName in ipairs(keys) do testFunction = test.suite[testName] test.testName = testName test.testFunction = testFunction if m.isValid(test) and not m.isSuppressed(test.suiteName .. "." .. test.testName) then local err = _.runTest(test) if err then failed = failed + 1 table.insert(failedTests, test.suiteName .. "." .. test.testName .. "\n" .. err) end end end end _.log(term.lightGreen, "[----------]", string.format(" %d tests from %s (%.0f ms total)\n", #keys, test.suiteName, (os.clock() - startTime) * 1000)) return failed, failedTests end function _.runTest(test) _.log(term.lightGreen, "[ RUN ]", string.format(" %s.%s", test.suiteName, test.testName)) local startTime = os.clock() local cwd = os.getcwd() local hooks = _.installTestingHooks() _TESTS_DIR = test.suite._TESTS_DIR _SCRIPT_DIR = test.suite._SCRIPT_DIR m.suiteName = test.suiteName m.testName = test.testName local ok, err = _.setupTest(test) if ok then ok, err = _.executeTest(test) end local tok, terr = _.teardownTest(test) ok = ok and tok err = err or terr _.removeTestingHooks(hooks) os.chdir(cwd) if ok then _.log(term.lightGreen, "[ OK ]", string.format(" %s.%s (%.0f ms)", test.suiteName, test.testName, (os.clock() - startTime) * 1000)) return nil else _.log(term.lightRed, "[ FAILED ]", string.format(" %s.%s (%.0f ms)", test.suiteName, test.testName, (os.clock() - startTime) * 1000)) m.print(string.format("%s", err)) return err end end function _.log(color, left, right) term.pushColor(color) io.write(left) term.popColor() m.print(right) end function _.preprocessTests(suites, filter) local suitesKeys = {} local suiteTestsKeys = {} local totalTestCount = 0 for suiteName, suite in pairs(suites) do if not m.isSuppressed(suiteName) and suite ~= nil and (not filter.suiteName or filter.suiteName == suiteName) then local test = {} table.insertsorted(suitesKeys, suiteName) test.suiteName = suiteName test.suite = suite suiteTestsKeys[suiteName] = {} for testName, testFunction in pairs(suite) do test.testName = testName test.testFunction = testFunction if m.isValid(test) and not m.isSuppressed(test.suiteName .. "." .. test.testName) and (not filter.testName or filter.testName == testName) then table.insertsorted(suiteTestsKeys[suiteName], testName) end end totalTestCount = totalTestCount + #suiteTestsKeys[suiteName] end end return suitesKeys, suiteTestsKeys, totalTestCount end function _.installTestingHooks() local hooks = {} hooks.action = _ACTION hooks.options = _OPTIONS hooks.targetOs = _TARGET_OS hooks.io_open = io.open hooks.io_output = io.output hooks.os_writefile_ifnotequal = os.writefile_ifnotequal hooks.p_utf8 = p.utf8 hooks.print = print hooks.setTextColor = term.setTextColor local mt = getmetatable(io.stderr) _.builtin_write = mt.write mt.write = _.stub_stderr_write _OPTIONS = table.shallowcopy(_OPTIONS) or {} setmetatable(_OPTIONS, getmetatable(hooks.options)) io.open = _.stub_io_open io.output = _.stub_io_output os.writefile_ifnotequal = _.stub_os_writefile_ifnotequal print = _.stub_print p.utf8 = _.stub_utf8 term.setTextColor = _.stub_setTextColor stderr_capture = nil p.clearWarnings() p.eol("\n") p.escaper(nil) p.indent("\t") p.api.reset() m.stderr_capture = nil m.value_openedfilename = nil m.value_openedfilemode = nil m.value_closedfile = false return hooks end function _.removeTestingHooks(hooks) p.action.set(hooks.action) _OPTIONS = hooks.options _TARGET_OS = hooks.targetOs io.open = hooks.io_open io.output = hooks.io_output os.writefile_ifnotequal = hooks.os_writefile_ifnotequal p.utf8 = hooks.p_utf8 print = hooks.print term.setTextColor = hooks.setTextColor local mt = getmetatable(io.stderr) mt.write = _.builtin_write end function _.setupTest(test) if type(test.suite.setup) == "function" then return xpcall(test.suite.setup, _.errorHandler) else return true end end function _.executeTest(test) local result, err p.capture(function() result, err = xpcall(test.testFunction, _.errorHandler) end) return result, err end function _.teardownTest(test) if type(test.suite.teardown) == "function" then return xpcall(test.suite.teardown, _.errorHandler) else return true end end function _.errorHandler(err) local msg = err -- if the error doesn't include a stack trace, add one if not msg:find("stack traceback:", 1, true) then msg = debug.traceback(err, 2) end -- trim of the trailing context of the originating xpcall local i = msg:find("[C]: in function 'xpcall'", 1, true) if i then msg = msg:sub(1, i - 3) end -- if the resulting stack trace is only one level deep, ignore it local n = select(2, msg:gsub('\n', '\n')) if n == 2 then msg = msg:sub(1, msg:find('\n', 1, true) - 1) end return msg end function _.stub_io_open(fname, mode) m.value_openedfilename = fname m.value_openedfilemode = mode return { read = function() end, close = function() m.value_closedfile = true end } end function _.stub_io_output(f) end function _.stub_os_writefile_ifnotequal(content, fname) m.value_openedfilename = fname m.value_closedfile = true return 0 end function _.stub_print(s) end function _.stub_stderr_write(...) if select(1, ...) == io.stderr then m.stderr_capture = (m.stderr_capture or "") .. select(2, ...) else return _.builtin_write(...) end end function _.stub_utf8() end function _.stub_setTextColor() end