------------------------------------------------------------------------
--- Assertors
--- New assert object. Behaves like original `assert` when called, and
--- comes with many other tests.
local assert = setmetatable({}, {
__call = _G.assert, -- use global assert when called.
})
--- Special table allowing to use `assert.is.truthy` instead of
--- `assert.is_truthy.`
assert.is = setmetatable({}, {
__index = function (t, k)
return assert['is_' .. k]
end
})
--- Special table allowing to use `assert.are.same` instead of
--- `assert.are_same.`
assert.are = setmetatable({}, {
__index = function (t, k)
return assert['are_' .. k]
end
})
--- Special table allowing to use `assert.error.matches` instead of
--- `assert.error_matches.`
assert.error = setmetatable({}, {
__index = function (t, k)
return assert['error_' .. k]
end
})
--- Create a new assertion function.
local function make_assertion (error_message, callback)
return function (...)
local assertion_holds, info = callback(...)
-- Calling the assertion function produced an error, report it.
if assertion_holds then
return
end
-- Assertion failed, format and throw the error message
local success, message
if type(error_message) == 'function' then
success, message = pcall(error_message, info, ...)
elseif type(error_message) == 'string' then
success, message = pcall(string.format, error_message, ...)
else
success, message = false, error_message
end
if not success then
error('assertion failed, but error could not be formatted:\n' ..
tostring(message), 1)
end
error('\n' .. message or 'assertion failed!', 2)
end
end
--- Value is truthy
assert.is_truthy = make_assertion(
"expected a truthy value, got %s",
function (x)
return x ~= false and x ~= nil
end
)
--- Value is falsy
assert.is_falsy = make_assertion(
"expected a falsy value, got %s",
function (x)
return not x
end
)
--- Value is true
assert.is_true = make_assertion(
"expected true, got %s",
function (x)
return x == true
end
)
--- Value is false
assert.is_false = make_assertion(
"expected false, got %s",
function (x)
return x == false
end
)
--- Value is nil
assert.is_nil = make_assertion(
"expected nil, got %s",
function (x)
return x == nil
end
)
--- Values are equal
assert.are_equal = make_assertion(
"expected values to be equal, got '%s' and '%s'",
function (x, y)
return x == y
end
)
local function cycle_aware_compare(t1, t2, cache)
if cache[t1] and cache[t1][t2] then return true end
local ty1 = type(t1)
local ty2 = type(t2)
-- if t1 == t2 then return true end
if ty1 ~= ty2 then return false end
if ty1 ~= 'table' then return t1 == t2 end
-- Check tables have the same set of keys
for k1 in pairs(t1) do
if t2[k1] == nil then return false end
end
for k2 in pairs(t2) do
if t1[k2] == nil then return false end
end
-- cache comparison result
cache[t1] = cache[t1] or {}
cache[t1][t2] = true
for k1, v1 in pairs(t1) do
local v2 = t2[k1]
if not cycle_aware_compare(v1, v2, cache) then return false end
end
return true
end
--- Check if tables are the same
assert.are_same = make_assertion(
'expected same values, got %s and %s',
function (x, y)
return cycle_aware_compare(x, y, {})
end
)
--- Checks that a error is raised and that the error satisfies the given
-- assertion.
assert.error_satisfies = make_assertion(
function (actual)
return ('assertion did not hold for error object:%s')
:format(actual)
end,
function (fn, assertion)
local success, err = pcall(fn)
if success then
return false
end
return pcall(assertion, err)
end
)
--- Checks that a error is raised and that the error equals an expected value.
assert.error_equals = make_assertion(
function (actual, fun, expected)
return ('expected error to equal %s, got: %s'):format(expected, actual)
end,
function (fn, expected)
local success, err = pcall(fn)
return not success and expected == err, err
end
)
--- Checks that a error is raised and that the message matches the given
--- pattern.
assert.error_matches = make_assertion(
function (actual, fun, expected)
return ('expected error to match pattern \'%s\' but got: %s')
:format(expected, actual)
end,
function (fn, pattern)
local success, msg = pcall(fn)
if success then
return false
end
return tostring(msg):match(pattern), msg
end
)
------------------------------------------------------------------------
local ok = true
local function test_success (name)
return {
name = name,
result = ok,
}
end
local function test_failure (name, err_msg)
return {
name = name,
result = err_msg,
}
end
------------------------------------------------------------------------
-- Test definitions
local function test_case (name, callback)
if callback == nil then
-- let's try currying
return function (callback_)
return callback_ and test_case(name, callback_) or error('no test')
end
end
local success, err_msg = pcall(callback)
return success
and test_success(name)
or test_failure(name, err_msg)
end
local function test_group (name, tests)
if tests == nil then
-- let's try to curry
return function (tests_)
return tests_ and test_group(name, tests_) or error('no tests')
end
end
return {
name = name,
result = tests,
}
end
------------------------------------------------------------------------
-- Property tests
local maxshrinks = 10
local function doshrink(property, value, shrink)
local shrunken = value
local numshrinks = 0
while numshrinks < maxshrinks do
local candidates = shrink(shrunken)
-- abort if shrinking failed, e.g., because the value could not be
-- peeked.
if candidates == nil then
break
end
for i, cand in ipairs(candidates) do
if not property(cand) then
numshrinks = numshrinks + 1
shrunken = cand
goto continue
end
end
-- no successful shrink happened, we are done
break
::continue::
end
return shrunken, numshrinks
end
local function forall (arbitrary, property)
if type(arbitrary) ~= 'table' then
local msg =string.format(
'Unknown or invalid arbitrary generator: %s',
tostring(arbitrary)
)
error(msg, 2)
end
local generator = arbitrary.generator
local shrink = arbitrary.shrink
return function ()
local i = 0
for value in generator() do
i = i + 1
if not property(value) then
local shrunken, steps = doshrink(property, value, shrink)
error(('falsifiable after %d steps; %d shrinking steps; ' ..
'property fails for %s')
:format(i, steps, shrunken))
end
end
return string.format("%d tests succeeded", i)
end
end
return {
assert = assert,
forall = forall,
ok = ok,
test_case = test_case,
test_group = test_group
}