--[[ The MIT License Copyright (c) 2009-2012 [Norman Clarke](mailto:norman@njclarke.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]]-- --- Telescope is a test library for Lua that allows for flexible, declarative -- tests. The documentation produced here is intended largely for developers -- working on Telescope. For information on using Telescope, please visit the -- project homepage at: http://github.com/norman/telescope#readme. -- @release 0.6 -- @class module -- @module 'telescope' local _M = {} local compat_env = require 'compat_env' local getfenv = _G.getfenv or compat_env.getfenv local setfenv = _G.setfenv or compat_env.setfenv local _VERSION = "0.6.0" --- The status codes that can be returned by an invoked test. These should not be overidden. -- @name status_codes -- @class table -- @field err - This is returned when an invoked test results in an error -- rather than a passed or failed assertion. -- @field fail - This is returned when an invoked test contains one or more failing assertions. -- @field pass - This is returned when all of a test's assertions pass. -- @field pending - This is returned when a test does not have a corresponding function. -- @field unassertive - This is returned when an invoked test does not produce -- errors, but does not contain any assertions. local status_codes = { err = 2, fail = 4, pass = 8, pending = 16, unassertive = 32 } --- Labels used to show the various status_codes as a single character. -- These can be overidden if you wish. -- @name status_labels -- @class table -- @see status_codes -- @field status_codes.err 'E' -- @field status_codes.fail 'F' -- @field status_codes.pass 'P' -- @field status_codes.pending '?' -- @field status_codes.unassertive 'U' local status_labels = { [status_codes.err] = 'E', [status_codes.fail] = 'F', [status_codes.pass] = 'P', [status_codes.pending] = '?', [status_codes.unassertive] = 'U' } --- The default names for context blocks. It defaults to "context", "spec" and -- "describe." -- @name context_aliases -- @class table local context_aliases = {"context", "describe", "spec"} --- The default names for test blocks. It defaults to "test," "it", "expect", -- "they" and "should." -- @name test_aliases -- @class table local test_aliases = {"test", "it", "expect", "should", "they"} --- The default names for "before" blocks. It defaults to "before" and "setup." -- The function in the before block will be run before each sibling test function -- or context. -- @name before_aliases -- @class table local before_aliases = {"before", "setup"} --- The default names for "after" blocks. It defaults to "after" and "teardown." -- The function in the after block will be run after each sibling test function -- or context. -- @name after_aliases -- @class table local after_aliases = {"after", "teardown"} -- Prefix to place before all assertion messages. Used by make_assertion(). local assertion_message_prefix = "Assert failed: expected " --- The default assertions. -- These are the assertions built into telescope. You can override them or -- create your own custom assertions using make_assertion. --
-- The name will be used as the basis of the positive and negative assertions; -- i.e., the name equal would be used to create the assertions -- assert_equal and assert_not_equal. --
-- @param message The base message that will be shown. ---- The assertion message is what is shown when the assertion fails. It will be -- prefixed with the string in telescope.assertion_message_prefix. -- The variables passed to telescope.make_assertion are interpolated -- in the message string using string.format. When creating the -- inverse assertion, the message is reused, with " to be " replaced -- by " not to be ". Hence a recommended format is something like: -- "%s to be similar to %s". --
-- @param func The assertion function itself. ---- The assertion function can have any number of arguments. --
-- @usage make_assertion("equal", "%s to be equal to %s", function(a, b) -- return a == b end) -- @function make_assertion local function make_assertion(name, message, func) local num_vars = 0 -- if the last vararg ends up nil, we'll need to pad the table with nils so -- that string.format gets the number of args it expects local format_message if type(message) == "function" then format_message = message else for _, _ in message:gmatch("%%s") do num_vars = num_vars + 1 end format_message = function(message, ...) local a = {} local args = {...} local nargs = select('#', ...) if nargs > num_vars then local userErrorMessage = args[num_vars+1] if type(userErrorMessage) == "string" then return(assertion_message_prefix .. userErrorMessage) else error(string.format('assert_%s expected %d arguments but got %d', name, num_vars, #args)) end end for i = 1, nargs do a[i] = tostring(args[i]) end for i = nargs+1, num_vars do a[i] = 'nil' end return (assertion_message_prefix .. message):format(unpack(a)) end end assertions["assert_" .. name] = function(...) if assertion_callback then assertion_callback(...) end if not func(...) then error({format_message(message, ...), debug.traceback()}) end end end --- (local) Return a table with table t's values as keys and keys as values. -- @param t The table. local function invert_table(t) local t2 = {} for k, v in pairs(t) do t2[v] = k end return t2 end -- (local) Truncate a string "s" to length "len", optionally followed by the -- string given in "after" if truncated; for example, truncate_string("hello -- world", 3, "...") -- @param s The string to truncate. -- @param len The desired length. -- @param after A string to append to s, if it is truncated. local function truncate_string(s, len, after) if #s <= len then return s else local s = s:sub(1, len):gsub("%s*$", '') if after then return s .. after else return s end end end --- (local) Filter a table's values by function. This function iterates over a -- table , returning only the table entries that, when passed into function f, -- yield a truthy value. -- @param t The table over which to iterate. -- @param f The filter function. local function filter(t, f) local a, b return function() repeat a, b = next(t, a) if not b then return end if f(a, b) then return a, b end until not b end end --- (local) Finds the value in the contexts table indexed with i, and returns a table -- of i's ancestor contexts. -- @param i The index in the contexts table to get ancestors for. -- @param contexts The table in which to find the ancestors. local function ancestors(i, contexts) if i == 0 then return end local a = {} local function func(j) if contexts[j].parent == 0 then return nil end table.insert(a, contexts[j].parent) func(contexts[j].parent) end func(i) return a end make_assertion("blank", "'%s' to be blank", function(a) return a == '' or a == nil end) make_assertion("empty", "'%s' to be an empty table", function(a) return not next(a) end) make_assertion("equal", "'%s' to be equal to '%s'", function(a, b) return a == b end) make_assertion("error", "result to be an error", function(f) return not pcall(f) end) make_assertion("false", "'%s' to be false", function(a) return a == false end) make_assertion("greater_than", "'%s' to be greater than '%s'", function(a, b) return a > b end) make_assertion("gte", "'%s' to be greater than or equal to '%s'", function(a, b) return a >= b end) make_assertion("less_than", "'%s' to be less than '%s'", function(a, b) return a < b end) make_assertion("lte", "'%s' to be less than or equal to '%s'", function(a, b) return a <= b end) make_assertion("match", "'%s' to be a match for %s", function(a, b) return (tostring(b)):match(a) end) make_assertion("nil", "'%s' to be nil", function(a) return a == nil end) make_assertion("true", "'%s' to be true", function(a) return a == true end) make_assertion("type", "'%s' to be a %s", function(a, b) return type(a) == b end) make_assertion("not_blank", "'%s' not to be blank", function(a) return a ~= '' and a ~= nil end) make_assertion("not_empty", "'%s' not to be an empty table", function(a) return not not next(a) end) make_assertion("not_equal", "'%s' not to be equal to '%s'", function(a, b) return a ~= b end) make_assertion("not_error", "result not to be an error", function(f) return not not pcall(f) end) make_assertion("not_match", "'%s' not to be a match for %s", function(a, b) return not (tostring(b)):match(a) end) make_assertion("not_nil", "'%s' not to be nil", function(a) return a ~= nil end) make_assertion("not_type", "'%s' not to be a %s", function(a, b) return type(a) ~= b end) --- Build a contexts table from the test file or function given in target. -- If the optional contexts table argument is provided, then the -- resulting contexts will be added to it. ---- The resulting contexts table's structure is as follows: --
--
-- {
-- {parent = 0, name = "this is a context", context = true},
-- {parent = 1, name = "this is a nested context", context = true},
-- {parent = 2, name = "this is a test", test = function},
-- {parent = 2, name = "this is another test", test = function},
-- {parent = 0, name = "this is test outside any context", test = function},
-- }
--
-- @param contexts A optional table in which to collect the resulting contexts
-- and function.
-- @function load_contexts
local function load_contexts(target, contexts)
local env = {}
local current_index = 0
local context_table = contexts or {}
local function context_block(name, func)
table.insert(context_table, {parent = current_index, name = name, context = true})
local previous_index = current_index
current_index = #context_table
func()
current_index = previous_index
end
local function test_block(name, func)
local test_table = {name = name, parent = current_index, test = func or true}
if current_index ~= 0 then
test_table.context_name = context_table[current_index].name
else
test_table.context_name = 'top level'
end
table.insert(context_table, test_table)
end
local function before_block(func)
context_table[current_index].before = func
end
local function after_block(func)
context_table[current_index].after = func
end
for _, v in ipairs(after_aliases) do env[v] = after_block end
for _, v in ipairs(before_aliases) do env[v] = before_block end
for _, v in ipairs(context_aliases) do env[v] = context_block end
for _, v in ipairs(test_aliases) do env[v] = test_block end
-- Set these functions in the module's meta table to allow accessing
-- telescope's test and context functions without env tricks. This will
-- however add tests to a context table used inside the module, so multiple
-- test files will add tests to the same top-level context, which may or may
-- not be desired.
setmetatable(_M, {__index = env})
setmetatable(env, {__index = _G})
local func, err = type(target) == 'string' and assert(loadfile(target)) or target
if err then error(err) end
setfenv(func, env)()
return context_table
end
-- in-place table reverse.
function table.reverse(t)
local len = #t+1
for i=1, (len-1)/2 do
t[i], t[len-i] = t[len-i], t[i]
end
end
--- Run all tests.
-- This function will exectute each function in the contexts table.
-- @param contexts The contexts created by load_contexts.
-- @param callbacks A table of callback functions to be invoked before or after
-- various test states.
-- -- There is a callback for each test status_code, and callbacks to run -- before or after each test invocation regardless of outcome. --
---- Callbacks can be used, for example, to drop into a debugger upon a failed -- assertion or error, for profiling, or updating a GUI progress meter. --
-- @param test_filter A function to filter tests that match only conditions that you specify. ---- For example, the folling would allow you to run only tests whose name matches a pattern: --
--
--
-- function(t) return t.name:match("%s* lexer") end
--
--