TestMethod.lua 17 KB

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