TestMethod.lua 20 KB

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