TestMethod.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. -- @class - TestMethod
  2. -- @desc - used to run a specific method from a module's /test/ suite
  3. -- each assertion is tracked and then printed to output
  4. TestMethod = {
  5. -- @method - TestMethod:new()
  6. -- @desc - create a new TestMethod object
  7. -- @param {string} method - string of method name to run
  8. -- @param {TestMethod} testmethod - parent testmethod this test belongs to
  9. -- @return {table} - returns the new Test object
  10. new = function(self, method, testmodule)
  11. local test = {
  12. testmodule = testmodule,
  13. method = method,
  14. asserts = {},
  15. start = love.timer.getTime(),
  16. finish = 0,
  17. count = 0,
  18. passed = false,
  19. skipped = false,
  20. skipreason = '',
  21. fatal = '',
  22. message = nil,
  23. result = {},
  24. colors = {
  25. red = {1, 0, 0, 1},
  26. redpale = {1, 0.5, 0.5, 1},
  27. red07 = {0.7, 0, 0, 1},
  28. green = {0, 1, 0, 1},
  29. greenhalf = {0, 0.5, 0, 1},
  30. greenfade = {0, 1, 0, 0.5},
  31. blue = {0, 0, 1, 1},
  32. bluefade = {0, 0, 1, 0.5},
  33. yellow = {1, 1, 0, 1},
  34. black = {0, 0, 0, 1},
  35. white = {1, 1, 1, 1}
  36. },
  37. delay = 0,
  38. delayed = false
  39. }
  40. setmetatable(test, self)
  41. self.__index = self
  42. return test
  43. end,
  44. -- @method - TestMethod:assertEquals()
  45. -- @desc - used to assert two values are equals
  46. -- @param {any} expected - expected value of the test
  47. -- @param {any} actual - actual value of the test
  48. -- @param {string} label - label for this test to use in exports
  49. -- @return {nil}
  50. assertEquals = function(self, expected, actual, label)
  51. self.count = self.count + 1
  52. table.insert(self.asserts, {
  53. key = 'assert #' .. tostring(self.count),
  54. passed = expected == actual,
  55. message = 'expected \'' .. tostring(expected) .. '\' got \'' ..
  56. tostring(actual) .. '\'',
  57. test = label
  58. })
  59. end,
  60. -- @method - TestMethod:assertPixels()
  61. -- @desc - checks a list of coloured pixels agaisnt given imgdata
  62. -- @param {ImageData} imgdata - image data to check
  63. -- @param {table} pixels - map of colors to list of pixel coords, i.e.
  64. -- { blue = { {1, 1}, {2, 2}, {3, 4} } }
  65. -- @return {nil}
  66. assertPixels = function(self, imgdata, pixels, label)
  67. for i, v in pairs(pixels) do
  68. local col = self.colors[i]
  69. local pixels = v
  70. for p=1,#pixels do
  71. local coord = pixels[p]
  72. local tr, tg, tb, ta = imgdata:getPixel(coord[1], coord[2])
  73. local compare_id = tostring(coord[1]) .. ',' .. tostring(coord[2])
  74. -- prevent us getting stuff like 0.501960785 for 0.5 red
  75. tr = math.floor((tr*10)+0.5)/10
  76. tg = math.floor((tg*10)+0.5)/10
  77. tb = math.floor((tb*10)+0.5)/10
  78. ta = math.floor((ta*10)+0.5)/10
  79. -- @TODO add some sort pixel tolerance to the coords
  80. self:assertEquals(col[1], tr, 'check pixel r for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')')
  81. self:assertEquals(col[2], tg, 'check pixel g for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')')
  82. self:assertEquals(col[3], tb, 'check pixel b for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')')
  83. self:assertEquals(col[4], ta, 'check pixel a for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')')
  84. end
  85. end
  86. end,
  87. -- @method - TestMethod:assertNotEquals()
  88. -- @desc - used to assert two values are not equal
  89. -- @param {any} expected - expected value of the test
  90. -- @param {any} actual - actual value of the test
  91. -- @param {string} label - label for this test to use in exports
  92. -- @return {nil}
  93. assertNotEquals = function(self, expected, actual, label)
  94. self.count = self.count + 1
  95. table.insert(self.asserts, {
  96. key = 'assert #' .. tostring(self.count),
  97. passed = expected ~= actual,
  98. message = 'avoiding \'' .. tostring(expected) .. '\' got \'' ..
  99. tostring(actual) .. '\'',
  100. test = label
  101. })
  102. end,
  103. -- @method - TestMethod:assertRange()
  104. -- @desc - used to check a value is within an expected range
  105. -- @param {number} actual - actual value of the test
  106. -- @param {number} min - minimum value the actual should be >= to
  107. -- @param {number} max - maximum value the actual should be <= to
  108. -- @param {string} label - label for this test to use in exports
  109. -- @return {nil}
  110. assertRange = function(self, actual, min, max, label)
  111. self.count = self.count + 1
  112. table.insert(self.asserts, {
  113. key = 'assert #' .. tostring(self.count),
  114. passed = actual >= min and actual <= max,
  115. message = 'value \'' .. tostring(actual) .. '\' out of range \'' ..
  116. tostring(min) .. '-' .. tostring(max) .. '\'',
  117. test = label
  118. })
  119. end,
  120. -- @method - TestMethod:assertMatch()
  121. -- @desc - used to check a value is within a list of values
  122. -- @param {number} list - list of valid values for the test
  123. -- @param {number} actual - actual value of the test to check is in the list
  124. -- @param {string} label - label for this test to use in exports
  125. -- @return {nil}
  126. assertMatch = function(self, list, actual, label)
  127. self.count = self.count + 1
  128. local found = false
  129. for l=1,#list do
  130. if list[l] == actual then found = true end;
  131. end
  132. table.insert(self.asserts, {
  133. key = 'assert #' .. tostring(self.count),
  134. passed = found == true,
  135. message = 'value \'' .. tostring(actual) .. '\' not found in \'' ..
  136. table.concat(list, ',') .. '\'',
  137. test = label
  138. })
  139. end,
  140. -- @method - TestMethod:assertGreaterEqual()
  141. -- @desc - used to check a value is >= than a certain target value
  142. -- @param {any} target - value to check the test agaisnt
  143. -- @param {any} actual - actual value of the test
  144. -- @param {string} label - label for this test to use in exports
  145. -- @return {nil}
  146. assertGreaterEqual = function(self, target, actual, label)
  147. self.count = self.count + 1
  148. local passing = false
  149. if target ~= nil and actual ~= nil then
  150. passing = actual >= target
  151. end
  152. table.insert(self.asserts, {
  153. key = 'assert #' .. tostring(self.count),
  154. passed = passing,
  155. message = 'value \'' .. tostring(actual) .. '\' not >= \'' ..
  156. tostring(target) .. '\'',
  157. test = label
  158. })
  159. end,
  160. -- @method - TestMethod:assertLessEqual()
  161. -- @desc - used to check a value is <= than a certain target value
  162. -- @param {any} target - value to check the test agaisnt
  163. -- @param {any} actual - actual value of the test
  164. -- @param {string} label - label for this test to use in exports
  165. -- @return {nil}
  166. assertLessEqual = function(self, target, actual, label)
  167. self.count = self.count + 1
  168. local passing = false
  169. if target ~= nil and actual ~= nil then
  170. passing = actual <= target
  171. end
  172. table.insert(self.asserts, {
  173. key = 'assert #' .. tostring(self.count),
  174. passed = passing,
  175. message = 'value \'' .. tostring(actual) .. '\' not <= \'' ..
  176. tostring(target) .. '\'',
  177. test = label
  178. })
  179. end,
  180. -- @method - TestMethod:assertObject()
  181. -- @desc - used to check a table is a love object, this runs 3 seperate
  182. -- tests to check table has the basic properties of an object
  183. -- @note - actual object functionality tests are done in the objects module
  184. -- @param {table} obj - table to check is a valid love object
  185. -- @return {nil}
  186. assertObject = function(self, obj)
  187. self:assertNotNil(obj)
  188. self:assertEquals('userdata', type(obj), 'check is userdata')
  189. if obj ~= nil then
  190. self:assertNotEquals(nil, obj:type(), 'check has :type()')
  191. end
  192. end,
  193. -- @method - TestMethod:assertNotNil()
  194. -- @desc - quick assert for value not nil
  195. -- @param {any} value - value to check not nil
  196. -- @return {nil}
  197. assertNotNil = function (self, value, err)
  198. self:assertNotEquals(nil, value, 'check not nil')
  199. if err ~= nil then
  200. table.insert(self.asserts, {
  201. key = 'assert #' .. tostring(self.count),
  202. passed = false,
  203. message = err,
  204. test = 'assert not nil catch'
  205. })
  206. end
  207. end,
  208. -- @method - TestMethod:skipTest()
  209. -- @desc - used to mark this test as skipped for a specific reason
  210. -- @param {string} reason - reason why method is being skipped
  211. -- @return {nil}
  212. skipTest = function(self, reason)
  213. self.skipped = true
  214. self.skipreason = reason
  215. end,
  216. -- currently unused
  217. setDelay = function(self, frames)
  218. self.delay = frames
  219. self.delayed = true
  220. love.test.delayed = self
  221. end,
  222. isDelayed = function(self)
  223. return self.delayed
  224. end,
  225. -- @method - TestMethod:evaluateTest()
  226. -- @desc - evaluates the results of all assertions for a final restult
  227. -- @return {nil}
  228. evaluateTest = function(self)
  229. local failure = ''
  230. local failures = 0
  231. for a=1,#self.asserts do
  232. -- @TODO just return first failed assertion msg? or all?
  233. -- currently just shows the first assert that failed
  234. if self.asserts[a].passed == false and self.skipped == false then
  235. if failure == '' then failure = self.asserts[a] end
  236. failures = failures + 1
  237. end
  238. end
  239. if self.fatal ~= '' then failure = self.fatal end
  240. local passed = tostring(#self.asserts - failures)
  241. local total = '(' .. passed .. '/' .. tostring(#self.asserts) .. ')'
  242. if self.skipped == true then
  243. self.testmodule.skipped = self.testmodule.skipped + 1
  244. love.test.totals[3] = love.test.totals[3] + 1
  245. self.result = {
  246. total = '',
  247. result = "SKIP",
  248. passed = false,
  249. message = '(0/0) - method skipped [' .. self.skipreason .. ']'
  250. }
  251. else
  252. if failure == '' and #self.asserts > 0 then
  253. self.passed = true
  254. self.testmodule.passed = self.testmodule.passed + 1
  255. love.test.totals[1] = love.test.totals[1] + 1
  256. self.result = {
  257. total = total,
  258. result = 'PASS',
  259. passed = true,
  260. message = nil
  261. }
  262. else
  263. self.passed = false
  264. self.testmodule.failed = self.testmodule.failed + 1
  265. love.test.totals[2] = love.test.totals[2] + 1
  266. if #self.asserts == 0 then
  267. local msg = 'no asserts defined'
  268. if self.fatal ~= '' then msg = self.fatal end
  269. self.result = {
  270. total = total,
  271. result = 'FAIL',
  272. passed = false,
  273. key = 'test',
  274. message = msg
  275. }
  276. else
  277. local key = failure['key']
  278. if failure['test'] ~= nil then
  279. key = key .. ' [' .. failure['test'] .. ']'
  280. end
  281. self.result = {
  282. total = total,
  283. result = 'FAIL',
  284. passed = false,
  285. key = key,
  286. message = failure['message']
  287. }
  288. end
  289. end
  290. end
  291. self:printResult()
  292. end,
  293. -- @method - TestMethod:printResult()
  294. -- @desc - prints the result of the test to the console as well as appends
  295. -- the XML + HTML for the test to the testsuite output
  296. -- @return {nil}
  297. printResult = function(self)
  298. -- get total timestamp
  299. -- @TODO make nicer, just need a 3DP ms value
  300. self.finish = love.timer.getTime() - self.start
  301. love.test.time = love.test.time + self.finish
  302. self.testmodule.time = self.testmodule.time + self.finish
  303. local endtime = UtilTimeFormat(love.timer.getTime() - self.start)
  304. -- get failure/skip message for output (if any)
  305. local failure = ''
  306. local output = ''
  307. if self.passed == false and self.skipped == false then
  308. failure = '\t\t\t<failure message="' .. self.result.key .. ' ' ..
  309. self.result.message .. '">' .. self.result.key .. ' ' .. self.result.message .. '</failure>\n'
  310. output = self.result.key .. ' ' .. self.result.message
  311. -- append failures if any to report md
  312. love.test.mdfailures = love.test.mdfailures .. '> 🔴 ' .. self.method .. ' \n' ..
  313. '> ' .. output .. ' \n\n'
  314. end
  315. if output == '' and self.skipped == true then
  316. failure = '\t\t\t<skipped message="' .. self.skipreason .. '" />\n'
  317. output = self.skipreason
  318. end
  319. -- append XML for the test class result
  320. self.testmodule.xml = self.testmodule.xml .. '\t\t<testcase classname="' ..
  321. self.method .. '" name="' .. self.method .. '" assertions="' .. tostring(#self.asserts) ..
  322. '" time="' .. endtime .. '">\n' ..
  323. failure .. '\t\t</testcase>\n'
  324. -- unused currently, adds a preview image for certain graphics methods to the output
  325. local preview = ''
  326. -- if self.testmodule.module == 'graphics' then
  327. -- local filename = 'love_test_graphics_rectangle'
  328. -- preview = '<div class="preview">' .. '<img src="' .. filename .. '_expected.png"/><p>Expected</p></div>' ..
  329. -- '<div class="preview"><img src="' .. filename .. '_actual.png"/><p>Actual</p></div>'
  330. -- end
  331. -- append HTML for the test class result
  332. local status = '🔴'
  333. local cls = 'red'
  334. if self.passed == true then status = '🟢'; cls = '' end
  335. if self.skipped == true then status = '🟡'; cls = '' end
  336. self.testmodule.html = self.testmodule.html ..
  337. '<tr class=" ' .. cls .. '">' ..
  338. '<td>' .. status .. '</td>' ..
  339. '<td>' .. self.method .. '</td>' ..
  340. '<td>' .. endtime .. 's</td>' ..
  341. '<td>' .. output .. preview .. '</td>' ..
  342. '</tr>'
  343. -- add message if assert failed
  344. local msg = ''
  345. if self.result.message ~= nil and self.skipped == false then
  346. msg = ' - ' .. self.result.key ..
  347. ' failed - (' .. self.result.message .. ')'
  348. end
  349. if self.skipped == true then
  350. msg = self.result.message
  351. end
  352. -- log final test result to console
  353. -- i know its hacky but its neat soz
  354. local tested = 'love.' .. self.testmodule.module .. '.' .. self.method .. '()'
  355. local matching = string.sub(self.testmodule.spacer, string.len(tested), 40)
  356. self.testmodule:log(
  357. self.testmodule.colors[self.result.result],
  358. ' ' .. tested .. matching,
  359. ' ==> ' .. self.result.result .. ' - ' .. endtime .. 's ' ..
  360. self.result.total .. msg
  361. )
  362. end
  363. }