TestMethod.lua 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  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:assertRange()
  115. -- @desc - used to check a value is within an expected range
  116. -- @param {number} actual - actual value of the test
  117. -- @param {number} min - minimum value the actual should be >= to
  118. -- @param {number} max - maximum value the actual should be <= to
  119. -- @param {string} label - label for this test to use in exports
  120. -- @return {nil}
  121. assertRange = function(self, actual, min, max, label)
  122. self.count = self.count + 1
  123. table.insert(self.asserts, {
  124. key = 'assert ' .. tostring(self.count),
  125. passed = actual >= min and actual <= max,
  126. message = 'value \'' .. tostring(actual) .. '\' out of range \'' ..
  127. tostring(min) .. '-' .. tostring(max) .. '\'',
  128. test = label or 'no label given'
  129. })
  130. end,
  131. -- @method - TestMethod:assertMatch()
  132. -- @desc - used to check a value is within a list of values
  133. -- @param {number} list - list of valid values for the test
  134. -- @param {number} actual - actual value of the test to check is in the list
  135. -- @param {string} label - label for this test to use in exports
  136. -- @return {nil}
  137. assertMatch = function(self, list, actual, label)
  138. self.count = self.count + 1
  139. local found = false
  140. for l=1,#list do
  141. if list[l] == actual then found = true end;
  142. end
  143. table.insert(self.asserts, {
  144. key = 'assert ' .. tostring(self.count),
  145. passed = found == true,
  146. message = 'value \'' .. tostring(actual) .. '\' not found in \'' ..
  147. table.concat(list, ',') .. '\'',
  148. test = label or 'no label given'
  149. })
  150. end,
  151. -- @method - TestMethod:assertGreaterEqual()
  152. -- @desc - used to check a value is >= than a certain target value
  153. -- @param {any} target - value to check the test agaisnt
  154. -- @param {any} actual - actual value of the test
  155. -- @param {string} label - label for this test to use in exports
  156. -- @return {nil}
  157. assertGreaterEqual = function(self, target, actual, label)
  158. self.count = self.count + 1
  159. local passing = false
  160. if target ~= nil and actual ~= nil then
  161. passing = actual >= target
  162. end
  163. table.insert(self.asserts, {
  164. key = 'assert ' .. tostring(self.count),
  165. passed = passing,
  166. message = 'value \'' .. tostring(actual) .. '\' not >= \'' ..
  167. tostring(target) .. '\'',
  168. test = label or 'no label given'
  169. })
  170. end,
  171. -- @method - TestMethod:assertLessEqual()
  172. -- @desc - used to check a value is <= than a certain target value
  173. -- @param {any} target - value to check the test agaisnt
  174. -- @param {any} actual - actual value of the test
  175. -- @param {string} label - label for this test to use in exports
  176. -- @return {nil}
  177. assertLessEqual = function(self, target, actual, label)
  178. self.count = self.count + 1
  179. local passing = false
  180. if target ~= nil and actual ~= nil then
  181. passing = actual <= target
  182. end
  183. table.insert(self.asserts, {
  184. key = 'assert ' .. tostring(self.count),
  185. passed = passing,
  186. message = 'value \'' .. tostring(actual) .. '\' not <= \'' ..
  187. tostring(target) .. '\'',
  188. test = label or 'no label given'
  189. })
  190. end,
  191. -- @method - TestMethod:assertObject()
  192. -- @desc - used to check a table is a love object, this runs 3 seperate
  193. -- tests to check table has the basic properties of an object
  194. -- @note - actual object functionality tests have their own methods
  195. -- @param {table} obj - table to check is a valid love object
  196. -- @return {nil}
  197. assertObject = function(self, obj)
  198. self:assertNotNil(obj)
  199. self:assertEquals('userdata', type(obj), 'check is userdata')
  200. if obj ~= nil then
  201. self:assertNotEquals(nil, obj:type(), 'check has :type()')
  202. end
  203. end,
  204. -- @method - TestMethod:assertCoords()
  205. -- @desc - used to check a pair of values (usually coordinates)
  206. -- @param {table} obj - table to check is a valid love object
  207. -- @return {nil}
  208. assertCoords = function(self, expected, actual, label)
  209. self.count = self.count + 1
  210. local passing = false
  211. if expected ~= nil and actual ~= nil then
  212. if expected[1] == actual[1] and expected[2] == actual[2] then
  213. passing = true
  214. end
  215. end
  216. table.insert(self.asserts, {
  217. key = 'assert ' .. tostring(self.count),
  218. passed = passing,
  219. message = 'expected \'' .. tostring(expected[1]) .. 'x,' ..
  220. tostring(expected[2]) .. 'y\' got \'' ..
  221. tostring(actual[1]) .. 'x,' .. tostring(actual[2]) .. 'y\'',
  222. test = label or 'no label given'
  223. })
  224. end,
  225. -- @method - TestMethod:assertNotNil()
  226. -- @desc - quick assert for value not nil
  227. -- @param {any} value - value to check not nil
  228. -- @return {nil}
  229. assertNotNil = function (self, value, err)
  230. self:assertNotEquals(nil, value, 'check not nil')
  231. if err ~= nil then
  232. table.insert(self.asserts, {
  233. key = 'assert ' .. tostring(self.count),
  234. passed = false,
  235. message = err,
  236. test = 'assert not nil catch'
  237. })
  238. end
  239. end,
  240. -- @method - TestMethod:compareImg()
  241. -- @desc - compares a given image to the 'expected' version, with a tolerance of
  242. -- 1px in any direction, and then saves it as the 'actual' version for
  243. -- report viewing
  244. -- @param {table} imgdata - imgdata to save as a png
  245. -- @return {nil}
  246. compareImg = function(self, imgdata)
  247. local expected_path = 'tempoutput/expected/love.test.graphics.' ..
  248. self.method .. '-' .. tostring(self.imgs) .. '.png'
  249. local ok, chunk, _ = pcall(love.image.newImageData, expected_path)
  250. if ok == false then return self:assertEquals(true, false, chunk) end
  251. local expected = chunk
  252. local iw = imgdata:getWidth()-1
  253. local ih = imgdata:getHeight()-1
  254. local differences = {}
  255. local rgba_tolerance = self.rgba_tolerance * (1/255)
  256. -- for each pixel, compare the expected vs the actual pixel data
  257. -- by default rgba_tolerance is 0
  258. for ix=0,iw do
  259. for iy=0,ih do
  260. local ir, ig, ib, ia = imgdata:getPixel(ix, iy)
  261. local points = {
  262. {expected:getPixel(ix, iy)}
  263. }
  264. if self.pixel_tolerance > 0 then
  265. if ix > 0 and iy < ih-1 then table.insert(points, {expected:getPixel(ix-1, iy+1)}) end
  266. if ix > 0 then table.insert(points, {expected:getPixel(ix-1, iy)}) end
  267. if ix > 0 and iy > 0 then table.insert(points, {expected:getPixel(ix-1, iy-1)}) end
  268. if iy < ih-1 then table.insert(points, {expected:getPixel(ix, iy+1)}) end
  269. if iy > 0 then table.insert(points, {expected:getPixel(ix, iy-1)}) end
  270. if ix < iw-1 and iy < ih-1 then table.insert(points, {expected:getPixel(ix+1, iy+1)}) end
  271. if ix < iw-1 then table.insert(points, {expected:getPixel(ix+1, iy)}) end
  272. if ix < iw-1 and iy > 0 then table.insert(points, {expected:getPixel(ix+1, iy-1)}) end
  273. end
  274. local has_match_r = false
  275. local has_match_g = false
  276. local has_match_b = false
  277. local has_match_a = false
  278. for t=1,#points do
  279. local epoint = points[t]
  280. if ir >= epoint[1] - rgba_tolerance and ir <= epoint[1] + rgba_tolerance then has_match_r = true; end
  281. if ig >= epoint[2] - rgba_tolerance and ig <= epoint[2] + rgba_tolerance then has_match_g = true; end
  282. if ib >= epoint[3] - rgba_tolerance and ib <= epoint[3] + rgba_tolerance then has_match_b = true; end
  283. if ia >= epoint[4] - rgba_tolerance and ia <= epoint[4] + rgba_tolerance then has_match_a = true; end
  284. end
  285. local matching = has_match_r and has_match_g and has_match_b and has_match_a
  286. local ymatch = ''
  287. local nmatch = ''
  288. if has_match_r then ymatch = ymatch .. 'r' else nmatch = nmatch .. 'r' end
  289. if has_match_g then ymatch = ymatch .. 'g' else nmatch = nmatch .. 'g' end
  290. if has_match_b then ymatch = ymatch .. 'b' else nmatch = nmatch .. 'b' end
  291. if has_match_a then ymatch = ymatch .. 'a' else nmatch = nmatch .. 'a' end
  292. local pixel = tostring(ir)..','..tostring(ig)..','..tostring(ib)..','..tostring(ia)
  293. self:assertEquals(true, matching, 'compare image pixel (' .. pixel .. ') at ' ..
  294. tostring(ix) .. ',' .. tostring(iy) .. ', matching = ' .. ymatch ..
  295. ', not matching = ' .. nmatch .. ' (' .. self.method .. '-' .. tostring(self.imgs) .. ')'
  296. )
  297. -- add difference co-ord for rendering later
  298. if matching ~= true then
  299. table.insert(differences, ix+1)
  300. table.insert(differences, iy+1)
  301. end
  302. end
  303. end
  304. local path = 'tempoutput/actual/love.test.graphics.' ..
  305. self.method .. '-' .. tostring(self.imgs) .. '.png'
  306. imgdata:encode('png', path)
  307. -- if we have differences draw them to a new canvas to display in HTML report
  308. local dpath = 'tempoutput/difference/love.test.graphics.' ..
  309. self.method .. '-' .. tostring(self.imgs) .. '.png'
  310. if #differences > 0 then
  311. local difference = love.graphics.newCanvas(iw+1, ih+1)
  312. love.graphics.setCanvas(difference)
  313. love.graphics.clear(0, 0, 0, 1)
  314. love.graphics.setColor(1, 0, 1, 1)
  315. love.graphics.points(differences)
  316. love.graphics.setColor(1, 1, 1, 1)
  317. love.graphics.setCanvas()
  318. love.graphics.readbackTexture(difference):encode('png', dpath)
  319. -- otherwise clear the old difference file (if any) to stop it coming up
  320. -- in future reports when there's no longer a difference
  321. elseif love.filesystem.openFile(dpath, 'r') then
  322. love.filesystem.remove(dpath)
  323. end
  324. self.imgs = self.imgs + 1
  325. end,
  326. -- @method - TestMethod:exportImg()
  327. -- @desc - exports the given imgdata to the 'output/expected/' folder, to use when
  328. -- writing new graphics tests to set the expected image output
  329. -- @NOTE - you should not leave this method in when you are finished this is
  330. -- for test writing only
  331. -- @param {table} imgdata - imgdata to save as a png
  332. -- @param {integer} imgdata - index of the png, graphic tests are run sequentially
  333. -- and each test image is numbered in order that its
  334. -- compared to, so set the number here to match
  335. -- @return {nil}
  336. exportImg = function(self, imgdata, index)
  337. local path = 'tempoutput/expected/love.test.graphics.' ..
  338. self.method .. '-' .. tostring(index) .. '.png'
  339. imgdata:encode('png', path)
  340. end,
  341. -- @method - TestMethod:skipTest()
  342. -- @desc - used to mark this test as skipped for a specific reason
  343. -- @param {string} reason - reason why method is being skipped
  344. -- @return {nil}
  345. skipTest = function(self, reason)
  346. self.skipped = true
  347. self.skipreason = reason
  348. end,
  349. -- @method - TestMethod:waitFrames()
  350. -- @desc - yields the method for x amount of frames
  351. -- @param {number} frames - no. frames to wait
  352. -- @return {nil}
  353. waitFrames = function(self, frames)
  354. for _=1,frames do coroutine.yield() end
  355. end,
  356. -- @method - TestMethod:waitSeconds()
  357. -- @desc - yields the method for x amount of seconds
  358. -- @param {number} seconds - no. seconds to wait
  359. -- @return {nil}
  360. waitSeconds = function(self, seconds)
  361. local start = love.timer.getTime()
  362. while love.timer.getTime() < start + seconds do
  363. coroutine.yield()
  364. end
  365. end,
  366. -- @method - TestMethod:isOS()
  367. -- @desc - checks for a specific OS (or list of OSs)
  368. -- @param {string/s} - each arg passed will be checked as a valid OS, as long
  369. -- as one passed the function will return true
  370. -- @return {boolean} - returns true if one of the OSs given matches actual OS
  371. isOS = function(self, ...)
  372. for os=1,select("#", ...) do
  373. if select(os, ...) == love.test.current_os then return true end
  374. end
  375. return false
  376. end,
  377. -- @method - TestMethod:isLuaVersion()
  378. -- @desc - checks for a specific Lua version (or list of versions)
  379. -- @param {number} - the minimum Lua version to check against
  380. -- @return {boolean} - returns true if the current Lua version is at least the given version
  381. isAtLeastLuaVersion = function(self, version)
  382. return love.test.lua_version >= version
  383. end,
  384. -- @method - TestMethod:isLuaJITEnabled()
  385. -- @desc - checks if LuaJIT is enabled
  386. -- @return {boolean} - returns true if LuaJIT is enabled
  387. isLuaJITEnabled = function(self)
  388. return love.test.has_lua_jit
  389. end,
  390. -- @method - TestMethod:evaluateTest()
  391. -- @desc - evaluates the results of all assertions for a final restult
  392. -- @return {nil}
  393. evaluateTest = function(self)
  394. local failure = ''
  395. local failures = 0
  396. -- check all asserts for failures, additional failures are also printed
  397. local assert_failures = {}
  398. for a=1,#self.asserts do
  399. if not self.asserts[a].passed and not self.skipped then
  400. if failure == '' then failure = self.asserts[a] end
  401. table.insert(assert_failures, self.asserts[a])
  402. failures = failures + 1
  403. end
  404. end
  405. if self.fatal ~= '' then failure = self.fatal end
  406. local passed = tostring(#self.asserts - failures)
  407. local total = '(' .. passed .. '/' .. tostring(#self.asserts) .. ')'
  408. -- skipped tests have a special log
  409. if self.skipped then
  410. self.testmodule.skipped = self.testmodule.skipped + 1
  411. love.test.totals[3] = love.test.totals[3] + 1
  412. self.result = {
  413. total = '',
  414. result = "SKIP",
  415. passed = false,
  416. message = '(0/0) - method skipped [' .. self.skipreason .. ']',
  417. failures = {}
  418. }
  419. else
  420. -- if no failure but has asserts, then passed
  421. if failure == '' and #self.asserts > 0 then
  422. self.passed = true
  423. self.testmodule.passed = self.testmodule.passed + 1
  424. love.test.totals[1] = love.test.totals[1] + 1
  425. self.result = {
  426. total = total,
  427. result = 'PASS',
  428. passed = true,
  429. message = nil,
  430. failures = {}
  431. }
  432. -- otherwise it failed
  433. else
  434. self.passed = false
  435. self.testmodule.failed = self.testmodule.failed + 1
  436. love.test.totals[2] = love.test.totals[2] + 1
  437. -- no asserts means invalid test
  438. if #self.asserts == 0 then
  439. local msg = 'no asserts defined'
  440. if self.fatal ~= '' then msg = self.fatal end
  441. self.result = {
  442. total = total,
  443. result = 'FAIL',
  444. passed = false,
  445. key = 'test',
  446. message = msg,
  447. failures = {}
  448. }
  449. -- otherwise we had failures, log the first and supply the list of
  450. -- additional failures if any for printResult()
  451. else
  452. local key = failure['key']
  453. if failure['test'] ~= nil then
  454. key = key .. ' [' .. failure['test'] .. ']'
  455. end
  456. local msg = failure['message']
  457. if self.fatal ~= '' then
  458. key = 'code'
  459. msg = self.fatal
  460. end
  461. self.result = {
  462. total = total,
  463. result = 'FAIL',
  464. passed = false,
  465. key = key,
  466. message = msg,
  467. failures = assert_failures
  468. }
  469. end
  470. end
  471. end
  472. self:printResult()
  473. end,
  474. -- @method - TestMethod:printResult()
  475. -- @desc - prints the result of the test to the console as well as appends
  476. -- the XML + HTML for the test to the testsuite output
  477. -- @return {nil}
  478. printResult = function(self)
  479. -- get total timestamp
  480. self.finish = love.timer.getTime() - self.start
  481. love.test.time = love.test.time + self.finish
  482. self.testmodule.time = self.testmodule.time + self.finish
  483. local endtime = UtilTimeFormat(love.timer.getTime() - self.start)
  484. -- get failure/skip message for output (if any)
  485. local failure = ''
  486. local output = ''
  487. if not self.passed and not self.skipped then
  488. failure = '\t\t\t<failure message="' .. self.result.key .. ' ' ..
  489. self.result.message .. '">' .. self.result.key .. ' ' .. self.result.message .. '</failure>\n'
  490. output = self.result.key .. ' ' .. self.result.message
  491. -- append failures if any to report md
  492. love.test.mdfailures = love.test.mdfailures .. '> 🔴 ' .. self.method .. ' \n' ..
  493. '> ' .. output .. ' \n\n'
  494. end
  495. if output == '' and self.skipped then
  496. failure = '\t\t\t<skipped message="' .. self.skipreason .. '" />\n'
  497. output = self.skipreason
  498. end
  499. -- append XML for the test class result
  500. self.testmodule.xml = self.testmodule.xml .. '\t\t<testcase classname="' ..
  501. self.method .. '" name="' .. self.method .. '" assertions="' .. tostring(#self.asserts) ..
  502. '" time="' .. endtime .. '">\n' ..
  503. failure .. '\t\t</testcase>\n'
  504. -- unused currently, adds a preview image for certain graphics methods to the output
  505. local preview = ''
  506. if self.testmodule.module == 'graphics' then
  507. local filename = 'love.test.graphics.' .. self.method
  508. for f=1,5 do
  509. local fstr = tostring(f)
  510. if love.filesystem.openFile('tempoutput/actual/' .. filename .. '-' .. fstr .. '.png', 'r') then
  511. preview = preview .. '<div class="preview-wrap">'
  512. preview = preview .. '<div class="preview">' .. '<img src="expected/' .. filename .. '-' .. fstr .. '.png"/><p>Expected</p></div>' ..
  513. '<div class="preview">' .. '<img src="actual/' .. filename .. '-' .. fstr .. '.png"/><p>Actual</p></div>'
  514. if love.filesystem.openFile('tempoutput/difference/' .. filename .. '-' .. fstr .. '.png', 'r') then
  515. preview = preview .. '<div class="preview">' .. '<img src="difference/' .. filename .. '-' .. fstr .. '.png"/><p>Difference</p></div>'
  516. end
  517. preview = preview .. '</div>'
  518. end
  519. end
  520. end
  521. -- append HTML for the test class result
  522. local status = '🔴'
  523. local cls = 'red'
  524. if self.passed then status = '🟢'; cls = '' end
  525. if self.skipped then status = '🟡'; cls = '' end
  526. self.testmodule.html = self.testmodule.html ..
  527. '<tr class=" ' .. cls .. '">' ..
  528. '<td>' .. status .. '</td>' ..
  529. '<td>' .. self.method .. '</td>' ..
  530. '<td>' .. endtime .. 's</td>' ..
  531. '<td>' .. output .. preview .. '</td>' ..
  532. '</tr>'
  533. -- add message if assert failed
  534. local msg = ''
  535. if self.result.message ~= nil and not self.skipped then
  536. msg = ' - ' .. self.result.key ..
  537. ' failed - (' .. self.result.message .. ')'
  538. end
  539. if self.skipped then
  540. msg = self.result.message
  541. end
  542. -- log final test result to console
  543. -- i know its hacky but its neat soz
  544. local tested = 'love.' .. self.testmodule.module .. '.' .. self.method .. '()'
  545. local matching = string.sub(self.testmodule.spacer, string.len(tested), 40)
  546. self.testmodule:log(
  547. self.testmodule.colors[self.result.result],
  548. ' ' .. tested .. matching,
  549. ' ==> ' .. self.result.result .. ' - ' .. endtime .. 's ' ..
  550. self.result.total .. msg
  551. )
  552. -- if we failed on multiple asserts, list them here - makes it easier for
  553. -- debugging new methods added that are failing multiple asserts
  554. if #self.result.failures > 1 then
  555. for f=2,#self.result.failures do
  556. local addf = self.result.failures[f]
  557. self.testmodule:log(
  558. self.testmodule.colors[self.result.result],
  559. ' ' .. tested .. matching,
  560. ' ==> ' ..
  561. addf['key'] .. ' [' .. addf['test'] .. '] failed - ' .. addf['message']
  562. )
  563. end
  564. end
  565. end
  566. }