test_runner.lua 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. ---
  2. -- self-test/test_runner.lua
  3. --
  4. -- Execute unit tests and test suites.
  5. --
  6. -- Author Jason Perkins
  7. -- Copyright (c) 2008-2016 Jason Perkins and the Premake project.
  8. ---
  9. local p = premake
  10. local m = p.modules.self_test
  11. local _ = {}
  12. function m.runTest(tests)
  13. local failed = 0
  14. local failedTests = {}
  15. local suites = m.getSuites()
  16. local suitesKeys, suiteTestsKeys, totalTestCount = _.preprocessTests(suites, tests)
  17. _.log(term.lightGreen, "[==========]", string.format(" Running %d tests from %d test suites.", totalTestCount, #suitesKeys))
  18. local startTime = os.clock()
  19. for index, suiteName in ipairs(suitesKeys) do
  20. suite = suites[suiteName]
  21. if not m.isSuppressed(suiteName) then
  22. local test = {
  23. suiteName = suiteName,
  24. suite = suite
  25. }
  26. local suiteFailed, suiteFailedTests = _.runTestSuite(test, suiteTestsKeys[suiteName])
  27. failed = failed + suiteFailed
  28. failedTests = table.join(failedTests, suiteFailedTests)
  29. end
  30. end
  31. _.log(term.lightGreen, "[==========]", string.format(" %d tests from %d test suites ran. (%.0f ms total)", totalTestCount, #suitesKeys, (os.clock() - startTime) * 1000))
  32. _.log(term.lightGreen, "[ PASSED ]", string.format(" %d tests.", totalTestCount - failed))
  33. if failed > 0 then
  34. _.log(term.lightRed, "[ FAILED ]", string.format(" %d tests, listed below:", failed))
  35. for index, testName in ipairs(failedTests) do
  36. _.log(term.lightRed, "[ FAILED ]", " " .. testName)
  37. end
  38. end
  39. return (totalTestCount - failed), failed
  40. end
  41. function _.runTestSuite(test, keys)
  42. local failed = 0
  43. local failedTests = {}
  44. _.log(term.lightGreen, "[----------]", string.format(" %d tests from %s", #keys, test.suiteName))
  45. local startTime = os.clock()
  46. if test.suite ~= nil then
  47. for index, testName in ipairs(keys) do
  48. testFunction = test.suite[testName]
  49. test.testName = testName
  50. test.testFunction = testFunction
  51. if m.isValid(test) and not m.isSuppressed(test.suiteName .. "." .. test.testName) then
  52. local err = _.runTest(test)
  53. if err then
  54. failed = failed + 1
  55. table.insert(failedTests, test.suiteName .. "." .. test.testName .. "\n" .. err)
  56. end
  57. end
  58. end
  59. end
  60. _.log(term.lightGreen, "[----------]", string.format(" %d tests from %s (%.0f ms total)\n", #keys, test.suiteName, (os.clock() - startTime) * 1000))
  61. return failed, failedTests
  62. end
  63. function _.runTest(test)
  64. _.log(term.lightGreen, "[ RUN ]", string.format(" %s.%s", test.suiteName, test.testName))
  65. local startTime = os.clock()
  66. local cwd = os.getcwd()
  67. local hooks = _.installTestingHooks()
  68. _TESTS_DIR = test.suite._TESTS_DIR
  69. _SCRIPT_DIR = test.suite._SCRIPT_DIR
  70. m.suiteName = test.suiteName
  71. m.testName = test.testName
  72. local ok, err = _.setupTest(test)
  73. if ok then
  74. ok, err = _.executeTest(test)
  75. end
  76. local tok, terr = _.teardownTest(test)
  77. ok = ok and tok
  78. err = err or terr
  79. _.removeTestingHooks(hooks)
  80. os.chdir(cwd)
  81. if ok then
  82. _.log(term.lightGreen, "[ OK ]", string.format(" %s.%s (%.0f ms)", test.suiteName, test.testName, (os.clock() - startTime) * 1000))
  83. return nil
  84. else
  85. _.log(term.lightRed, "[ FAILED ]", string.format(" %s.%s (%.0f ms)", test.suiteName, test.testName, (os.clock() - startTime) * 1000))
  86. m.print(string.format("%s", err))
  87. return err
  88. end
  89. end
  90. function _.log(color, left, right)
  91. term.pushColor(color)
  92. io.write(left)
  93. term.popColor()
  94. m.print(right)
  95. end
  96. function _.preprocessTests(suites, filters)
  97. local suitesKeys = {}
  98. local suiteTestsKeys = {}
  99. local totalTestCount = 0
  100. for i, filter in ipairs(filters) do
  101. for suiteName, suite in pairs(suites) do
  102. if not m.isSuppressed(suiteName) and suite ~= nil and (not filter.suiteName or filter.suiteName == suiteName) then
  103. local test = {}
  104. test.suiteName = suiteName
  105. test.suite = suite
  106. if not table.contains(suitesKeys, suiteName) then
  107. table.insertsorted(suitesKeys, suiteName)
  108. suiteTestsKeys[suiteName] = {}
  109. end
  110. for testName, testFunction in pairs(suite) do
  111. test.testName = testName
  112. test.testFunction = testFunction
  113. if m.isValid(test) and not m.isSuppressed(test.suiteName .. "." .. test.testName) and (not filter.testName or filter.testName == testName) then
  114. if not table.contains(suiteTestsKeys[suiteName], testName) then
  115. table.insertsorted(suiteTestsKeys[suiteName], testName)
  116. totalTestCount = totalTestCount + 1
  117. end
  118. end
  119. end
  120. end
  121. end
  122. end
  123. return suitesKeys, suiteTestsKeys, totalTestCount
  124. end
  125. function _.installTestingHooks()
  126. local hooks = {}
  127. hooks.action = _ACTION
  128. hooks.options = _OPTIONS
  129. hooks.targetOs = _TARGET_OS
  130. hooks.io_open = io.open
  131. hooks.io_output = io.output
  132. hooks.os_writefile_ifnotequal = os.writefile_ifnotequal
  133. hooks.p_utf8 = p.utf8
  134. hooks.print = print
  135. hooks.setTextColor = term.setTextColor
  136. local mt = getmetatable(io.stderr)
  137. _.builtin_write = mt.write
  138. mt.write = _.stub_stderr_write
  139. _OPTIONS = table.shallowcopy(_OPTIONS) or {}
  140. setmetatable(_OPTIONS, getmetatable(hooks.options))
  141. io.open = _.stub_io_open
  142. io.output = _.stub_io_output
  143. os.writefile_ifnotequal = _.stub_os_writefile_ifnotequal
  144. print = _.stub_print
  145. p.utf8 = _.stub_utf8
  146. term.setTextColor = _.stub_setTextColor
  147. stderr_capture = nil
  148. p.clearWarnings()
  149. p.eol("\n")
  150. p.escaper(nil)
  151. p.indent("\t")
  152. p.api.reset()
  153. m.stderr_capture = nil
  154. m.value_openedfilename = nil
  155. m.value_openedfilemode = nil
  156. m.value_closedfile = false
  157. return hooks
  158. end
  159. function _.removeTestingHooks(hooks)
  160. p.action.set(hooks.action)
  161. _OPTIONS = hooks.options
  162. _TARGET_OS = hooks.targetOs
  163. io.open = hooks.io_open
  164. io.output = hooks.io_output
  165. os.writefile_ifnotequal = hooks.os_writefile_ifnotequal
  166. p.utf8 = hooks.p_utf8
  167. print = hooks.print
  168. term.setTextColor = hooks.setTextColor
  169. local mt = getmetatable(io.stderr)
  170. mt.write = _.builtin_write
  171. end
  172. function _.setupTest(test)
  173. if type(test.suite.setup) == "function" then
  174. return xpcall(test.suite.setup, _.errorHandler)
  175. else
  176. return true
  177. end
  178. end
  179. function _.executeTest(test)
  180. local result, err
  181. p.capture(function()
  182. result, err = xpcall(test.testFunction, _.errorHandler)
  183. end)
  184. return result, err
  185. end
  186. function _.teardownTest(test)
  187. if type(test.suite.teardown) == "function" then
  188. return xpcall(test.suite.teardown, _.errorHandler)
  189. else
  190. return true
  191. end
  192. end
  193. function _.errorHandler(err)
  194. local msg = err
  195. -- if the error doesn't include a stack trace, add one
  196. if not msg:find("stack traceback:", 1, true) then
  197. msg = debug.traceback(err, 2)
  198. end
  199. -- trim of the trailing context of the originating xpcall
  200. local i = msg:find("[C]: in function 'xpcall'", 1, true)
  201. if i then
  202. msg = msg:sub(1, i - 3)
  203. end
  204. -- if the resulting stack trace is only one level deep, ignore it
  205. local n = select(2, msg:gsub('\n', '\n'))
  206. if n == 2 then
  207. msg = msg:sub(1, msg:find('\n', 1, true) - 1)
  208. end
  209. return msg
  210. end
  211. function _.stub_io_open(fname, mode)
  212. m.value_openedfilename = fname
  213. m.value_openedfilemode = mode
  214. return {
  215. read = function()
  216. end,
  217. close = function()
  218. m.value_closedfile = true
  219. end
  220. }
  221. end
  222. function _.stub_io_output(f)
  223. end
  224. function _.stub_os_writefile_ifnotequal(content, fname)
  225. m.value_openedfilename = fname
  226. m.value_closedfile = true
  227. return 0
  228. end
  229. function _.stub_print(s)
  230. end
  231. function _.stub_stderr_write(...)
  232. if select(1, ...) == io.stderr then
  233. m.stderr_capture = (m.stderr_capture or "") .. select(2, ...)
  234. else
  235. return _.builtin_write(...)
  236. end
  237. end
  238. function _.stub_utf8()
  239. end
  240. function _.stub_setTextColor()
  241. end