lust.lua 4.5 KB

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