2019-03-02 02:09:14 +00:00
|
|
|
--- A very basic test framework for ComputerCraft
|
|
|
|
--
|
|
|
|
-- Like Busted (http://olivinelabs.com/busted/), but more memorable.
|
|
|
|
--
|
|
|
|
-- @usage
|
|
|
|
-- describe("something to test", function()
|
|
|
|
-- it("some property", function()
|
|
|
|
-- expect(some_function()):equals("What it should equal")
|
|
|
|
-- end)
|
|
|
|
-- end)
|
|
|
|
|
|
|
|
--- Assert an argument to the given function has the specified type.
|
|
|
|
--
|
2019-12-07 10:33:47 +00:00
|
|
|
-- @tparam string func The function's name
|
|
|
|
-- @tparam int idx The argument index to this function
|
2020-01-23 15:11:50 +00:00
|
|
|
-- @tparam string ty The type this argument should have. May be 'value' for
|
2019-12-07 10:33:47 +00:00
|
|
|
-- any non-nil value.
|
|
|
|
-- @param val val The value to check
|
|
|
|
-- @throws If this value doesn't match the expected type.
|
|
|
|
local function check(func, idx, ty, val)
|
2019-03-02 02:09:14 +00:00
|
|
|
if ty == 'value' then
|
|
|
|
if val == nil then
|
2019-12-07 10:33:47 +00:00
|
|
|
error(('%s: bad argument #%d (got nil)'):format(func, idx), 3)
|
2019-03-02 02:09:14 +00:00
|
|
|
end
|
|
|
|
elseif type(val) ~= ty then
|
2019-12-07 10:33:47 +00:00
|
|
|
return error(('%s: bad argument #%d (expected %s, got %s)'):format(func, idx, ty, type(val)), 3)
|
2019-03-02 02:09:14 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-06-29 14:37:41 +00:00
|
|
|
--- A stub - wraps a value within a a table,
|
|
|
|
local stub_mt = {}
|
|
|
|
stub_mt.__index = stub_mt
|
|
|
|
|
|
|
|
--- Revert this stub, restoring the previous value.
|
|
|
|
--
|
|
|
|
-- Note, a stub can only be reverted once.
|
|
|
|
function stub_mt:revert()
|
|
|
|
if not self.active then return end
|
|
|
|
|
|
|
|
self.active = false
|
|
|
|
rawset(self.stubbed_in, self.key, self.original)
|
|
|
|
end
|
|
|
|
|
2019-05-30 18:36:28 +00:00
|
|
|
local active_stubs = {}
|
|
|
|
|
2019-07-08 08:24:05 +00:00
|
|
|
local function default_stub() end
|
|
|
|
|
2019-06-29 14:37:41 +00:00
|
|
|
--- Stub a table entry with a new value.
|
2019-05-30 18:36:28 +00:00
|
|
|
--
|
2019-06-29 14:37:41 +00:00
|
|
|
-- @tparam table
|
|
|
|
-- @tparam string key The variable to stub
|
2019-07-08 08:24:05 +00:00
|
|
|
-- @param[opt] value The value to stub it with. If this is a function, one can
|
|
|
|
-- use the various stub expectation methods to determine what it was called
|
|
|
|
-- with. Defaults to an empty function - pass @{nil} in explicitly to set the
|
|
|
|
-- value to nil.
|
2019-06-29 14:37:41 +00:00
|
|
|
-- @treturn Stub The resulting stub
|
2019-07-08 08:24:05 +00:00
|
|
|
local function stub(tbl, key, ...)
|
2019-06-03 19:33:09 +00:00
|
|
|
check('stub', 1, 'table', tbl)
|
2019-06-29 14:37:41 +00:00
|
|
|
check('stub', 2, 'string', key)
|
|
|
|
|
|
|
|
local stub = setmetatable({
|
|
|
|
active = true,
|
|
|
|
stubbed_in = tbl,
|
|
|
|
key = key,
|
|
|
|
original = rawget(tbl, key),
|
|
|
|
}, stub_mt)
|
|
|
|
|
2019-07-08 08:24:05 +00:00
|
|
|
local value = ...
|
|
|
|
if select('#', ...) == 0 then value = default_stub end
|
2019-06-29 14:37:41 +00:00
|
|
|
if type(value) == "function" then
|
|
|
|
local arguments, delegate = {}, value
|
|
|
|
stub.arguments = arguments
|
|
|
|
value = function(...)
|
|
|
|
arguments[#arguments + 1] = table.pack(...)
|
2019-06-29 14:44:53 +00:00
|
|
|
return delegate(...)
|
2019-06-29 14:37:41 +00:00
|
|
|
end
|
|
|
|
end
|
2019-06-03 19:33:09 +00:00
|
|
|
|
2019-06-29 14:37:41 +00:00
|
|
|
table.insert(active_stubs, stub)
|
|
|
|
rawset(tbl, key, value)
|
|
|
|
return stub
|
2019-05-30 18:36:28 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
--- Capture the current global state of the computer
|
|
|
|
local function push_state()
|
|
|
|
local stubs = active_stubs
|
|
|
|
active_stubs = {}
|
|
|
|
return {
|
|
|
|
term = term.current(),
|
|
|
|
input = io.input(),
|
|
|
|
output = io.output(),
|
2019-06-14 07:15:12 +00:00
|
|
|
dir = shell.dir(),
|
|
|
|
path = shell.path(),
|
2019-07-08 08:24:05 +00:00
|
|
|
aliases = shell.aliases(),
|
2019-05-30 18:36:28 +00:00
|
|
|
stubs = stubs,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Restore the global state of the computer to a previous version
|
|
|
|
local function pop_state(state)
|
2019-06-29 14:37:41 +00:00
|
|
|
for i = #active_stubs, 1, -1 do active_stubs[i]:revert() end
|
2019-06-01 08:23:18 +00:00
|
|
|
|
2019-05-30 18:36:28 +00:00
|
|
|
active_stubs = state.stubs
|
|
|
|
|
|
|
|
term.redirect(state.term)
|
|
|
|
io.input(state.input)
|
|
|
|
io.output(state.output)
|
2019-06-14 07:15:12 +00:00
|
|
|
shell.setDir(state.dir)
|
|
|
|
shell.setPath(state.path)
|
2019-07-08 08:24:05 +00:00
|
|
|
|
|
|
|
local aliases = shell.aliases()
|
|
|
|
for k in pairs(aliases) do
|
|
|
|
if not state.aliases[k] then shell.clearAlias(k) end
|
|
|
|
end
|
|
|
|
for k, v in pairs(state.aliases) do
|
|
|
|
if aliases[k] ~= v then shell.setAlias(k, v) end
|
|
|
|
end
|
2019-05-30 18:36:28 +00:00
|
|
|
end
|
|
|
|
|
2019-03-02 02:09:14 +00:00
|
|
|
local error_mt = { __tostring = function(self) return self.message end }
|
|
|
|
|
|
|
|
--- Attempt to execute the provided function, gathering a stack trace when it
|
|
|
|
-- errors.
|
|
|
|
--
|
2019-12-07 10:33:47 +00:00
|
|
|
-- @tparam function() fn The function to run
|
2019-03-02 02:09:14 +00:00
|
|
|
-- @return[1] true
|
|
|
|
-- @return[2] false
|
|
|
|
-- @return[2] The error object
|
|
|
|
local function try(fn)
|
|
|
|
if not debug or not debug.traceback then
|
|
|
|
local ok, err = pcall(fn)
|
|
|
|
if ok or getmetatable(err) == error_mt then
|
|
|
|
return ok, err
|
|
|
|
else
|
|
|
|
return ok, setmetatable({ message = tostring(err) }, error_mt)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local ok, err = xpcall(fn, function(err)
|
2019-05-25 17:04:56 +00:00
|
|
|
return { message = err, trace = debug.traceback(nil, 2) }
|
2019-03-02 02:09:14 +00:00
|
|
|
end)
|
|
|
|
|
2019-05-25 17:04:56 +00:00
|
|
|
-- If we succeeded, propagate it
|
2019-03-02 02:09:14 +00:00
|
|
|
if ok then return ok, err end
|
2019-05-25 17:04:56 +00:00
|
|
|
|
|
|
|
-- Error handling failed for some reason - just return a simpler error
|
2019-03-02 02:09:14 +00:00
|
|
|
if type(err) ~= "table" then
|
2019-05-25 17:04:56 +00:00
|
|
|
return ok, setmetatable({ message = tostring(err) }, error_mt)
|
2019-03-02 02:09:14 +00:00
|
|
|
end
|
|
|
|
|
2019-05-25 17:04:56 +00:00
|
|
|
-- Find the common substring the errors' trace and the current one. Then
|
|
|
|
-- eliminate it.
|
2019-03-02 02:09:14 +00:00
|
|
|
local trace = debug.traceback()
|
|
|
|
for i = 1, #trace do
|
|
|
|
if trace:sub(-i) ~= err.trace:sub(-i) then
|
|
|
|
err.trace = err.trace:sub(1, -i)
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-05-25 17:04:56 +00:00
|
|
|
-- If we've received a rethrown error, copy
|
|
|
|
if getmetatable(err.message) == error_mt then
|
|
|
|
for k, v in pairs(err.message) do err[k] = v end
|
|
|
|
return ok, err
|
|
|
|
end
|
|
|
|
|
2019-03-02 02:09:14 +00:00
|
|
|
return ok, setmetatable(err, error_mt)
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Fail a test with the given message
|
|
|
|
--
|
|
|
|
-- @tparam string message The message to fail with
|
2019-12-07 10:33:47 +00:00
|
|
|
-- @throws An error with the given message
|
2019-03-02 02:09:14 +00:00
|
|
|
local function fail(message)
|
|
|
|
check('fail', 1, 'string', message)
|
|
|
|
error(setmetatable({ message = message, fail = true }, error_mt))
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Format an object in order to make it more readable
|
|
|
|
--
|
|
|
|
-- @param value The value to format
|
|
|
|
-- @treturn string The formatted value
|
|
|
|
local function format(value)
|
|
|
|
-- TODO: Look into something like mbs's pretty printer.
|
|
|
|
local ok, res = pcall(textutils.serialise, value)
|
|
|
|
if ok then return res else return tostring(value) end
|
|
|
|
end
|
|
|
|
|
|
|
|
local expect_mt = {}
|
|
|
|
expect_mt.__index = expect_mt
|
|
|
|
|
|
|
|
--- Assert that this expectation has the provided value
|
|
|
|
--
|
|
|
|
-- @param value The value to require this expectation to be equal to
|
2019-12-07 10:33:47 +00:00
|
|
|
-- @throws If the values are not equal
|
2019-03-02 02:09:14 +00:00
|
|
|
function expect_mt:equals(value)
|
|
|
|
if value ~= self.value then
|
|
|
|
fail(("Expected %s\n but got %s"):format(format(value), format(self.value)))
|
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
expect_mt.equal = expect_mt.equals
|
|
|
|
expect_mt.eq = expect_mt.equals
|
|
|
|
|
|
|
|
--- Assert that this expectation does not equal the provided value
|
|
|
|
--
|
|
|
|
-- @param value The value to require this expectation to not be equal to
|
2019-12-07 10:33:47 +00:00
|
|
|
-- @throws If the values are equal
|
2019-03-02 02:09:14 +00:00
|
|
|
function expect_mt:not_equals(value)
|
|
|
|
if value == self.value then
|
|
|
|
fail(("Expected any value but %s"):format(format(value)))
|
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
expect_mt.not_equal = expect_mt.not_equals
|
|
|
|
expect_mt.ne = expect_mt.not_equals
|
|
|
|
|
|
|
|
--- Assert that this expectation has something of the provided type
|
|
|
|
--
|
|
|
|
-- @tparam string exp_type The type to require this expectation to have
|
2019-12-07 10:33:47 +00:00
|
|
|
-- @throws If it does not have that thpe
|
2019-03-02 02:09:14 +00:00
|
|
|
function expect_mt:type(exp_type)
|
|
|
|
local actual_type = type(self.value)
|
|
|
|
if exp_type ~= actual_type then
|
2020-04-16 09:48:26 +00:00
|
|
|
fail(("Expected value of type %s\nbut got %s"):format(exp_type, actual_type))
|
2019-03-02 02:09:14 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
2019-03-10 12:24:55 +00:00
|
|
|
local function matches(eq, exact, left, right)
|
2019-03-02 02:09:14 +00:00
|
|
|
if left == right then return true end
|
|
|
|
|
|
|
|
local ty = type(left)
|
|
|
|
if ty ~= type(right) or ty ~= "table" then return false end
|
|
|
|
|
|
|
|
-- If we've already explored/are exploring the left and right then return
|
|
|
|
if eq[left] and eq[left][right] then return true end
|
2020-04-18 09:09:40 +00:00
|
|
|
if not eq[left] then eq[left] = { [right] = true } else eq[left][right] = true end
|
|
|
|
if not eq[right] then eq[right] = { [left] = true } else eq[right][left] = true end
|
2019-03-02 02:09:14 +00:00
|
|
|
|
|
|
|
-- Verify all pairs in left are equal to those in right
|
|
|
|
for k, v in pairs(left) do
|
2019-03-10 12:24:55 +00:00
|
|
|
if not matches(eq, exact, v, right[k]) then return false end
|
2019-03-02 02:09:14 +00:00
|
|
|
end
|
|
|
|
|
2019-03-10 12:24:55 +00:00
|
|
|
if exact then
|
|
|
|
-- And verify all pairs in right are present in left
|
|
|
|
for k in pairs(right) do
|
|
|
|
if left[k] == nil then return false end
|
|
|
|
end
|
2019-03-02 02:09:14 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
2019-06-29 14:37:41 +00:00
|
|
|
local function pairwise_equal(left, right)
|
|
|
|
if left.n ~= right.n then return false end
|
|
|
|
|
|
|
|
for i = 1, left.n do
|
|
|
|
if left[i] ~= right[i] then return false end
|
|
|
|
end
|
|
|
|
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
2019-03-02 02:09:14 +00:00
|
|
|
--- Assert that this expectation is structurally equivalent to
|
|
|
|
-- the provided object.
|
|
|
|
--
|
|
|
|
-- @param value The value to check for structural equivalence
|
2019-12-07 10:33:47 +00:00
|
|
|
-- @throws If they are not equivalent
|
2019-03-02 02:09:14 +00:00
|
|
|
function expect_mt:same(value)
|
2019-03-10 12:24:55 +00:00
|
|
|
if not matches({}, true, self.value, value) then
|
2020-04-16 09:48:26 +00:00
|
|
|
fail(("Expected %s\nbut got %s"):format(format(value), format(self.value)))
|
2019-03-02 02:09:14 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
2019-03-10 12:24:55 +00:00
|
|
|
--- Assert that this expectation contains all fields mentioned
|
|
|
|
-- in the provided object.
|
|
|
|
--
|
|
|
|
-- @param value The value to check against
|
2019-12-07 10:33:47 +00:00
|
|
|
-- @throws If this does not match the provided value
|
2019-03-10 12:24:55 +00:00
|
|
|
function expect_mt:matches(value)
|
|
|
|
if not matches({}, false, value, self.value) then
|
|
|
|
fail(("Expected %s\nto match %s"):format(format(self.value), format(value)))
|
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
2019-06-29 14:37:41 +00:00
|
|
|
--- Assert that this stub was called a specific number of times.
|
|
|
|
--
|
|
|
|
-- @tparam[opt] number The exact number of times the function must be called.
|
|
|
|
-- If not given just require the function to be called at least once.
|
2019-12-07 10:33:47 +00:00
|
|
|
-- @throws If this function was not called the expected number of times.
|
2019-06-29 14:37:41 +00:00
|
|
|
function expect_mt:called(times)
|
|
|
|
if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then
|
|
|
|
fail(("Expected stubbed function, got %s"):format(type(self.value)))
|
|
|
|
end
|
|
|
|
|
|
|
|
local called = #self.value.arguments
|
|
|
|
|
|
|
|
if times == nil then
|
|
|
|
if called == 0 then
|
|
|
|
fail("Expected stub to be called\nbut it was not.")
|
|
|
|
end
|
|
|
|
else
|
|
|
|
check('stub', 1, 'number', times)
|
|
|
|
if called ~= times then
|
|
|
|
fail(("Expected stub to be called %d times\nbut was called %d times."):format(times, called))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
2019-07-08 08:24:05 +00:00
|
|
|
local function called_with_check(eq, self, ...)
|
2019-06-29 14:37:41 +00:00
|
|
|
if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then
|
|
|
|
fail(("Expected stubbed function, got %s"):format(type(self.value)))
|
|
|
|
end
|
|
|
|
|
|
|
|
local exp_args = table.pack(...)
|
|
|
|
local actual_args = self.value.arguments
|
|
|
|
for i = 1, #actual_args do
|
2019-07-08 08:24:05 +00:00
|
|
|
if eq(actual_args[i], exp_args) then return self end
|
2019-06-29 14:37:41 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
local head = ("Expected stub to be called with %s\nbut was"):format(format(exp_args))
|
|
|
|
if #actual_args == 0 then
|
|
|
|
fail(head .. " not called at all")
|
|
|
|
elseif #actual_args == 1 then
|
|
|
|
fail(("%s called with %s."):format(head, format(actual_args[1])))
|
|
|
|
else
|
|
|
|
local lines = { head .. " called with:" }
|
|
|
|
for i = 1, #actual_args do lines[i + 1] = " - " .. format(actual_args[i]) end
|
|
|
|
|
|
|
|
fail(table.concat(lines, "\n"))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-07-08 08:24:05 +00:00
|
|
|
--- Assert that this stub was called with a set of arguments
|
|
|
|
--
|
|
|
|
-- Arguments are compared using exact equality.
|
|
|
|
function expect_mt:called_with(...)
|
|
|
|
return called_with_check(pairwise_equal, self, ...)
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Assert that this stub was called with a set of arguments
|
|
|
|
--
|
|
|
|
-- Arguments are compared using matching.
|
|
|
|
function expect_mt:called_with_matching(...)
|
|
|
|
return called_with_check(matches, self, ...)
|
|
|
|
end
|
|
|
|
|
2020-04-16 09:48:26 +00:00
|
|
|
--- Assert that this expectation matches a Lua pattern
|
|
|
|
--
|
|
|
|
-- @tparam string pattern The pattern to match against
|
|
|
|
-- @throws If it does not match this pattern.
|
|
|
|
function expect_mt:str_match(pattern)
|
|
|
|
local actual_type = type(self.value)
|
|
|
|
if actual_type ~= "string" then
|
|
|
|
fail(("Expected value of type string\nbut got %s"):format(actual_type))
|
|
|
|
end
|
|
|
|
if not self.value:find(pattern) then
|
|
|
|
fail(("Expected %q\n to match pattern %q"):format(self.value, pattern))
|
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
2020-04-10 09:27:53 +00:00
|
|
|
local expect = {}
|
|
|
|
setmetatable(expect, expect)
|
|
|
|
|
|
|
|
--- Construct an expectation on the error message calling this function
|
|
|
|
-- produces
|
|
|
|
--
|
|
|
|
-- @tparam fun The function to call
|
|
|
|
-- @param ... The function arguments
|
|
|
|
-- @return The new expectation
|
|
|
|
function expect.error(fun, ...)
|
|
|
|
local ok, res = pcall(fun, ...) local _, line = pcall(error, "", 2)
|
|
|
|
if ok then fail("expected function to error") end
|
|
|
|
if res:sub(1, #line) == line then
|
|
|
|
res = res:sub(#line + 1)
|
|
|
|
elseif res:sub(1, 7) == "pcall: " then
|
|
|
|
res = res:sub(8)
|
|
|
|
end
|
|
|
|
return setmetatable({ value = res }, expect_mt)
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Construct a new expectation from the provided value
|
|
|
|
--
|
|
|
|
-- @param value The value to apply assertions to
|
|
|
|
-- @return The new expectation
|
|
|
|
function expect:__call(value)
|
|
|
|
return setmetatable({ value = value }, expect_mt)
|
|
|
|
end
|
2019-03-02 02:09:14 +00:00
|
|
|
|
|
|
|
--- The stack of "describe"s.
|
|
|
|
local test_stack = { n = 0 }
|
|
|
|
|
|
|
|
--- Whether we're now running tests, and so cannot run any more.
|
|
|
|
local tests_locked = false
|
|
|
|
|
|
|
|
--- The list of tests that we'll run
|
2020-04-10 09:27:53 +00:00
|
|
|
local test_list = {}
|
|
|
|
local test_map, test_count = {}, 0
|
2019-03-02 02:09:14 +00:00
|
|
|
|
|
|
|
--- Add a new test to our queue.
|
|
|
|
--
|
|
|
|
-- @param test The descriptor of this test
|
|
|
|
local function do_test(test)
|
|
|
|
-- Set the name if it doesn't already exist
|
|
|
|
if not test.name then test.name = table.concat(test_stack, "\0", 1, test_stack.n) end
|
|
|
|
test_count = test_count + 1
|
|
|
|
test_list[test_count] = test
|
|
|
|
test_map[test.name] = test_count
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Get the "friendly" name of this test.
|
|
|
|
--
|
|
|
|
-- @treturn string This test's friendly name
|
|
|
|
local function test_name(test) return (test.name:gsub("\0", " \26 ")) end
|
|
|
|
|
|
|
|
--- Describe something which will be tested, such as a function or situation
|
|
|
|
--
|
|
|
|
-- @tparam string name The name of the object to test
|
|
|
|
-- @tparam function body A function which describes the tests for this object.
|
|
|
|
local function describe(name, body)
|
|
|
|
check('describe', 1, 'string', name)
|
|
|
|
check('describe', 2, 'function', body)
|
|
|
|
if tests_locked then error("Cannot describe something while running tests", 2) end
|
|
|
|
|
|
|
|
-- Push our name onto the stack, eval and pop it
|
|
|
|
local n = test_stack.n + 1
|
|
|
|
test_stack[n], test_stack.n = name, n
|
|
|
|
|
|
|
|
local ok, err = try(body)
|
|
|
|
|
|
|
|
-- We count errors as a (failing) test.
|
|
|
|
if not ok then do_test { error = err } end
|
|
|
|
|
|
|
|
test_stack.n = n - 1
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Declare a single test within a context
|
|
|
|
--
|
|
|
|
-- @tparam string name What you are testing
|
|
|
|
-- @tparam function body A function which runs the test, failing if it does
|
|
|
|
-- the assertions are not met.
|
|
|
|
local function it(name, body)
|
|
|
|
check('it', 1, 'string', name)
|
|
|
|
check('it', 2, 'function', body)
|
|
|
|
if tests_locked then error("Cannot create test while running tests", 2) end
|
|
|
|
|
|
|
|
-- Push name onto the stack
|
|
|
|
local n = test_stack.n + 1
|
|
|
|
test_stack[n], test_stack.n, tests_locked = name, n, true
|
|
|
|
|
|
|
|
do_test { action = body }
|
|
|
|
|
|
|
|
-- Pop the test from the stack
|
|
|
|
test_stack.n, tests_locked = n - 1, false
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Declare a single not-yet-implemented test
|
|
|
|
--
|
|
|
|
-- @tparam string name What you really should be testing but aren't
|
|
|
|
local function pending(name)
|
|
|
|
check('it', 1, 'string', name)
|
|
|
|
if tests_locked then error("Cannot create test while running tests", 2) end
|
|
|
|
|
|
|
|
local _, loc = pcall(error, "", 3)
|
|
|
|
loc = loc:gsub(":%s*$", "")
|
|
|
|
|
|
|
|
local n = test_stack.n + 1
|
|
|
|
test_stack[n], test_stack.n = name, n
|
|
|
|
do_test { pending = true, trace = loc }
|
|
|
|
test_stack.n = n - 1
|
|
|
|
end
|
|
|
|
|
|
|
|
local arg = ...
|
|
|
|
if arg == "--help" or arg == "-h" then
|
|
|
|
io.write("Usage: mcfly [DIR]\n")
|
|
|
|
io.write("\n")
|
|
|
|
io.write("Run tests in the provided DIRectory, or `spec` if not given.")
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local root_dir = shell.resolve(arg or "spec")
|
|
|
|
if not fs.isDir(root_dir) then
|
|
|
|
io.stderr:write(("%q is not a directory.\n"):format(root_dir))
|
|
|
|
error()
|
|
|
|
end
|
|
|
|
|
2019-06-03 19:33:09 +00:00
|
|
|
-- Ensure the test folder is also on the package path
|
|
|
|
package.path = ("/%s/?.lua;/%s/?/init.lua;%s"):format(root_dir, root_dir, package.path)
|
|
|
|
|
2019-03-02 02:09:14 +00:00
|
|
|
do
|
|
|
|
-- Load in the tests from all our files
|
2019-05-30 18:36:28 +00:00
|
|
|
local env = setmetatable({}, { __index = _ENV })
|
|
|
|
|
|
|
|
local function set_env(tbl)
|
|
|
|
for k in pairs(env) do env[k] = nil end
|
|
|
|
for k, v in pairs(tbl) do env[k] = v end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- When declaring tests, you shouldn't be able to use test methods
|
|
|
|
set_env { describe = describe, it = it, pending = pending }
|
2019-03-02 02:09:14 +00:00
|
|
|
|
|
|
|
local suffix = "_spec.lua"
|
|
|
|
local function run_in(sub_dir)
|
|
|
|
for _, name in ipairs(fs.list(sub_dir)) do
|
|
|
|
local file = fs.combine(sub_dir, name)
|
|
|
|
if fs.isDir(file) then
|
|
|
|
run_in(file)
|
|
|
|
elseif file:sub(-#suffix) == suffix then
|
2019-07-12 21:04:28 +00:00
|
|
|
local fun, err = loadfile(file, nil, env)
|
2019-03-02 02:09:14 +00:00
|
|
|
if not fun then
|
|
|
|
do_test { name = file:sub(#root_dir + 2), error = { message = err } }
|
|
|
|
else
|
|
|
|
local ok, err = try(fun)
|
|
|
|
if not ok then do_test { name = file:sub(#root_dir + 2), error = err } end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
run_in(root_dir)
|
2019-05-30 18:36:28 +00:00
|
|
|
|
|
|
|
-- When running tests, you shouldn't be able to declare new ones.
|
|
|
|
set_env { expect = expect, fail = fail, stub = stub }
|
2019-03-02 02:09:14 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- Error if we've found no tests
|
|
|
|
if test_count == 0 then
|
|
|
|
io.stderr:write(("Could not find any tests in %q\n"):format(root_dir))
|
|
|
|
error()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- The results of each test, as well as how many passed and the count.
|
|
|
|
local test_results, test_status, tests_run = { n = 0 }, {}, 0
|
|
|
|
|
|
|
|
-- All possible test statuses
|
|
|
|
local statuses = {
|
|
|
|
pass = { desc = "Pass", col = colours.green, dot = "\7" }, -- Circle
|
|
|
|
fail = { desc = "Failed", col = colours.red, dot = "\4" }, -- Diamond
|
|
|
|
error = { desc = "Error", col = colours.magenta, dot = "\4" },
|
|
|
|
pending = { desc = "Pending", col = colours.yellow, dot = "\186" }, -- Hollow circle
|
|
|
|
}
|
|
|
|
|
|
|
|
-- Set up each test status count.
|
|
|
|
for k in pairs(statuses) do test_status[k] = 0 end
|
|
|
|
|
|
|
|
--- Do the actual running of our test
|
|
|
|
local function do_run(test)
|
|
|
|
-- If we're a pre-computed test, determine our status message. Otherwise,
|
|
|
|
-- skip.
|
|
|
|
local status, err
|
|
|
|
if test.pending then
|
|
|
|
status = "pending"
|
|
|
|
elseif test.error then
|
|
|
|
err = test.error
|
|
|
|
status = "error"
|
|
|
|
elseif test.action then
|
2019-05-30 18:36:28 +00:00
|
|
|
local state = push_state()
|
|
|
|
|
2019-03-02 02:09:14 +00:00
|
|
|
local ok
|
|
|
|
ok, err = try(test.action)
|
|
|
|
status = ok and "pass" or (err.fail and "fail" or "error")
|
2019-05-30 18:36:28 +00:00
|
|
|
|
|
|
|
pop_state(state)
|
2019-03-02 02:09:14 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- If we've a boolean status, then convert it into a string
|
|
|
|
if status == true then status = "pass"
|
|
|
|
elseif status == false then status = err.fail and "fail" or "error"
|
|
|
|
end
|
|
|
|
|
|
|
|
tests_run = tests_run + 1
|
|
|
|
test_status[status] = test_status[status] + 1
|
|
|
|
test_results[tests_run] = {
|
|
|
|
status = status, name = test.name,
|
|
|
|
message = test.message or err and err.message,
|
|
|
|
trace = test.trace or err and err.trace,
|
|
|
|
}
|
|
|
|
|
|
|
|
-- If we're running under howlci, then log some info.
|
|
|
|
if howlci then howlci.status(status, test_name(test)) end
|
|
|
|
if cct_test then cct_test.submit(test_results[tests_run]) end
|
|
|
|
|
|
|
|
-- Print our progress dot
|
|
|
|
local data = statuses[status]
|
|
|
|
term.setTextColour(data.col) io.write(data.dot)
|
|
|
|
term.setTextColour(colours.white)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Loop over all our tests, running them as required.
|
|
|
|
if cct_test then
|
|
|
|
-- If we're within a cct_test environment, then submit them and wait on tests
|
|
|
|
-- to be run.
|
|
|
|
cct_test.start(test_map)
|
|
|
|
while true do
|
|
|
|
local _, name = os.pullEvent("cct_test_run")
|
|
|
|
if not name then break end
|
|
|
|
do_run(test_list[test_map[name]])
|
|
|
|
end
|
|
|
|
else
|
|
|
|
for _, test in pairs(test_list) do do_run(test) end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Otherwise, display the results of each failure
|
|
|
|
io.write("\n\n")
|
|
|
|
for i = 1, tests_run do
|
|
|
|
local test = test_results[i]
|
|
|
|
if test.status ~= "pass" then
|
|
|
|
local status_data = statuses[test.status]
|
|
|
|
|
|
|
|
term.setTextColour(status_data.col)
|
|
|
|
io.write(status_data.desc)
|
|
|
|
term.setTextColour(colours.white)
|
|
|
|
io.write(" \26 " .. test_name(test) .. "\n")
|
|
|
|
|
|
|
|
if test.message then
|
|
|
|
io.write(" " .. test.message:gsub("\n", "\n ") .. "\n")
|
|
|
|
end
|
|
|
|
|
|
|
|
if test.trace then
|
|
|
|
term.setTextColour(colours.lightGrey)
|
|
|
|
io.write(" " .. test.trace:gsub("\n", "\n ") .. "\n")
|
|
|
|
end
|
|
|
|
|
|
|
|
io.write("\n")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- And some summary statistics
|
|
|
|
local actual_count = tests_run - test_status.pending
|
|
|
|
local info = ("Ran %s test(s), of which %s passed (%g%%).")
|
2019-12-07 10:33:47 +00:00
|
|
|
:format(actual_count, test_status.pass, test_status.pass / actual_count * 100)
|
2019-03-02 02:09:14 +00:00
|
|
|
|
|
|
|
if test_status.pending > 0 then
|
|
|
|
info = info .. (" Skipped %d pending test(s)."):format(test_status.pending)
|
|
|
|
end
|
|
|
|
|
|
|
|
term.setTextColour(colours.white) io.write(info .. "\n")
|
|
|
|
if howlci then howlci.log("debug", info) sleep(3) end
|