lust.lua 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. -- lust v0.2.0 - Lua test framework
  2. -- https://github.com/bjornbytes/lust
  3. -- MIT LICENSE
  4. local lust = {}
  5. lust.level = 0
  6. lust.passes = 0
  7. lust.errors = 0
  8. lust.befores = {}
  9. lust.afters = {}
  10. local red = string.char(27) .. '[31m'
  11. local green = string.char(27) .. '[32m'
  12. local normal = string.char(27) .. '[0m'
  13. local function indent(level) return string.rep('\t', level or lust.level) end
  14. function lust.nocolor()
  15. red, green, normal = '', '', ''
  16. return lust
  17. end
  18. function lust.describe(name, fn)
  19. print(indent() .. name)
  20. lust.level = lust.level + 1
  21. fn()
  22. lust.befores[lust.level] = {}
  23. lust.afters[lust.level] = {}
  24. lust.level = lust.level - 1
  25. end
  26. function lust.it(name, fn)
  27. for level = 1, lust.level do
  28. if lust.befores[level] then
  29. for i = 1, #lust.befores[level] do
  30. lust.befores[level][i](name)
  31. end
  32. end
  33. end
  34. local success, err = pcall(fn)
  35. if success then lust.passes = lust.passes + 1
  36. else lust.errors = lust.errors + 1 end
  37. local color = success and green or red
  38. local label = success and 'PASS' or 'FAIL'
  39. print(indent() .. color .. label .. normal .. ' ' .. name)
  40. if err then
  41. print(indent(lust.level + 1) .. red .. tostring(err) .. normal)
  42. end
  43. for level = 1, lust.level do
  44. if lust.afters[level] then
  45. for i = 1, #lust.afters[level] do
  46. lust.afters[level][i](name)
  47. end
  48. end
  49. end
  50. end
  51. function lust.before(fn)
  52. lust.befores[lust.level] = lust.befores[lust.level] or {}
  53. table.insert(lust.befores[lust.level], fn)
  54. end
  55. function lust.after(fn)
  56. lust.afters[lust.level] = lust.afters[lust.level] or {}
  57. table.insert(lust.afters[lust.level], fn)
  58. end
  59. -- Assertions
  60. local function isa(v, x)
  61. if type(x) == 'string' then
  62. return type(v) == x,
  63. 'expected ' .. tostring(v) .. ' to be a ' .. x,
  64. 'expected ' .. tostring(v) .. ' to not be a ' .. x
  65. elseif type(x) == 'table' then
  66. if type(v) ~= 'table' then
  67. return false,
  68. 'expected ' .. tostring(v) .. ' to be a ' .. tostring(x),
  69. 'expected ' .. tostring(v) .. ' to not be a ' .. tostring(x)
  70. end
  71. local seen = {}
  72. local meta = v
  73. while meta and not seen[meta] do
  74. if meta == x then return true end
  75. seen[meta] = true
  76. meta = getmetatable(meta) and getmetatable(meta).__index
  77. end
  78. return false,
  79. 'expected ' .. tostring(v) .. ' to be a ' .. tostring(x),
  80. 'expected ' .. tostring(v) .. ' to not be a ' .. tostring(x)
  81. end
  82. error('invalid type ' .. tostring(x))
  83. end
  84. local function has(t, x)
  85. for k, v in pairs(t) do
  86. if v == x then return true end
  87. end
  88. return false
  89. end
  90. local function eq(t1, t2, eps)
  91. if type(t1) ~= type(t2) then return false end
  92. if type(t1) == 'number' then return math.abs(t1 - t2) <= (eps or 0) end
  93. if type(t1) ~= 'table' then return t1 == t2 end
  94. for k, _ in pairs(t1) do
  95. if not eq(t1[k], t2[k], eps) then return false end
  96. end
  97. for k, _ in pairs(t2) do
  98. if not eq(t2[k], t1[k], eps) then return false end
  99. end
  100. return true
  101. end
  102. local function stringify(t)
  103. if type(t) == 'string' then return "'" .. tostring(t) .. "'" end
  104. if type(t) ~= 'table' or getmetatable(t) and getmetatable(t).__tostring then return tostring(t) end
  105. local strings = {}
  106. for i, v in ipairs(t) do
  107. strings[#strings + 1] = stringify(v)
  108. end
  109. for k, v in pairs(t) do
  110. if type(k) ~= 'number' or k > #t or k < 1 then
  111. strings[#strings + 1] = ('[%s] = %s'):format(stringify(k), stringify(v))
  112. end
  113. end
  114. return '{ ' .. table.concat(strings, ', ') .. ' }'
  115. end
  116. local paths = {
  117. [''] = { 'to', 'to_not' },
  118. to = { 'have', 'equal', 'be', 'exist', 'fail', 'match' },
  119. to_not = { 'have', 'equal', 'be', 'exist', 'fail', 'match', chain = function(a) a.negate = not a.negate end },
  120. a = { test = isa },
  121. an = { test = isa },
  122. be = { 'a', 'an', 'truthy',
  123. test = function(v, x)
  124. return v == x,
  125. 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to be the same',
  126. 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to not be the same'
  127. end
  128. },
  129. exist = {
  130. test = function(v)
  131. return v ~= nil,
  132. 'expected ' .. tostring(v) .. ' to exist',
  133. 'expected ' .. tostring(v) .. ' to not exist'
  134. end
  135. },
  136. truthy = {
  137. test = function(v)
  138. return v,
  139. 'expected ' .. tostring(v) .. ' to be truthy',
  140. 'expected ' .. tostring(v) .. ' to not be truthy'
  141. end
  142. },
  143. equal = {
  144. test = function(v, x, eps)
  145. local comparison = ''
  146. local equal = eq(v, x, eps)
  147. if not equal and (type(v) == 'table' or type(x) == 'table') then
  148. comparison = comparison .. '\n' .. indent(lust.level + 1) .. 'LHS: ' .. stringify(v)
  149. comparison = comparison .. '\n' .. indent(lust.level + 1) .. 'RHS: ' .. stringify(x)
  150. end
  151. return equal,
  152. 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to be equal' .. comparison,
  153. 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to not be equal'
  154. end
  155. },
  156. have = {
  157. test = function(v, x)
  158. if type(v) ~= 'table' then
  159. error('expected ' .. tostring(v) .. ' to be a table')
  160. end
  161. return has(v, x),
  162. 'expected ' .. tostring(v) .. ' to contain ' .. tostring(x),
  163. 'expected ' .. tostring(v) .. ' to not contain ' .. tostring(x)
  164. end
  165. },
  166. fail = { 'with',
  167. test = function(v)
  168. return not pcall(v),
  169. 'expected ' .. tostring(v) .. ' to fail',
  170. 'expected ' .. tostring(v) .. ' to not fail'
  171. end
  172. },
  173. with = {
  174. test = function(v, pattern)
  175. local ok, message = pcall(v)
  176. return not ok and message:match(pattern),
  177. 'expected ' .. tostring(v) .. ' to fail with error matching "' .. pattern .. '"',
  178. 'expected ' .. tostring(v) .. ' to not fail with error matching "' .. pattern .. '"'
  179. end
  180. },
  181. match = {
  182. test = function(v, p)
  183. if type(v) ~= 'string' then v = tostring(v) end
  184. local result = string.find(v, p)
  185. return result ~= nil,
  186. 'expected ' .. v .. ' to match pattern [[' .. p .. ']]',
  187. 'expected ' .. v .. ' to not match pattern [[' .. p .. ']]'
  188. end
  189. }
  190. }
  191. function lust.expect(v)
  192. local assertion = {}
  193. assertion.val = v
  194. assertion.action = ''
  195. assertion.negate = false
  196. setmetatable(assertion, {
  197. __index = function(t, k)
  198. if has(paths[rawget(t, 'action')], k) then
  199. rawset(t, 'action', k)
  200. local chain = paths[rawget(t, 'action')].chain
  201. if chain then chain(t) end
  202. return t
  203. end
  204. return rawget(t, k)
  205. end,
  206. __call = function(t, ...)
  207. if paths[t.action].test then
  208. local res, err, nerr = paths[t.action].test(t.val, ...)
  209. if assertion.negate then
  210. res = not res
  211. err = nerr or err
  212. end
  213. if not res then
  214. error(err or 'unknown failure', 2)
  215. end
  216. end
  217. end
  218. })
  219. return assertion
  220. end
  221. function lust.spy(target, name, run)
  222. local spy = {}
  223. local subject
  224. local function capture(...)
  225. table.insert(spy, {...})
  226. return subject(...)
  227. end
  228. if type(target) == 'table' then
  229. subject = target[name]
  230. target[name] = capture
  231. else
  232. run = name
  233. subject = target or function() end
  234. end
  235. setmetatable(spy, {__call = function(_, ...) return capture(...) end})
  236. if run then run() end
  237. return spy
  238. end
  239. lust.test = lust.it
  240. lust.paths = paths
  241. return lust