lust.lua 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. -- lust - Lua test framework
  2. -- https://github.com/bjornbytes/lust
  3. -- License - MIT, see LICENSE for details.
  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.describe(name, fn)
  15. print(indent() .. name)
  16. lust.level = lust.level + 1
  17. fn()
  18. lust.befores[lust.level] = {}
  19. lust.afters[lust.level] = {}
  20. lust.level = lust.level - 1
  21. end
  22. function lust.it(name, fn)
  23. for level = 1, lust.level do
  24. if lust.befores[level] then
  25. for i = 1, #lust.befores[level] do
  26. lust.befores[level][i](name)
  27. end
  28. end
  29. end
  30. local success, err = pcall(fn)
  31. if success then lust.passes = lust.passes + 1
  32. else lust.errors = lust.errors + 1 end
  33. local color = success and green or red
  34. local label = success and 'PASS' or 'FAIL'
  35. print(indent() .. color .. label .. normal .. ' ' .. name)
  36. if err then
  37. print(indent(lust.level + 1) .. red .. err .. normal)
  38. end
  39. for level = 1, lust.level do
  40. if lust.afters[level] then
  41. for i = 1, #lust.afters[level] do
  42. lust.afters[level][i](name)
  43. end
  44. end
  45. end
  46. end
  47. function lust.before(fn)
  48. lust.befores[lust.level] = lust.befores[lust.level] or {}
  49. table.insert(lust.befores[lust.level], fn)
  50. end
  51. function lust.after(fn)
  52. lust.afters[lust.level] = lust.afters[lust.level] or {}
  53. table.insert(lust.afters[lust.level], fn)
  54. end
  55. -- Assertions
  56. local function isa(v, x)
  57. if type(x) == 'string' then return type(v) == x, tostring(v) .. ' is not a ' .. x
  58. elseif type(x) == 'table' then
  59. if type(v) ~= 'table' then return false, tostring(v) .. ' is not a ' .. tostring(x) end
  60. local seen = {}
  61. local meta = v
  62. while meta and not seen[meta] do
  63. if meta == x then return true end
  64. seen[meta] = true
  65. meta = getmetatable(meta) and getmetatable(meta).__index
  66. end
  67. return false, tostring(v) .. ' is not a ' .. tostring(x)
  68. end
  69. return false, 'invalid type ' .. tostring(x)
  70. end
  71. local function has(t, x)
  72. for k, v in pairs(t) do
  73. if v == x then return true end
  74. end
  75. return false
  76. end
  77. local function strict_eq(t1, t2)
  78. if type(t1) ~= type(t2) then return false end
  79. if type(t1) ~= 'table' then return t1 == t2 end
  80. if #t1 ~= #t2 then return false end
  81. for k, _ in pairs(t1) do
  82. if not strict_eq(t1[k], t2[k]) then return false end
  83. end
  84. for k, _ in pairs(t2) do
  85. if not strict_eq(t2[k], t1[k]) then return false end
  86. end
  87. return true
  88. end
  89. local paths = {
  90. [''] = {'to', 'to_not'},
  91. to = {'have', 'equal', 'be', 'exist', 'fail'},
  92. to_not = {'have', 'equal', 'be', 'exist', 'fail', chain = function(a) a.negate = not a.negate end},
  93. be = {'a', 'an', 'truthy', 'falsy', f = function(v, x)
  94. return v == x, tostring(v) .. ' and ' .. tostring(x) .. ' are not equal'
  95. end},
  96. a = {f = isa},
  97. an = {f = isa},
  98. exist = {f = function(v) return v ~= nil, tostring(v) .. ' is nil' end},
  99. truthy = {f = function(v) return v, tostring(v) .. ' is not truthy' end},
  100. falsy = {f = function(v) return not v, tostring(v) .. ' is not falsy' end},
  101. equal = {f = function(v, x) return strict_eq(v, x), tostring(v) .. ' and ' .. tostring(x) .. ' are not strictly equal' end},
  102. have = {
  103. f = function(v, x)
  104. if type(v) ~= 'table' then return false, 'table "' .. tostring(v) .. '" is not a table' end
  105. return has(v, x), 'table "' .. tostring(v) .. '" does not have ' .. tostring(x)
  106. end
  107. },
  108. fail = {'with', f = function(v) return not pcall(v), tostring(v) .. ' did not fail' end},
  109. with = {f = function(v, x) local _, e = pcall(v) return e and e:find(x), tostring(v) .. ' did not fail with ' .. tostring(x) end}
  110. }
  111. function lust.expect(v)
  112. local assertion = {}
  113. assertion.val = v
  114. assertion.action = ''
  115. assertion.negate = false
  116. setmetatable(assertion, {
  117. __index = function(t, k)
  118. if has(paths[rawget(t, 'action')], k) then
  119. rawset(t, 'action', k)
  120. local chain = paths[rawget(t, 'action')].chain
  121. if chain then chain(t) end
  122. return t
  123. end
  124. return rawget(t, k)
  125. end,
  126. __call = function(t, ...)
  127. if paths[t.action].f then
  128. local res, err = paths[t.action].f(t.val, ...)
  129. if assertion.negate then res = not res end
  130. if not res then
  131. error(err or 'unknown failure', 2)
  132. end
  133. end
  134. end
  135. })
  136. return assertion
  137. end
  138. function lust.spy(target, name, run)
  139. local spy = {}
  140. local subject
  141. local function capture(...)
  142. table.insert(spy, {...})
  143. return subject(...)
  144. end
  145. if type(target) == 'table' then
  146. subject = target[name]
  147. target[name] = capture
  148. else
  149. run = name
  150. subject = target or function() end
  151. end
  152. setmetatable(spy, {__call = function(_, ...) return capture(...) end})
  153. if run then run() end
  154. return spy
  155. end
  156. lust.test = lust.it
  157. lust.paths = paths
  158. return lust