Commit: 29d07dc26cf96a2fd41876246e3a67617df1e2ca
Parent: c9b81ad595d7d35a29d2661244e7dc0d8c87ee20
Author: Marko Leinikka
Date: Tue, 29 Mar 2022 20:25:55 +0300
add unit testing for get_episode_number
Run with: lua test.lua
Diffstat:
A | luaunit.lua | | | 3453 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | test.lua | | | 24 | ++++++++++++++++++++++++ |
2 files changed, 3477 insertions(+), 0 deletions(-)
diff --git a/luaunit.lua b/luaunit.lua
@@ -0,0 +1,3453 @@
+--[[
+ luaunit.lua
+
+Description: A unit testing framework
+Homepage: https://github.com/bluebird75/luaunit
+Development by Philippe Fremy <phil@freehackers.org>
+Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit)
+License: BSD License, see LICENSE.txt
+]]--
+
+require("math")
+local M={}
+
+-- private exported functions (for testing)
+M.private = {}
+
+M.VERSION='3.4'
+M._VERSION=M.VERSION -- For LuaUnit v2 compatibility
+
+-- a version which distinguish between regular Lua and LuaJit
+M._LUAVERSION = (jit and jit.version) or _VERSION
+
+--[[ Some people like assertEquals( actual, expected ) and some people prefer
+assertEquals( expected, actual ).
+]]--
+M.ORDER_ACTUAL_EXPECTED = true
+M.PRINT_TABLE_REF_IN_ERROR_MSG = false
+M.LINE_LENGTH = 80
+M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items
+M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items
+
+-- this setting allow to remove entries from the stack-trace, for
+-- example to hide a call to a framework which would be calling luaunit
+M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE = 0
+
+--[[ EPS is meant to help with Lua's floating point math in simple corner
+cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers
+with rational binary representation) if the user doesn't provide some explicit
+error margin.
+
+The default margin used by almostEquals() in such cases is EPS; and since
+Lua may be compiled with different numeric precisions (single vs. double), we
+try to select a useful default for it dynamically. Note: If the initial value
+is not acceptable, it can be changed by the user to better suit specific needs.
+
+See also: https://en.wikipedia.org/wiki/Machine_epsilon
+]]
+M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16
+if math.abs(1.1 - 1 - 0.1) > M.EPS then
+ -- rounding error is above EPS, assume single precision
+ M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07
+end
+
+-- set this to false to debug luaunit
+local STRIP_LUAUNIT_FROM_STACKTRACE = true
+
+M.VERBOSITY_DEFAULT = 10
+M.VERBOSITY_LOW = 1
+M.VERBOSITY_QUIET = 0
+M.VERBOSITY_VERBOSE = 20
+M.DEFAULT_DEEP_ANALYSIS = nil
+M.FORCE_DEEP_ANALYSIS = true
+M.DISABLE_DEEP_ANALYSIS = false
+
+-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values
+-- EXPORT_ASSERT_TO_GLOBALS = true
+
+-- we need to keep a copy of the script args before it is overriden
+local cmdline_argv = rawget(_G, "arg")
+
+M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests
+M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early
+M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests
+
+
+
+M.USAGE=[[Usage: lua <your_test_suite.lua> [options] [testname1 [testname2] ... ]
+Options:
+ -h, --help: Print this help
+ --version: Print version information
+ -v, --verbose: Increase verbosity
+ -q, --quiet: Set verbosity to minimum
+ -e, --error: Stop on first error
+ -f, --failure: Stop on first failure or error
+ -s, --shuffle: Shuffle tests before running them
+ -o, --output OUTPUT: Set output type to OUTPUT
+ Possible values: text, tap, junit, nil
+ -n, --name NAME: For junit only, mandatory name of xml file
+ -r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT
+ -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN
+ May be repeated to include several patterns
+ Make sure you escape magic chars like +? with %
+ -x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN
+ May be repeated to exclude several patterns
+ Make sure you escape magic chars like +? with %
+ testname1, testname2, ... : tests to run in the form of testFunction,
+ TestClass or TestClass.testMethod
+
+You may also control LuaUnit options with the following environment variables:
+* LUAUNIT_OUTPUT: same as --output
+* LUAUNIT_JUNIT_FNAME: same as --name ]]
+
+----------------------------------------------------------------
+--
+-- general utility functions
+--
+----------------------------------------------------------------
+
+--[[ Note on catching exit
+
+I have seen the case where running a big suite of test cases and one of them would
+perform a os.exit(0), making the outside world think that the full test suite was executed
+successfully.
+
+This is an attempt to mitigate this problem: we override os.exit() to now let a test
+exit the framework while we are running. When we are not running, it behaves normally.
+]]
+
+M.oldOsExit = os.exit
+os.exit = function(...)
+ if M.LuaUnit and #M.LuaUnit.instances ~= 0 then
+ local msg = [[You are trying to exit but there is still a running instance of LuaUnit.
+LuaUnit expects to run until the end before exiting with a complete status of successful/failed tests.
+
+To force exit LuaUnit while running, please call before os.exit (assuming lu is the luaunit module loaded):
+
+ lu.unregisterCurrentSuite()
+
+]]
+ M.private.error_fmt(2, msg)
+ end
+ M.oldOsExit(...)
+end
+
+local function pcall_or_abort(func, ...)
+ -- unpack is a global function for Lua 5.1, otherwise use table.unpack
+ local unpack = rawget(_G, "unpack") or table.unpack
+ local result = {pcall(func, ...)}
+ if not result[1] then
+ -- an error occurred
+ print(result[2]) -- error message
+ print()
+ print(M.USAGE)
+ os.exit(-1)
+ end
+ return unpack(result, 2)
+end
+
+local crossTypeOrdering = {
+ number = 1, boolean = 2, string = 3, table = 4, other = 5
+}
+local crossTypeComparison = {
+ number = function(a, b) return a < b end,
+ string = function(a, b) return a < b end,
+ other = function(a, b) return tostring(a) < tostring(b) end,
+}
+
+local function crossTypeSort(a, b)
+ local type_a, type_b = type(a), type(b)
+ if type_a == type_b then
+ local func = crossTypeComparison[type_a] or crossTypeComparison.other
+ return func(a, b)
+ end
+ type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other
+ type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other
+ return type_a < type_b
+end
+
+local function __genSortedIndex( t )
+ -- Returns a sequence consisting of t's keys, sorted.
+ local sortedIndex = {}
+
+ for key,_ in pairs(t) do
+ table.insert(sortedIndex, key)
+ end
+
+ table.sort(sortedIndex, crossTypeSort)
+ return sortedIndex
+end
+M.private.__genSortedIndex = __genSortedIndex
+
+local function sortedNext(state, control)
+ -- Equivalent of the next() function of table iteration, but returns the
+ -- keys in sorted order (see __genSortedIndex and crossTypeSort).
+ -- The state is a temporary variable during iteration and contains the
+ -- sorted key table (state.sortedIdx). It also stores the last index (into
+ -- the keys) used by the iteration, to find the next one quickly.
+ local key
+
+ --print("sortedNext: control = "..tostring(control) )
+ if control == nil then
+ -- start of iteration
+ state.count = #state.sortedIdx
+ state.lastIdx = 1
+ key = state.sortedIdx[1]
+ return key, state.t[key]
+ end
+
+ -- normally, we expect the control variable to match the last key used
+ if control ~= state.sortedIdx[state.lastIdx] then
+ -- strange, we have to find the next value by ourselves
+ -- the key table is sorted in crossTypeSort() order! -> use bisection
+ local lower, upper = 1, state.count
+ repeat
+ state.lastIdx = math.modf((lower + upper) / 2)
+ key = state.sortedIdx[state.lastIdx]
+ if key == control then
+ break -- key found (and thus prev index)
+ end
+ if crossTypeSort(key, control) then
+ -- key < control, continue search "right" (towards upper bound)
+ lower = state.lastIdx + 1
+ else
+ -- key > control, continue search "left" (towards lower bound)
+ upper = state.lastIdx - 1
+ end
+ until lower > upper
+ if lower > upper then -- only true if the key wasn't found, ...
+ state.lastIdx = state.count -- ... so ensure no match in code below
+ end
+ end
+
+ -- proceed by retrieving the next value (or nil) from the sorted keys
+ state.lastIdx = state.lastIdx + 1
+ key = state.sortedIdx[state.lastIdx]
+ if key then
+ return key, state.t[key]
+ end
+
+ -- getting here means returning `nil`, which will end the iteration
+end
+
+local function sortedPairs(tbl)
+ -- Equivalent of the pairs() function on tables. Allows to iterate in
+ -- sorted order. As required by "generic for" loops, this will return the
+ -- iterator (function), an "invariant state", and the initial control value.
+ -- (see http://www.lua.org/pil/7.2.html)
+ return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil
+end
+M.private.sortedPairs = sortedPairs
+
+-- seed the random with a strongly varying seed
+math.randomseed(math.floor(os.clock()*1E11))
+
+local function randomizeTable( t )
+ -- randomize the item orders of the table t
+ for i = #t, 2, -1 do
+ local j = math.random(i)
+ if i ~= j then
+ t[i], t[j] = t[j], t[i]
+ end
+ end
+end
+M.private.randomizeTable = randomizeTable
+
+local function strsplit(delimiter, text)
+-- Split text into a list consisting of the strings in text, separated
+-- by strings matching delimiter (which may _NOT_ be a pattern).
+-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores")
+ if delimiter == "" or delimiter == nil then -- this would result in endless loops
+ error("delimiter is nil or empty string!")
+ end
+ if text == nil then
+ return nil
+ end
+
+ local list, pos, first, last = {}, 1
+ while true do
+ first, last = text:find(delimiter, pos, true)
+ if first then -- found?
+ table.insert(list, text:sub(pos, first - 1))
+ pos = last + 1
+ else
+ table.insert(list, text:sub(pos))
+ break
+ end
+ end
+ return list
+end
+M.private.strsplit = strsplit
+
+local function hasNewLine( s )
+ -- return true if s has a newline
+ return (string.find(s, '\n', 1, true) ~= nil)
+end
+M.private.hasNewLine = hasNewLine
+
+local function prefixString( prefix, s )
+ -- Prefix all the lines of s with prefix
+ return prefix .. string.gsub(s, '\n', '\n' .. prefix)
+end
+M.private.prefixString = prefixString
+
+local function strMatch(s, pattern, start, final )
+ -- return true if s matches completely the pattern from index start to index end
+ -- return false in every other cases
+ -- if start is nil, matches from the beginning of the string
+ -- if final is nil, matches to the end of the string
+ start = start or 1
+ final = final or string.len(s)
+
+ local foundStart, foundEnd = string.find(s, pattern, start, false)
+ return foundStart == start and foundEnd == final
+end
+M.private.strMatch = strMatch
+
+local function patternFilter(patterns, expr)
+ -- Run `expr` through the inclusion and exclusion rules defined in patterns
+ -- and return true if expr shall be included, false for excluded.
+ -- Inclusion pattern are defined as normal patterns, exclusions
+ -- patterns start with `!` and are followed by a normal pattern
+
+ -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT
+ -- default: true if no explicit "include" is found, set to false otherwise
+ local default, result = true, nil
+
+ if patterns ~= nil then
+ for _, pattern in ipairs(patterns) do
+ local exclude = pattern:sub(1,1) == '!'
+ if exclude then
+ pattern = pattern:sub(2)
+ else
+ -- at least one include pattern specified, a match is required
+ default = false
+ end
+ -- print('pattern: ',pattern)
+ -- print('exclude: ',exclude)
+ -- print('default: ',default)
+
+ if string.find(expr, pattern) then
+ -- set result to false when excluding, true otherwise
+ result = not exclude
+ end
+ end
+ end
+
+ if result ~= nil then
+ return result
+ end
+ return default
+end
+M.private.patternFilter = patternFilter
+
+local function xmlEscape( s )
+ -- Return s escaped for XML attributes
+ -- escapes table:
+ -- " "
+ -- ' '
+ -- < <
+ -- > >
+ -- & &
+
+ return string.gsub( s, '.', {
+ ['&'] = "&",
+ ['"'] = """,
+ ["'"] = "'",
+ ['<'] = "<",
+ ['>'] = ">",
+ } )
+end
+M.private.xmlEscape = xmlEscape
+
+local function xmlCDataEscape( s )
+ -- Return s escaped for CData section, escapes: "]]>"
+ return string.gsub( s, ']]>', ']]>' )
+end
+M.private.xmlCDataEscape = xmlCDataEscape
+
+
+local function lstrip( s )
+ --[[Return s with all leading white spaces and tabs removed]]
+ local idx = 0
+ while idx < s:len() do
+ idx = idx + 1
+ local c = s:sub(idx,idx)
+ if c ~= ' ' and c ~= '\t' then
+ break
+ end
+ end
+ return s:sub(idx)
+end
+M.private.lstrip = lstrip
+
+local function extractFileLineInfo( s )
+ --[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg"
+
+ Return the "file.lua:linenb" information
+ ]]
+ local s2 = lstrip(s)
+ local firstColon = s2:find(':', 1, true)
+ if firstColon == nil then
+ -- string is not in the format file:line:
+ return s
+ end
+ local secondColon = s2:find(':', firstColon+1, true)
+ if secondColon == nil then
+ -- string is not in the format file:line:
+ return s
+ end
+
+ return s2:sub(1, secondColon-1)
+end
+M.private.extractFileLineInfo = extractFileLineInfo
+
+
+local function stripLuaunitTrace2( stackTrace, errMsg )
+ --[[
+ -- Example of a traceback:
+ <<stack traceback:
+ example_with_luaunit.lua:130: in function 'test2_withFailure'
+ ./luaunit.lua:1449: in function <./luaunit.lua:1449>
+ [C]: in function 'xpcall'
+ ./luaunit.lua:1449: in function 'protectedCall'
+ ./luaunit.lua:1508: in function 'execOneFunction'
+ ./luaunit.lua:1596: in function 'runSuiteByInstances'
+ ./luaunit.lua:1660: in function 'runSuiteByNames'
+ ./luaunit.lua:1736: in function 'runSuite'
+ example_with_luaunit.lua:140: in main chunk
+ [C]: in ?>>
+ error message: <<example_with_luaunit.lua:130: expected 2, got 1>>
+
+ Other example:
+ <<stack traceback:
+ ./luaunit.lua:545: in function 'assertEquals'
+ example_with_luaunit.lua:58: in function 'TestToto.test7'
+ ./luaunit.lua:1517: in function <./luaunit.lua:1517>
+ [C]: in function 'xpcall'
+ ./luaunit.lua:1517: in function 'protectedCall'
+ ./luaunit.lua:1578: in function 'execOneFunction'
+ ./luaunit.lua:1677: in function 'runSuiteByInstances'
+ ./luaunit.lua:1730: in function 'runSuiteByNames'
+ ./luaunit.lua:1806: in function 'runSuite'
+ example_with_luaunit.lua:140: in main chunk
+ [C]: in ?>>
+ error message: <<example_with_luaunit.lua:58: expected 2, got 1>>
+
+ <<stack traceback:
+ luaunit2/example_with_luaunit.lua:124: in function 'test1_withFailure'
+ luaunit2/luaunit.lua:1532: in function <luaunit2/luaunit.lua:1532>
+ [C]: in function 'xpcall'
+ luaunit2/luaunit.lua:1532: in function 'protectedCall'
+ luaunit2/luaunit.lua:1591: in function 'execOneFunction'
+ luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances'
+ luaunit2/luaunit.lua:1743: in function 'runSuiteByNames'
+ luaunit2/luaunit.lua:1819: in function 'runSuite'
+ luaunit2/example_with_luaunit.lua:140: in main chunk
+ [C]: in ?>>
+ error message: <<luaunit2/example_with_luaunit.lua:124: expected 2, got 1>>
+
+
+ -- first line is "stack traceback": KEEP
+ -- next line may be luaunit line: REMOVE
+ -- next lines are call in the program under testOk: REMOVE
+ -- next lines are calls from luaunit to call the program under test: KEEP
+
+ -- Strategy:
+ -- keep first line
+ -- remove lines that are part of luaunit
+ -- kepp lines until we hit a luaunit line
+
+ The strategy for stripping is:
+ * keep first line "stack traceback:"
+ * part1:
+ * analyse all lines of the stack from bottom to top of the stack (first line to last line)
+ * extract the "file:line:" part of the line
+ * compare it with the "file:line" part of the error message
+ * if it does not match strip the line
+ * if it matches, keep the line and move to part 2
+ * part2:
+ * anything NOT starting with luaunit.lua is the interesting part of the stack trace
+ * anything starting again with luaunit.lua is part of the test launcher and should be stripped out
+ ]]
+
+ local function isLuaunitInternalLine( s )
+ -- return true if line of stack trace comes from inside luaunit
+ return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil
+ end
+
+ -- print( '<<'..stackTrace..'>>' )
+
+ local t = strsplit( '\n', stackTrace )
+ -- print( prettystr(t) )
+
+ local idx = 2
+
+ local errMsgFileLine = extractFileLineInfo(errMsg)
+ -- print('emfi="'..errMsgFileLine..'"')
+
+ -- remove lines that are still part of luaunit
+ while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do
+ -- print('Removing : '..t[idx] )
+ table.remove(t, idx)
+ end
+
+ -- keep lines until we hit luaunit again
+ while t[idx] and (not isLuaunitInternalLine(t[idx])) do
+ -- print('Keeping : '..t[idx] )
+ idx = idx + 1
+ end
+
+ -- remove remaining luaunit lines
+ while t[idx] do
+ -- print('Removing2 : '..t[idx] )
+ table.remove(t, idx)
+ end
+
+ -- print( prettystr(t) )
+ return table.concat( t, '\n')
+
+end
+M.private.stripLuaunitTrace2 = stripLuaunitTrace2
+
+
+local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable )
+ local type_v = type(v)
+ if "string" == type_v then
+ -- use clever delimiters according to content:
+ -- enclose with single quotes if string contains ", but no '
+ if v:find('"', 1, true) and not v:find("'", 1, true) then
+ return "'" .. v .. "'"
+ end
+ -- use double quotes otherwise, escape embedded "
+ return '"' .. v:gsub('"', '\\"') .. '"'
+
+ elseif "table" == type_v then
+ --if v.__class__ then
+ -- return string.gsub( tostring(v), 'table', v.__class__ )
+ --end
+ return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable)
+
+ elseif "number" == type_v then
+ -- eliminate differences in formatting between various Lua versions
+ if v ~= v then
+ return "#NaN" -- "not a number"
+ end
+ if v == math.huge then
+ return "#Inf" -- "infinite"
+ end
+ if v == -math.huge then
+ return "-#Inf"
+ end
+ if _VERSION == "Lua 5.3" then
+ local i = math.tointeger(v)
+ if i then
+ return tostring(i)
+ end
+ end
+ end
+
+ return tostring(v)
+end
+
+local function prettystr( v )
+ --[[ Pretty string conversion, to display the full content of a variable of any type.
+
+ * string are enclosed with " by default, or with ' if string contains a "
+ * tables are expanded to show their full content, with indentation in case of nested tables
+ ]]--
+ local cycleDetectTable = {}
+ local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable)
+ if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then
+ -- some table contain recursive references,
+ -- so we must recompute the value by including all table references
+ -- else the result looks like crap
+ cycleDetectTable = {}
+ s = prettystr_sub(v, 1, true, cycleDetectTable)
+ end
+ return s
+end
+M.prettystr = prettystr
+
+function M.adjust_err_msg_with_iter( err_msg, iter_msg )
+ --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed,
+ add the iteration message if any and return the result.
+
+ err_msg: string, error message captured with pcall
+ iter_msg: a string describing the current iteration ("iteration N") or nil
+ if there is no iteration in this test.
+
+ Returns: (new_err_msg, test_status)
+ new_err_msg: string, adjusted error message, or nil in case of success
+ test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information
+ contained in the error message.
+ ]]
+ if iter_msg then
+ iter_msg = iter_msg..', '
+ else
+ iter_msg = ''
+ end
+
+ local RE_FILE_LINE = '.*:%d+: '
+
+ -- error message is not necessarily a string,
+ -- so convert the value to string with prettystr()
+ if type( err_msg ) ~= 'string' then
+ err_msg = prettystr( err_msg )
+ end
+
+ if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then
+ -- test finished early with success()
+ return nil, M.NodeStatus.SUCCESS
+ end
+
+ if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then
+ -- substitute prefix by iteration message
+ err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1)
+ -- print("failure detected")
+ return err_msg, M.NodeStatus.SKIP
+ end
+
+ if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then
+ -- substitute prefix by iteration message
+ err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1)
+ -- print("failure detected")
+ return err_msg, M.NodeStatus.FAIL
+ end
+
+
+
+ -- print("error detected")
+ -- regular error, not a failure
+ if iter_msg then
+ local match
+ -- "./test\\test_luaunit.lua:2241: some error msg
+ match = err_msg:match( '(.*:%d+: ).*' )
+ if match then
+ err_msg = err_msg:gsub( match, match .. iter_msg )
+ else
+ -- no file:line: infromation, just add the iteration info at the beginning of the line
+ err_msg = iter_msg .. err_msg
+ end
+ end
+ return err_msg, M.NodeStatus.ERROR
+end
+
+local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin )
+ --[[
+ Prepares a nice error message when comparing tables, performing a deeper
+ analysis.
+
+ Arguments:
+ * table_a, table_b: tables to be compared
+ * doDeepAnalysis:
+ M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries
+ M.FORCE_DEEP_ANALYSIS : always perform deep analysis
+ M.DISABLE_DEEP_ANALYSIS: never perform deep analysis
+ * margin: supplied only for almost equality
+
+ Returns: {success, result}
+ * success: false if deep analysis could not be performed
+ in this case, just use standard assertion message
+ * result: if success is true, a multi-line string with deep analysis of the two lists
+ ]]
+
+ -- check if table_a & table_b are suitable for deep analysis
+ if type(table_a) ~= 'table' or type(table_b) ~= 'table' then
+ return false
+ end
+
+ if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then
+ return false
+ end
+
+ local len_a, len_b, isPureList = #table_a, #table_b, true
+
+ for k1, v1 in pairs(table_a) do
+ if type(k1) ~= 'number' or k1 > len_a then
+ -- this table a mapping
+ isPureList = false
+ break
+ end
+ end
+
+ if isPureList then
+ for k2, v2 in pairs(table_b) do
+ if type(k2) ~= 'number' or k2 > len_b then
+ -- this table a mapping
+ isPureList = false
+ break
+ end
+ end
+ end
+
+ if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then
+ if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then
+ return false
+ end
+ end
+
+ if isPureList then
+ return M.private.mismatchFormattingPureList( table_a, table_b, margin )
+ else
+ -- only work on mapping for the moment
+ -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis )
+ return false
+ end
+end
+M.private.tryMismatchFormatting = tryMismatchFormatting
+
+local function getTaTbDescr()
+ if not M.ORDER_ACTUAL_EXPECTED then
+ return 'expected', 'actual'
+ end
+ return 'actual', 'expected'
+end
+
+local function extendWithStrFmt( res, ... )
+ table.insert( res, string.format( ... ) )
+end
+
+local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis )
+ --[[
+ Prepares a nice error message when comparing tables which are not pure lists, performing a deeper
+ analysis.
+
+ Returns: {success, result}
+ * success: false if deep analysis could not be performed
+ in this case, just use standard assertion message
+ * result: if success is true, a multi-line string with deep analysis of the two lists
+ ]]
+
+ -- disable for the moment
+ --[[
+ local result = {}
+ local descrTa, descrTb = getTaTbDescr()
+
+ local keysCommon = {}
+ local keysOnlyTa = {}
+ local keysOnlyTb = {}
+ local keysDiffTaTb = {}
+
+ local k, v
+
+ for k,v in pairs( table_a ) do
+ if is_equal( v, table_b[k] ) then
+ table.insert( keysCommon, k )
+ else
+ if table_b[k] == nil then
+ table.insert( keysOnlyTa, k )
+ else
+ table.insert( keysDiffTaTb, k )
+ end
+ end
+ end
+
+ for k,v in pairs( table_b ) do
+ if not is_equal( v, table_a[k] ) and table_a[k] == nil then
+ table.insert( keysOnlyTb, k )
+ end
+ end
+
+ local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa
+ local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb
+ local limited_display = (len_a < 5 or len_b < 5)
+
+ if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then
+ return false
+ end
+
+ if not limited_display then
+ if len_a == len_b then
+ extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a )
+ else
+ extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b )
+ end
+
+ if #keysCommon == 0 and #keysDiffTaTb == 0 then
+ table.insert( result, 'Table A and B have no keys in common, they are totally different')
+ else
+ local s_other = 'other '
+ if #keysCommon then
+ extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon )
+ else
+ table.insert( result, 'Table A and B have no identical items' )
+ s_other = ''
+ end
+
+ if #keysDiffTaTb ~= 0 then
+ result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb)
+ else
+ result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb)
+ end
+ end
+
+ extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb )
+ end
+
+ local function keytostring(k)
+ if "string" == type(k) and k:match("^[_%a][_%w]*$") then
+ return k
+ end
+ return prettystr(k)
+ end
+
+ if #keysDiffTaTb ~= 0 then
+ table.insert( result, 'Items differing in A and B:')
+ for k,v in sortedPairs( keysDiffTaTb ) do
+ extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )
+ extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )
+ end
+ end
+
+ if #keysOnlyTa ~= 0 then
+ table.insert( result, 'Items only in table A:' )
+ for k,v in sortedPairs( keysOnlyTa ) do
+ extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )
+ end
+ end
+
+ if #keysOnlyTb ~= 0 then
+ table.insert( result, 'Items only in table B:' )
+ for k,v in sortedPairs( keysOnlyTb ) do
+ extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )
+ end
+ end
+
+ if #keysCommon ~= 0 then
+ table.insert( result, 'Items common to A and B:')
+ for k,v in sortedPairs( keysCommon ) do
+ extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) )
+ end
+ end
+
+ return true, table.concat( result, '\n')
+ ]]
+end
+M.private.mismatchFormattingMapping = mismatchFormattingMapping
+
+local function mismatchFormattingPureList( table_a, table_b, margin )
+ --[[
+ Prepares a nice error message when comparing tables which are lists, performing a deeper
+ analysis.
+
+ margin is supplied only for almost equality
+
+ Returns: {success, result}
+ * success: false if deep analysis could not be performed
+ in this case, just use standard assertion message
+ * result: if success is true, a multi-line string with deep analysis of the two lists
+ ]]
+ local result, descrTa, descrTb = {}, getTaTbDescr()
+
+ local len_a, len_b, refa, refb = #table_a, #table_b, '', ''
+ if M.PRINT_TABLE_REF_IN_ERROR_MSG then
+ refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) )
+ end
+ local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b)
+ local deltalv = longest - shortest
+
+ local commonUntil = shortest
+ for i = 1, shortest do
+ if not M.private.is_table_equals(table_a[i], table_b[i], margin) then
+ commonUntil = i - 1
+ break
+ end
+ end
+
+ local commonBackTo = shortest - 1
+ for i = 0, shortest - 1 do
+ if not M.private.is_table_equals(table_a[len_a-i], table_b[len_b-i], margin) then
+ commonBackTo = i - 1
+ break
+ end
+ end
+
+
+ table.insert( result, 'List difference analysis:' )
+ if len_a == len_b then
+ -- TODO: handle expected/actual naming
+ extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb )
+ else
+ extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b )
+ end
+
+ extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 )
+ if commonBackTo >= 0 then
+ if deltalv > 0 then
+ extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo )
+ else
+ extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo )
+ end
+ end
+
+ local function insertABValue(ai, bi)
+ bi = bi or ai
+ if M.private.is_table_equals( table_a[ai], table_b[bi], margin) then
+ return extendWithStrFmt( result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) )
+ else
+ extendWithStrFmt( result, ' - A[%d]: %s', ai, prettystr(table_a[ai]))
+ extendWithStrFmt( result, ' + B[%d]: %s', bi, prettystr(table_b[bi]))
+ end
+ end
+
+ -- common parts to list A & B, at the beginning
+ if commonUntil > 0 then
+ table.insert( result, '* Common parts:' )
+ for i = 1, commonUntil do
+ insertABValue( i )
+ end
+ end
+
+ -- diffing parts to list A & B
+ if commonUntil < shortest - commonBackTo - 1 then
+ table.insert( result, '* Differing parts:' )
+ for i = commonUntil + 1, shortest - commonBackTo - 1 do
+ insertABValue( i )
+ end
+ end
+
+ -- display indexes of one list, with no match on other list
+ if shortest - commonBackTo <= longest - commonBackTo - 1 then
+ table.insert( result, '* Present only in one list:' )
+ for i = shortest - commonBackTo, longest - commonBackTo - 1 do
+ if len_a > len_b then
+ extendWithStrFmt( result, ' - A[%d]: %s', i, prettystr(table_a[i]) )
+ -- table.insert( result, '+ (no matching B index)')
+ else
+ -- table.insert( result, '- no matching A index')
+ extendWithStrFmt( result, ' + B[%d]: %s', i, prettystr(table_b[i]) )
+ end
+ end
+ end
+
+ -- common parts to list A & B, at the end
+ if commonBackTo >= 0 then
+ table.insert( result, '* Common parts at the end of the lists' )
+ for i = longest - commonBackTo, longest do
+ if len_a > len_b then
+ insertABValue( i, i-deltalv )
+ else
+ insertABValue( i-deltalv, i )
+ end
+ end
+ end
+
+ return true, table.concat( result, '\n')
+end
+M.private.mismatchFormattingPureList = mismatchFormattingPureList
+
+local function prettystrPairs(value1, value2, suffix_a, suffix_b)
+ --[[
+ This function helps with the recurring task of constructing the "expected
+ vs. actual" error messages. It takes two arbitrary values and formats
+ corresponding strings with prettystr().
+
+ To keep the (possibly complex) output more readable in case the resulting
+ strings contain line breaks, they get automatically prefixed with additional
+ newlines. Both suffixes are optional (default to empty strings), and get
+ appended to the "value1" string. "suffix_a" is used if line breaks were
+ encountered, "suffix_b" otherwise.
+
+ Returns the two formatted strings (including padding/newlines).
+ ]]
+ local str1, str2 = prettystr(value1), prettystr(value2)
+ if hasNewLine(str1) or hasNewLine(str2) then
+ -- line break(s) detected, add padding
+ return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2
+ end
+ return str1 .. (suffix_b or ""), str2
+end
+M.private.prettystrPairs = prettystrPairs
+
+local UNKNOWN_REF = 'table 00-unknown ref'
+local ref_generator = { value=1, [UNKNOWN_REF]=0 }
+
+local function table_ref( t )
+ -- return the default tostring() for tables, with the table ID, even if the table has a metatable
+ -- with the __tostring converter
+ local ref = ''
+ local mt = getmetatable( t )
+ if mt == nil then
+ ref = tostring(t)
+ else
+ local success, result
+ success, result = pcall(setmetatable, t, nil)
+ if not success then
+ -- protected table, if __tostring is defined, we can
+ -- not get the reference. And we can not know in advance.
+ ref = tostring(t)
+ if not ref:match( 'table: 0?x?[%x]+' ) then
+ return UNKNOWN_REF
+ end
+ else
+ ref = tostring(t)
+ setmetatable( t, mt )
+ end
+ end
+ -- strip the "table: " part
+ ref = ref:sub(8)
+ if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then
+ -- Create a new reference number
+ ref_generator[ref] = ref_generator.value
+ ref_generator.value = ref_generator.value+1
+ end
+ if M.PRINT_TABLE_REF_IN_ERROR_MSG then
+ return string.format('table %02d-%s', ref_generator[ref], ref)
+ else
+ return string.format('table %02d', ref_generator[ref])
+ end
+end
+M.private.table_ref = table_ref
+
+local TABLE_TOSTRING_SEP = ", "
+local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP)
+
+local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable )
+ printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG
+ cycleDetectTable = cycleDetectTable or {}
+ cycleDetectTable[tbl] = true
+
+ local result, dispOnMultLines = {}, false
+
+ -- like prettystr but do not enclose with "" if the string is just alphanumerical
+ -- this is better for displaying table keys who are often simple strings
+ local function keytostring(k)
+ if "string" == type(k) and k:match("^[_%a][_%w]*$") then
+ return k
+ end
+ return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable)
+ end
+
+ local mt = getmetatable( tbl )
+
+ if mt and mt.__tostring then
+ -- if table has a __tostring() function in its metatable, use it to display the table
+ -- else, compute a regular table
+ result = tostring(tbl)
+ if type(result) ~= 'string' then
+ return string.format( '<invalid tostring() result: "%s" >', prettystr(result) )
+ end
+ result = strsplit( '\n', result )
+ return M.private._table_tostring_format_multiline_string( result, indentLevel )
+
+ else
+ -- no metatable, compute the table representation
+
+ local entry, count, seq_index = nil, 0, 1
+ for k, v in sortedPairs( tbl ) do
+
+ -- key part
+ if k == seq_index then
+ -- for the sequential part of tables, we'll skip the "<key>=" output
+ entry = ''
+ seq_index = seq_index + 1
+ elseif cycleDetectTable[k] then
+ -- recursion in the key detected
+ cycleDetectTable.detected = true
+ entry = "<"..table_ref(k)..">="
+ else
+ entry = keytostring(k) .. "="
+ end
+
+ -- value part
+ if cycleDetectTable[v] then
+ -- recursion in the value detected!
+ cycleDetectTable.detected = true
+ entry = entry .. "<"..table_ref(v)..">"
+ else
+ entry = entry ..
+ prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable )
+ end
+ count = count + 1
+ result[count] = entry
+ end
+ return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs )
+ end
+
+end
+M.private._table_tostring = _table_tostring -- prettystr_sub() needs it
+
+local function _table_tostring_format_multiline_string( tbl_str, indentLevel )
+ local indentString = '\n'..string.rep(" ", indentLevel - 1)
+ return table.concat( tbl_str, indentString )
+
+end
+M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string
+
+
+local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs )
+ -- final function called in _table_to_string() to format the resulting list of
+ -- string describing the table.
+
+ local dispOnMultLines = false
+
+ -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values
+ local totalLength = 0
+ for k, v in ipairs( result ) do
+ totalLength = totalLength + string.len( v )
+ if totalLength >= M.LINE_LENGTH then
+ dispOnMultLines = true
+ break
+ end
+ end
+
+ -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded
+ -- with the values and the separators.
+ if not dispOnMultLines then
+ -- adjust with length of separator(s):
+ -- two items need 1 sep, three items two seps, ... plus len of '{}'
+ if #result > 0 then
+ totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1)
+ end
+ dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH)
+ end
+
+ -- now reformat the result table (currently holding element strings)
+ if dispOnMultLines then
+ local indentString = string.rep(" ", indentLevel - 1)
+ result = {
+ "{\n ",
+ indentString,
+ table.concat(result, ",\n " .. indentString),
+ "\n",
+ indentString,
+ "}"
+ }
+ else
+ result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"}
+ end
+ if printTableRefs then
+ table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref
+ end
+ return table.concat(result)
+end
+M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it
+
+local function table_findkeyof(t, element)
+ -- Return the key k of the given element in table t, so that t[k] == element
+ -- (or `nil` if element is not present within t). Note that we use our
+ -- 'general' is_equal comparison for matching, so this function should
+ -- handle table-type elements gracefully and consistently.
+ if type(t) == "table" then
+ for k, v in pairs(t) do
+ if M.private.is_table_equals(v, element) then
+ return k
+ end
+ end
+ end
+ return nil
+end
+
+local function _is_table_items_equals(actual, expected )
+ local type_a, type_e = type(actual), type(expected)
+
+ if type_a ~= type_e then
+ return false
+
+ elseif (type_a == 'table') --[[and (type_e == 'table')]] then
+ for k, v in pairs(actual) do
+ if table_findkeyof(expected, v) == nil then
+ return false -- v not contained in expected
+ end
+ end
+ for k, v in pairs(expected) do
+ if table_findkeyof(actual, v) == nil then
+ return false -- v not contained in actual
+ end
+ end
+ return true
+
+ elseif actual ~= expected then
+ return false
+ end
+
+ return true
+end
+
+--[[
+This is a specialized metatable to help with the bookkeeping of recursions
+in _is_table_equals(). It provides an __index table that implements utility
+functions for easier management of the table. The "cached" method queries
+the state of a specific (actual,expected) pair; and the "store" method sets
+this state to the given value. The state of pairs not "seen" / visited is
+assumed to be `nil`.
+]]
+local _recursion_cache_MT = {
+ __index = {
+ -- Return the cached value for an (actual,expected) pair (or `nil`)
+ cached = function(t, actual, expected)
+ local subtable = t[actual] or {}
+ return subtable[expected]
+ end,
+
+ -- Store cached value for a specific (actual,expected) pair.
+ -- Returns the value, so it's easy to use for a "tailcall" (return ...).
+ store = function(t, actual, expected, value, asymmetric)
+ local subtable = t[actual]
+ if not subtable then
+ subtable = {}
+ t[actual] = subtable
+ end
+ subtable[expected] = value
+
+ -- Unless explicitly marked "asymmetric": Consider the recursion
+ -- on (expected,actual) to be equivalent to (actual,expected) by
+ -- default, and thus cache the value for both.
+ if not asymmetric then
+ t:store(expected, actual, value, true)
+ end
+
+ return value
+ end
+ }
+}
+
+local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlmostEqual)
+ --[[Returns true if both table are equal.
+
+ If argument marginForAlmostEqual is suppied, number comparison is done using alomstEqual instead
+ of strict equality.
+
+ cycleDetectTable is an internal argument used during recursion on tables.
+ ]]
+ --print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected)..
+ -- '\n , '..prettystr(cycleDetectTable)..'\n , '..prettystr(marginForAlmostEqual)..' )')
+
+ local type_a, type_e = type(actual), type(expected)
+
+ if type_a ~= type_e then
+ return false -- different types won't match
+ end
+
+ if type_a == 'number' then
+ if marginForAlmostEqual ~= nil then
+ return M.almostEquals(actual, expected, marginForAlmostEqual)
+ else
+ return actual == expected
+ end
+ elseif type_a ~= 'table' then
+ -- other types compare directly
+ return actual == expected
+ end
+
+ cycleDetectTable = cycleDetectTable or { actual={}, expected={} }
+ if cycleDetectTable.actual[ actual ] then
+ -- oh, we hit a cycle in actual
+ if cycleDetectTable.expected[ expected ] then
+ -- uh, we hit a cycle at the same time in expected
+ -- so the two tables have similar structure
+ return true
+ end
+
+ -- cycle was hit only in actual, the structure differs from expected
+ return false
+ end
+
+ if cycleDetectTable.expected[ expected ] then
+ -- no cycle in actual, but cycle in expected
+ -- the structure differ
+ return false
+ end
+
+ -- at this point, no table cycle detected, we are
+ -- seeing this table for the first time
+
+ -- mark the cycle detection
+ cycleDetectTable.actual[ actual ] = true
+ cycleDetectTable.expected[ expected ] = true
+
+
+ local actualKeysMatched = {}
+ for k, v in pairs(actual) do
+ actualKeysMatched[k] = true -- Keep track of matched keys
+ if not _is_table_equals(v, expected[k], cycleDetectTable, marginForAlmostEqual) then
+ -- table differs on this key
+ -- clear the cycle detection before returning
+ cycleDetectTable.actual[ actual ] = nil
+ cycleDetectTable.expected[ expected ] = nil
+ return false
+ end
+ end
+
+ for k, v in pairs(expected) do
+ if not actualKeysMatched[k] then
+ -- Found a key that we did not see in "actual" -> mismatch
+ -- clear the cycle detection before returning
+ cycleDetectTable.actual[ actual ] = nil
+ cycleDetectTable.expected[ expected ] = nil
+ return false
+ end
+ -- Otherwise actual[k] was already matched against v = expected[k].
+ end
+
+ -- all key match, we have a match !
+ cycleDetectTable.actual[ actual ] = nil
+ cycleDetectTable.expected[ expected ] = nil
+ return true
+end
+M.private._is_table_equals = _is_table_equals
+
+local function failure(main_msg, extra_msg_or_nil, level)
+ -- raise an error indicating a test failure
+ -- for error() compatibility we adjust "level" here (by +1), to report the
+ -- calling context
+ local msg
+ if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then
+ msg = extra_msg_or_nil .. '\n' .. main_msg
+ else
+ msg = main_msg
+ end
+ error(M.FAILURE_PREFIX .. msg, (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE)
+end
+
+local function is_table_equals(actual, expected, marginForAlmostEqual)
+ return _is_table_equals(actual, expected, nil, marginForAlmostEqual)
+end
+M.private.is_table_equals = is_table_equals
+
+local function fail_fmt(level, extra_msg_or_nil, ...)
+ -- failure with printf-style formatted message and given error level
+ failure(string.format(...), extra_msg_or_nil, (level or 1) + 1)
+end
+M.private.fail_fmt = fail_fmt
+
+local function error_fmt(level, ...)
+ -- printf-style error()
+ error(string.format(...), (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE)
+end
+M.private.error_fmt = error_fmt
+
+----------------------------------------------------------------
+--
+-- assertions
+--
+----------------------------------------------------------------
+
+local function errorMsgEquality(actual, expected, doDeepAnalysis, margin)
+ -- margin is supplied only for almost equal verification
+
+ if not M.ORDER_ACTUAL_EXPECTED then
+ expected, actual = actual, expected
+ end
+ if type(expected) == 'string' or type(expected) == 'table' then
+ local strExpected, strActual = prettystrPairs(expected, actual)
+ local result = string.format("expected: %s\nactual: %s", strExpected, strActual)
+ if margin then
+ result = result .. '\nwere not equal by the margin of: '..prettystr(margin)
+ end
+
+ -- extend with mismatch analysis if possible:
+ local success, mismatchResult
+ success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis, margin )
+ if success then
+ result = table.concat( { result, mismatchResult }, '\n' )
+ end
+ return result
+ end
+ return string.format("expected: %s, actual: %s",
+ prettystr(expected), prettystr(actual))
+end
+
+function M.assertError(f, ...)
+ -- assert that calling f with the arguments will raise an error
+ -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error
+ if pcall( f, ... ) then
+ failure( "Expected an error when calling function but no error generated", nil, 2 )
+ end
+end
+
+function M.fail( msg )
+ -- stops a test due to a failure
+ failure( msg, nil, 2 )
+end
+
+function M.failIf( cond, msg )
+ -- Fails a test with "msg" if condition is true
+ if cond then
+ failure( msg, nil, 2 )
+ end
+end
+
+function M.skip(msg)
+ -- skip a running test
+ error_fmt(2, M.SKIP_PREFIX .. msg)
+end
+
+function M.skipIf( cond, msg )
+ -- skip a running test if condition is met
+ if cond then
+ error_fmt(2, M.SKIP_PREFIX .. msg)
+ end
+end
+
+function M.runOnlyIf( cond, msg )
+ -- continue a running test if condition is met, else skip it
+ if not cond then
+ error_fmt(2, M.SKIP_PREFIX .. prettystr(msg))
+ end
+end
+
+function M.success()
+ -- stops a test with a success
+ error_fmt(2, M.SUCCESS_PREFIX)
+end
+
+function M.successIf( cond )
+ -- stops a test with a success if condition is met
+ if cond then
+ error_fmt(2, M.SUCCESS_PREFIX)
+ end
+end
+
+
+------------------------------------------------------------------
+-- Equality assertions
+------------------------------------------------------------------
+
+function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis)
+ if type(actual) == 'table' and type(expected) == 'table' then
+ if not is_table_equals(actual, expected) then
+ failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 )
+ end
+ elseif type(actual) ~= type(expected) then
+ failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 )
+ elseif actual ~= expected then
+ failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 )
+ end
+end
+
+function M.almostEquals( actual, expected, margin )
+ if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then
+ error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s',
+ prettystr(actual), prettystr(expected), prettystr(margin))
+ end
+ if margin < 0 then
+ error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin)
+ end
+ return math.abs(expected - actual) <= margin
+end
+
+function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil )
+ -- check that two floats are close by margin
+ margin = margin or M.EPS
+ if type(margin) ~= 'number' then
+ error_fmt(2, 'almostEquals: margin must be a number, not %s', prettystr(margin))
+ end
+
+ if type(actual) == 'table' and type(expected) == 'table' then
+ -- handle almost equals for table
+ if not is_table_equals(actual, expected, margin) then
+ failure( errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2 )
+ end
+ elseif type(actual) == 'number' and type(expected) == 'number' and type(margin) == 'number' then
+ if not M.almostEquals(actual, expected, margin) then
+ if not M.ORDER_ACTUAL_EXPECTED then
+ expected, actual = actual, expected
+ end
+ local delta = math.abs(actual - expected)
+ fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' ..
+ 'Actual: %s, expected: %s, delta %s above margin of %s',
+ actual, expected, delta, margin)
+ end
+ else
+ error_fmt(3, 'almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s',
+ prettystr(actual), prettystr(expected), prettystr(margin))
+ end
+end
+
+function M.assertNotEquals(actual, expected, extra_msg_or_nil)
+ if type(actual) ~= type(expected) then
+ return
+ end
+
+ if type(actual) == 'table' and type(expected) == 'table' then
+ if not is_table_equals(actual, expected) then
+ return
+ end
+ elseif actual ~= expected then
+ return
+ end
+ fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual))
+end
+
+function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil )
+ -- check that two floats are not close by margin
+ margin = margin or M.EPS
+ if M.almostEquals(actual, expected, margin) then
+ if not M.ORDER_ACTUAL_EXPECTED then
+ expected, actual = actual, expected
+ end
+ local delta = math.abs(actual - expected)
+ fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' ..
+ ', delta %s below margin of %s',
+ actual, expected, delta, margin)
+ end
+end
+
+function M.assertItemsEquals(actual, expected, extra_msg_or_nil)
+ -- checks that the items of table expected
+ -- are contained in table actual. Warning, this function
+ -- is at least O(n^2)
+ if not _is_table_items_equals(actual, expected ) then
+ expected, actual = prettystrPairs(expected, actual)
+ fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s',
+ expected, actual)
+ end
+end
+
+------------------------------------------------------------------
+-- String assertion
+------------------------------------------------------------------
+
+function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil )
+ -- this relies on lua string.find function
+ -- a string always contains the empty string
+ -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) )
+ -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) )
+ if not string.find(str, sub, 1, not isPattern) then
+ sub, str = prettystrPairs(sub, str, '\n')
+ fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s',
+ isPattern and 'pattern' or 'substring', sub, str)
+ end
+end
+
+function M.assertStrIContains( str, sub, extra_msg_or_nil )
+ -- this relies on lua string.find function
+ -- a string always contains the empty string
+ if not string.find(str:lower(), sub:lower(), 1, true) then
+ sub, str = prettystrPairs(sub, str, '\n')
+ fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s',
+ sub, str)
+ end
+end
+
+function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil )
+ -- this relies on lua string.find function
+ -- a string always contains the empty string
+ if string.find(str, sub, 1, not isPattern) then
+ sub, str = prettystrPairs(sub, str, '\n')
+ fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s',
+ isPattern and 'pattern' or 'substring', sub, str)
+ end
+end
+
+function M.assertNotStrIContains( str, sub, extra_msg_or_nil )
+ -- this relies on lua string.find function
+ -- a string always contains the empty string
+ if string.find(str:lower(), sub:lower(), 1, true) then
+ sub, str = prettystrPairs(sub, str, '\n')
+ fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s',
+ sub, str)
+ end
+end
+
+function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil )
+ -- Verify a full match for the string
+ if not strMatch( str, pattern, start, final ) then
+ pattern, str = prettystrPairs(pattern, str, '\n')
+ fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s',
+ pattern, str)
+ end
+end
+
+local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... )
+ local no_error, error_msg = pcall( func, ... )
+ if no_error then
+ failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 )
+ end
+ if type(expectedMsg) == "string" and type(error_msg) ~= "string" then
+ -- table are converted to string automatically
+ error_msg = tostring(error_msg)
+ end
+ local differ = false
+ if stripFileAndLine then
+ if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then
+ differ = true
+ end
+ else
+ if error_msg ~= expectedMsg then
+ local tr = type(error_msg)
+ local te = type(expectedMsg)
+ if te == 'table' then
+ if tr ~= 'table' then
+ differ = true
+ else
+ local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg)
+ if not ok then
+ differ = true
+ end
+ end
+ else
+ differ = true
+ end
+ end
+ end
+
+ if differ then
+ error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg)
+ fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n',
+ expectedMsg, error_msg)
+ end
+end
+
+function M.assertErrorMsgEquals( expectedMsg, func, ... )
+ -- assert that calling f with the arguments will raise an error
+ -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error
+ _assertErrorMsgEquals(false, expectedMsg, func, ...)
+end
+
+function M.assertErrorMsgContentEquals(expectedMsg, func, ...)
+ _assertErrorMsgEquals(true, expectedMsg, func, ...)
+end
+
+function M.assertErrorMsgContains( partialMsg, func, ... )
+ -- assert that calling f with the arguments will raise an error
+ -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error
+ local no_error, error_msg = pcall( func, ... )
+ if no_error then
+ failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 )
+ end
+ if type(error_msg) ~= "string" then
+ error_msg = tostring(error_msg)
+ end
+ if not string.find( error_msg, partialMsg, nil, true ) then
+ error_msg, partialMsg = prettystrPairs(error_msg, partialMsg)
+ fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n',
+ partialMsg, error_msg)
+ end
+end
+
+function M.assertErrorMsgMatches( expectedMsg, func, ... )
+ -- assert that calling f with the arguments will raise an error
+ -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error
+ local no_error, error_msg = pcall( func, ... )
+ if no_error then
+ failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 )
+ end
+ if type(error_msg) ~= "string" then
+ error_msg = tostring(error_msg)
+ end
+ if not strMatch( error_msg, expectedMsg ) then
+ expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg)
+ fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n',
+ expectedMsg, error_msg)
+ end
+end
+
+------------------------------------------------------------------
+-- Type assertions
+------------------------------------------------------------------
+
+function M.assertEvalToTrue(value, extra_msg_or_nil)
+ if not value then
+ failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertEvalToFalse(value, extra_msg_or_nil)
+ if value then
+ failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertIsTrue(value, extra_msg_or_nil)
+ if value ~= true then
+ failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertNotIsTrue(value, extra_msg_or_nil)
+ if value == true then
+ failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertIsFalse(value, extra_msg_or_nil)
+ if value ~= false then
+ failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertNotIsFalse(value, extra_msg_or_nil)
+ if value == false then
+ failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertIsNil(value, extra_msg_or_nil)
+ if value ~= nil then
+ failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertNotIsNil(value, extra_msg_or_nil)
+ if value == nil then
+ failure("expected: not nil, actual: nil", extra_msg_or_nil, 2)
+ end
+end
+
+--[[
+Add type assertion functions to the module table M. Each of these functions
+takes a single parameter "value", and checks that its Lua type matches the
+expected string (derived from the function name):
+
+M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx"
+]]
+for _, funcName in ipairs(
+ {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean',
+ 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'}
+) do
+ local typeExpected = funcName:match("^assertIs([A-Z]%a*)$")
+ -- Lua type() always returns lowercase, also make sure the match() succeeded
+ typeExpected = typeExpected and typeExpected:lower()
+ or error("bad function name '"..funcName.."' for type assertion")
+
+ M[funcName] = function(value, extra_msg_or_nil)
+ if type(value) ~= typeExpected then
+ if type(value) == 'nil' then
+ fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil',
+ typeExpected, type(value), prettystrPairs(value))
+ else
+ fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s',
+ typeExpected, type(value), prettystrPairs(value))
+ end
+ end
+ end
+end
+
+--[[
+Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility)
+M.isXxx(value) -> returns true if type(value) conforms to "xxx"
+]]
+for _, typeExpected in ipairs(
+ {'Number', 'String', 'Table', 'Boolean',
+ 'Function', 'Userdata', 'Thread', 'Nil' }
+) do
+ local typeExpectedLower = typeExpected:lower()
+ local isType = function(value)
+ return (type(value) == typeExpectedLower)
+ end
+ M['is'..typeExpected] = isType
+ M['is_'..typeExpectedLower] = isType
+end
+
+--[[
+Add non-type assertion functions to the module table M. Each of these functions
+takes a single parameter "value", and checks that its Lua type differs from the
+expected string (derived from the function name):
+
+M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx"
+]]
+for _, funcName in ipairs(
+ {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean',
+ 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'}
+) do
+ local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$")
+ -- Lua type() always returns lowercase, also make sure the match() succeeded
+ typeUnexpected = typeUnexpected and typeUnexpected:lower()
+ or error("bad function name '"..funcName.."' for type assertion")
+
+ M[funcName] = function(value, extra_msg_or_nil)
+ if type(value) == typeUnexpected then
+ fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s',
+ typeUnexpected, prettystrPairs(value))
+ end
+ end
+end
+
+function M.assertIs(actual, expected, extra_msg_or_nil)
+ if actual ~= expected then
+ if not M.ORDER_ACTUAL_EXPECTED then
+ actual, expected = expected, actual
+ end
+ local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG
+ M.PRINT_TABLE_REF_IN_ERROR_MSG = true
+ expected, actual = prettystrPairs(expected, actual, '\n', '')
+ M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg
+ fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s',
+ expected, actual)
+ end
+end
+
+function M.assertNotIs(actual, expected, extra_msg_or_nil)
+ if actual == expected then
+ local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG
+ M.PRINT_TABLE_REF_IN_ERROR_MSG = true
+ local s_expected
+ if not M.ORDER_ACTUAL_EXPECTED then
+ s_expected = prettystrPairs(actual)
+ else
+ s_expected = prettystrPairs(expected)
+ end
+ M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg
+ fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected )
+ end
+end
+
+
+------------------------------------------------------------------
+-- Scientific assertions
+------------------------------------------------------------------
+
+
+function M.assertIsNaN(value, extra_msg_or_nil)
+ if type(value) ~= "number" or value == value then
+ failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertNotIsNaN(value, extra_msg_or_nil)
+ if type(value) == "number" and value ~= value then
+ failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertIsInf(value, extra_msg_or_nil)
+ if type(value) ~= "number" or math.abs(value) ~= math.huge then
+ failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertIsPlusInf(value, extra_msg_or_nil)
+ if type(value) ~= "number" or value ~= math.huge then
+ failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertIsMinusInf(value, extra_msg_or_nil)
+ if type(value) ~= "number" or value ~= -math.huge then
+ failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertNotIsPlusInf(value, extra_msg_or_nil)
+ if type(value) == "number" and value == math.huge then
+ failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertNotIsMinusInf(value, extra_msg_or_nil)
+ if type(value) == "number" and value == -math.huge then
+ failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertNotIsInf(value, extra_msg_or_nil)
+ if type(value) == "number" and math.abs(value) == math.huge then
+ failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertIsPlusZero(value, extra_msg_or_nil)
+ if type(value) ~= 'number' or value ~= 0 then
+ failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ else if (1/value == -math.huge) then
+ -- more precise error diagnosis
+ failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2)
+ else if (1/value ~= math.huge) then
+ -- strange, case should have already been covered
+ failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+ end
+ end
+end
+
+function M.assertIsMinusZero(value, extra_msg_or_nil)
+ if type(value) ~= 'number' or value ~= 0 then
+ failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ else if (1/value == math.huge) then
+ -- more precise error diagnosis
+ failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2)
+ else if (1/value ~= -math.huge) then
+ -- strange, case should have already been covered
+ failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+ end
+ end
+ end
+end
+
+function M.assertNotIsPlusZero(value, extra_msg_or_nil)
+ if type(value) == 'number' and (1/value == math.huge) then
+ failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertNotIsMinusZero(value, extra_msg_or_nil)
+ if type(value) == 'number' and (1/value == -math.huge) then
+ failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2)
+ end
+end
+
+function M.assertTableContains(t, expected, extra_msg_or_nil)
+ -- checks that table t contains the expected element
+ if table_findkeyof(t, expected) == nil then
+ t, expected = prettystrPairs(t, expected)
+ fail_fmt(2, extra_msg_or_nil, 'Table %s does NOT contain the expected element %s',
+ t, expected)
+ end
+end
+
+function M.assertNotTableContains(t, expected, extra_msg_or_nil)
+ -- checks that table t doesn't contain the expected element
+ local k = table_findkeyof(t, expected)
+ if k ~= nil then
+ t, expected = prettystrPairs(t, expected)
+ fail_fmt(2, extra_msg_or_nil, 'Table %s DOES contain the unwanted element %s (at key %s)',
+ t, expected, prettystr(k))
+ end
+end
+
+----------------------------------------------------------------
+-- Compatibility layer
+----------------------------------------------------------------
+
+-- for compatibility with LuaUnit v2.x
+function M.wrapFunctions()
+ -- In LuaUnit version <= 2.1 , this function was necessary to include
+ -- a test function inside the global test suite. Nowadays, the functions
+ -- are simply run directly as part of the test discovery process.
+ -- so just do nothing !
+ io.stderr:write[[Use of WrapFunctions() is no longer needed.
+Just prefix your test function names with "test" or "Test" and they
+will be picked up and run by LuaUnit.
+]]
+end
+
+local list_of_funcs = {
+ -- { official function name , alias }
+
+ -- general assertions
+ { 'assertEquals' , 'assert_equals' },
+ { 'assertItemsEquals' , 'assert_items_equals' },
+ { 'assertNotEquals' , 'assert_not_equals' },
+ { 'assertAlmostEquals' , 'assert_almost_equals' },
+ { 'assertNotAlmostEquals' , 'assert_not_almost_equals' },
+ { 'assertEvalToTrue' , 'assert_eval_to_true' },
+ { 'assertEvalToFalse' , 'assert_eval_to_false' },
+ { 'assertStrContains' , 'assert_str_contains' },
+ { 'assertStrIContains' , 'assert_str_icontains' },
+ { 'assertNotStrContains' , 'assert_not_str_contains' },
+ { 'assertNotStrIContains' , 'assert_not_str_icontains' },
+ { 'assertStrMatches' , 'assert_str_matches' },
+ { 'assertError' , 'assert_error' },
+ { 'assertErrorMsgEquals' , 'assert_error_msg_equals' },
+ { 'assertErrorMsgContains' , 'assert_error_msg_contains' },
+ { 'assertErrorMsgMatches' , 'assert_error_msg_matches' },
+ { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' },
+ { 'assertIs' , 'assert_is' },
+ { 'assertNotIs' , 'assert_not_is' },
+ { 'assertTableContains' , 'assert_table_contains' },
+ { 'assertNotTableContains' , 'assert_not_table_contains' },
+ { 'wrapFunctions' , 'WrapFunctions' },
+ { 'wrapFunctions' , 'wrap_functions' },
+
+ -- type assertions: assertIsXXX -> assert_is_xxx
+ { 'assertIsNumber' , 'assert_is_number' },
+ { 'assertIsString' , 'assert_is_string' },
+ { 'assertIsTable' , 'assert_is_table' },
+ { 'assertIsBoolean' , 'assert_is_boolean' },
+ { 'assertIsNil' , 'assert_is_nil' },
+ { 'assertIsTrue' , 'assert_is_true' },
+ { 'assertIsFalse' , 'assert_is_false' },
+ { 'assertIsNaN' , 'assert_is_nan' },
+ { 'assertIsInf' , 'assert_is_inf' },
+ { 'assertIsPlusInf' , 'assert_is_plus_inf' },
+ { 'assertIsMinusInf' , 'assert_is_minus_inf' },
+ { 'assertIsPlusZero' , 'assert_is_plus_zero' },
+ { 'assertIsMinusZero' , 'assert_is_minus_zero' },
+ { 'assertIsFunction' , 'assert_is_function' },
+ { 'assertIsThread' , 'assert_is_thread' },
+ { 'assertIsUserdata' , 'assert_is_userdata' },
+
+ -- type assertions: assertIsXXX -> assertXxx
+ { 'assertIsNumber' , 'assertNumber' },
+ { 'assertIsString' , 'assertString' },
+ { 'assertIsTable' , 'assertTable' },
+ { 'assertIsBoolean' , 'assertBoolean' },
+ { 'assertIsNil' , 'assertNil' },
+ { 'assertIsTrue' , 'assertTrue' },
+ { 'assertIsFalse' , 'assertFalse' },
+ { 'assertIsNaN' , 'assertNaN' },
+ { 'assertIsInf' , 'assertInf' },
+ { 'assertIsPlusInf' , 'assertPlusInf' },
+ { 'assertIsMinusInf' , 'assertMinusInf' },
+ { 'assertIsPlusZero' , 'assertPlusZero' },
+ { 'assertIsMinusZero' , 'assertMinusZero'},
+ { 'assertIsFunction' , 'assertFunction' },
+ { 'assertIsThread' , 'assertThread' },
+ { 'assertIsUserdata' , 'assertUserdata' },
+
+ -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat)
+ { 'assertIsNumber' , 'assert_number' },
+ { 'assertIsString' , 'assert_string' },
+ { 'assertIsTable' , 'assert_table' },
+ { 'assertIsBoolean' , 'assert_boolean' },
+ { 'assertIsNil' , 'assert_nil' },
+ { 'assertIsTrue' , 'assert_true' },
+ { 'assertIsFalse' , 'assert_false' },
+ { 'assertIsNaN' , 'assert_nan' },
+ { 'assertIsInf' , 'assert_inf' },
+ { 'assertIsPlusInf' , 'assert_plus_inf' },
+ { 'assertIsMinusInf' , 'assert_minus_inf' },
+ { 'assertIsPlusZero' , 'assert_plus_zero' },
+ { 'assertIsMinusZero' , 'assert_minus_zero' },
+ { 'assertIsFunction' , 'assert_function' },
+ { 'assertIsThread' , 'assert_thread' },
+ { 'assertIsUserdata' , 'assert_userdata' },
+
+ -- type assertions: assertNotIsXXX -> assert_not_is_xxx
+ { 'assertNotIsNumber' , 'assert_not_is_number' },
+ { 'assertNotIsString' , 'assert_not_is_string' },
+ { 'assertNotIsTable' , 'assert_not_is_table' },
+ { 'assertNotIsBoolean' , 'assert_not_is_boolean' },
+ { 'assertNotIsNil' , 'assert_not_is_nil' },
+ { 'assertNotIsTrue' , 'assert_not_is_true' },
+ { 'assertNotIsFalse' , 'assert_not_is_false' },
+ { 'assertNotIsNaN' , 'assert_not_is_nan' },
+ { 'assertNotIsInf' , 'assert_not_is_inf' },
+ { 'assertNotIsPlusInf' , 'assert_not_plus_inf' },
+ { 'assertNotIsMinusInf' , 'assert_not_minus_inf' },
+ { 'assertNotIsPlusZero' , 'assert_not_plus_zero' },
+ { 'assertNotIsMinusZero' , 'assert_not_minus_zero' },
+ { 'assertNotIsFunction' , 'assert_not_is_function' },
+ { 'assertNotIsThread' , 'assert_not_is_thread' },
+ { 'assertNotIsUserdata' , 'assert_not_is_userdata' },
+
+ -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat)
+ { 'assertNotIsNumber' , 'assertNotNumber' },
+ { 'assertNotIsString' , 'assertNotString' },
+ { 'assertNotIsTable' , 'assertNotTable' },
+ { 'assertNotIsBoolean' , 'assertNotBoolean' },
+ { 'assertNotIsNil' , 'assertNotNil' },
+ { 'assertNotIsTrue' , 'assertNotTrue' },
+ { 'assertNotIsFalse' , 'assertNotFalse' },
+ { 'assertNotIsNaN' , 'assertNotNaN' },
+ { 'assertNotIsInf' , 'assertNotInf' },
+ { 'assertNotIsPlusInf' , 'assertNotPlusInf' },
+ { 'assertNotIsMinusInf' , 'assertNotMinusInf' },
+ { 'assertNotIsPlusZero' , 'assertNotPlusZero' },
+ { 'assertNotIsMinusZero' , 'assertNotMinusZero' },
+ { 'assertNotIsFunction' , 'assertNotFunction' },
+ { 'assertNotIsThread' , 'assertNotThread' },
+ { 'assertNotIsUserdata' , 'assertNotUserdata' },
+
+ -- type assertions: assertNotIsXXX -> assert_not_xxx
+ { 'assertNotIsNumber' , 'assert_not_number' },
+ { 'assertNotIsString' , 'assert_not_string' },
+ { 'assertNotIsTable' , 'assert_not_table' },
+ { 'assertNotIsBoolean' , 'assert_not_boolean' },
+ { 'assertNotIsNil' , 'assert_not_nil' },
+ { 'assertNotIsTrue' , 'assert_not_true' },
+ { 'assertNotIsFalse' , 'assert_not_false' },
+ { 'assertNotIsNaN' , 'assert_not_nan' },
+ { 'assertNotIsInf' , 'assert_not_inf' },
+ { 'assertNotIsPlusInf' , 'assert_not_plus_inf' },
+ { 'assertNotIsMinusInf' , 'assert_not_minus_inf' },
+ { 'assertNotIsPlusZero' , 'assert_not_plus_zero' },
+ { 'assertNotIsMinusZero' , 'assert_not_minus_zero' },
+ { 'assertNotIsFunction' , 'assert_not_function' },
+ { 'assertNotIsThread' , 'assert_not_thread' },
+ { 'assertNotIsUserdata' , 'assert_not_userdata' },
+
+ -- all assertions with Coroutine duplicate Thread assertions
+ { 'assertIsThread' , 'assertIsCoroutine' },
+ { 'assertIsThread' , 'assertCoroutine' },
+ { 'assertIsThread' , 'assert_is_coroutine' },
+ { 'assertIsThread' , 'assert_coroutine' },
+ { 'assertNotIsThread' , 'assertNotIsCoroutine' },
+ { 'assertNotIsThread' , 'assertNotCoroutine' },
+ { 'assertNotIsThread' , 'assert_not_is_coroutine' },
+ { 'assertNotIsThread' , 'assert_not_coroutine' },
+}
+
+-- Create all aliases in M
+for _,v in ipairs( list_of_funcs ) do
+ local funcname, alias = v[1], v[2]
+ M[alias] = M[funcname]
+
+ if EXPORT_ASSERT_TO_GLOBALS then
+ _G[funcname] = M[funcname]
+ _G[alias] = M[funcname]
+ end
+end
+
+----------------------------------------------------------------
+--
+-- Outputters
+--
+----------------------------------------------------------------
+
+-- A common "base" class for outputters
+-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html
+
+local genericOutput = { __class__ = 'genericOutput' } -- class
+local genericOutput_MT = { __index = genericOutput } -- metatable
+M.genericOutput = genericOutput -- publish, so that custom classes may derive from it
+
+function genericOutput.new(runner, default_verbosity)
+ -- runner is the "parent" object controlling the output, usually a LuaUnit instance
+ local t = { runner = runner }
+ if runner then
+ t.result = runner.result
+ t.verbosity = runner.verbosity or default_verbosity
+ t.fname = runner.fname
+ else
+ t.verbosity = default_verbosity
+ end
+ return setmetatable( t, genericOutput_MT)
+end
+
+-- abstract ("empty") methods
+function genericOutput:startSuite()
+ -- Called once, when the suite is started
+end
+
+function genericOutput:startClass(className)
+ -- Called each time a new test class is started
+end
+
+function genericOutput:startTest(testName)
+ -- called each time a new test is started, right before the setUp()
+ -- the current test status node is already created and available in: self.result.currentNode
+end
+
+function genericOutput:updateStatus(node)
+ -- called with status failed or error as soon as the error/failure is encountered
+ -- this method is NOT called for a successful test because a test is marked as successful by default
+ -- and does not need to be updated
+end
+
+function genericOutput:endTest(node)
+ -- called when the test is finished, after the tearDown() method
+end
+
+function genericOutput:endClass()
+ -- called when executing the class is finished, before moving on to the next class of at the end of the test execution
+end
+
+function genericOutput:endSuite()
+ -- called at the end of the test suite execution
+end
+
+
+----------------------------------------------------------------
+-- class TapOutput
+----------------------------------------------------------------
+
+local TapOutput = genericOutput.new() -- derived class
+local TapOutput_MT = { __index = TapOutput } -- metatable
+TapOutput.__class__ = 'TapOutput'
+
+ -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html
+
+ function TapOutput.new(runner)
+ local t = genericOutput.new(runner, M.VERBOSITY_LOW)
+ return setmetatable( t, TapOutput_MT)
+ end
+ function TapOutput:startSuite()
+ print("1.."..self.result.selectedCount)
+ print('# Started on '..self.result.startDate)
+ end
+ function TapOutput:startClass(className)
+ if className ~= '[TestFunctions]' then
+ print('# Starting class: '..className)
+ end
+ end
+
+ function TapOutput:updateStatus( node )
+ if node:isSkipped() then
+ io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n" )
+ return
+ end
+
+ io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n")
+ if self.verbosity > M.VERBOSITY_LOW then
+ print( prefixString( '# ', node.msg ) )
+ end
+ if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then
+ print( prefixString( '# ', node.stackTrace ) )
+ end
+ end
+
+ function TapOutput:endTest( node )
+ if node:isSuccess() then
+ io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n")
+ end
+ end
+
+ function TapOutput:endSuite()
+ print( '# '..M.LuaUnit.statusLine( self.result ) )
+ return self.result.notSuccessCount
+ end
+
+
+-- class TapOutput end
+
+----------------------------------------------------------------
+-- class JUnitOutput
+----------------------------------------------------------------
+
+-- See directory junitxml for more information about the junit format
+local JUnitOutput = genericOutput.new() -- derived class
+local JUnitOutput_MT = { __index = JUnitOutput } -- metatable
+JUnitOutput.__class__ = 'JUnitOutput'
+
+ function JUnitOutput.new(runner)
+ local t = genericOutput.new(runner, M.VERBOSITY_LOW)
+ t.testList = {}
+ return setmetatable( t, JUnitOutput_MT )
+ end
+
+ function JUnitOutput:startSuite()
+ -- open xml file early to deal with errors
+ if self.fname == nil then
+ error('With Junit, an output filename must be supplied with --name!')
+ end
+ if string.sub(self.fname,-4) ~= '.xml' then
+ self.fname = self.fname..'.xml'
+ end
+ self.fd = io.open(self.fname, "w")
+ if self.fd == nil then
+ error("Could not open file for writing: "..self.fname)
+ end
+
+ print('# XML output to '..self.fname)
+ print('# Started on '..self.result.startDate)
+ end
+ function JUnitOutput:startClass(className)
+ if className ~= '[TestFunctions]' then
+ print('# Starting class: '..className)
+ end
+ end
+ function JUnitOutput:startTest(testName)
+ print('# Starting test: '..testName)
+ end
+
+ function JUnitOutput:updateStatus( node )
+ if node:isFailure() then
+ print( '# Failure: ' .. prefixString( '# ', node.msg ):sub(4, nil) )
+ -- print('# ' .. node.stackTrace)
+ elseif node:isError() then
+ print( '# Error: ' .. prefixString( '# ' , node.msg ):sub(4, nil) )
+ -- print('# ' .. node.stackTrace)
+ end
+ end
+
+ function JUnitOutput:endSuite()
+ print( '# '..M.LuaUnit.statusLine(self.result))
+
+ -- XML file writing
+ self.fd:write('<?xml version="1.0" encoding="UTF-8" ?>\n')
+ self.fd:write('<testsuites>\n')
+ self.fd:write(string.format(
+ ' <testsuite name="LuaUnit" id="00001" package="" hostname="localhost" tests="%d" timestamp="%s" time="%0.3f" errors="%d" failures="%d" skipped="%d">\n',
+ self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount ))
+ self.fd:write(" <properties>\n")
+ self.fd:write(string.format(' <property name="Lua Version" value="%s"/>\n', _VERSION ) )
+ self.fd:write(string.format(' <property name="LuaUnit Version" value="%s"/>\n', M.VERSION) )
+ -- XXX please include system name and version if possible
+ self.fd:write(" </properties>\n")
+
+ for i,node in ipairs(self.result.allTests) do
+ self.fd:write(string.format(' <testcase classname="%s" name="%s" time="%0.3f">\n',
+ node.className, node.testName, node.duration ) )
+ if node:isNotSuccess() then
+ self.fd:write(node:statusXML())
+ end
+ self.fd:write(' </testcase>\n')
+ end
+
+ -- Next two lines are needed to validate junit ANT xsd, but really not useful in general:
+ self.fd:write(' <system-out/>\n')
+ self.fd:write(' <system-err/>\n')
+
+ self.fd:write(' </testsuite>\n')
+ self.fd:write('</testsuites>\n')
+ self.fd:close()
+ return self.result.notSuccessCount
+ end
+
+
+-- class TapOutput end
+
+----------------------------------------------------------------
+-- class TextOutput
+----------------------------------------------------------------
+
+--[[ Example of other unit-tests suite text output
+
+-- Python Non verbose:
+
+For each test: . or F or E
+
+If some failed tests:
+ ==============
+ ERROR / FAILURE: TestName (testfile.testclass)
+ ---------
+ Stack trace
+
+
+then --------------
+then "Ran x tests in 0.000s"
+then OK or FAILED (failures=1, error=1)
+
+-- Python Verbose:
+testname (filename.classname) ... ok
+testname (filename.classname) ... FAIL
+testname (filename.classname) ... ERROR
+
+then --------------
+then "Ran x tests in 0.000s"
+then OK or FAILED (failures=1, error=1)
+
+-- Ruby:
+Started
+ .
+ Finished in 0.002695 seconds.
+
+ 1 tests, 2 assertions, 0 failures, 0 errors
+
+-- Ruby:
+>> ruby tc_simple_number2.rb
+Loaded suite tc_simple_number2
+Started
+F..
+Finished in 0.038617 seconds.
+
+ 1) Failure:
+test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]:
+Adding doesn't work.
+<3> expected but was
+<4>.
+
+3 tests, 4 assertions, 1 failures, 0 errors
+
+-- Java Junit
+.......F.
+Time: 0,003
+There was 1 failure:
+1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError
+ at junit.samples.VectorTest.testCapacity(VectorTest.java:87)
+ at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+ at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
+ at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
+
+FAILURES!!!
+Tests run: 8, Failures: 1, Errors: 0
+
+
+-- Maven
+
+# mvn test
+-------------------------------------------------------
+ T E S T S
+-------------------------------------------------------
+Running math.AdditionTest
+Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed:
+0.03 sec <<< FAILURE!
+
+Results :
+
+Failed tests:
+ testLireSymbole(math.AdditionTest)
+
+Tests run: 2, Failures: 1, Errors: 0, Skipped: 0
+
+
+-- LuaUnit
+---- non verbose
+* display . or F or E when running tests
+---- verbose
+* display test name + ok/fail
+----
+* blank line
+* number) ERROR or FAILURE: TestName
+ Stack trace
+* blank line
+* number) ERROR or FAILURE: TestName
+ Stack trace
+
+then --------------
+then "Ran x tests in 0.000s (%d not selected, %d skipped)"
+then OK or FAILED (failures=1, error=1)
+
+
+]]
+
+local TextOutput = genericOutput.new() -- derived class
+local TextOutput_MT = { __index = TextOutput } -- metatable
+TextOutput.__class__ = 'TextOutput'
+
+ function TextOutput.new(runner)
+ local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT)
+ t.errorList = {}
+ return setmetatable( t, TextOutput_MT )
+ end
+
+ function TextOutput:startSuite()
+ if self.verbosity > M.VERBOSITY_DEFAULT then
+ print( 'Started on '.. self.result.startDate )
+ end
+ end
+
+ function TextOutput:startTest(testName)
+ if self.verbosity > M.VERBOSITY_DEFAULT then
+ io.stdout:write( " ", self.result.currentNode.testName, " ... " )
+ end
+ end
+
+ function TextOutput:endTest( node )
+ if node:isSuccess() then
+ if self.verbosity > M.VERBOSITY_DEFAULT then
+ io.stdout:write("Ok\n")
+ else
+ io.stdout:write(".")
+ io.stdout:flush()
+ end
+ else
+ if self.verbosity > M.VERBOSITY_DEFAULT then
+ print( node.status )
+ print( node.msg )
+ --[[
+ -- find out when to do this:
+ if self.verbosity > M.VERBOSITY_DEFAULT then
+ print( node.stackTrace )
+ end
+ ]]
+ else
+ -- write only the first character of status E, F or S
+ io.stdout:write(string.sub(node.status, 1, 1))
+ io.stdout:flush()
+ end
+ end
+ end
+
+ function TextOutput:displayOneFailedTest( index, fail )
+ print(index..") "..fail.testName )
+ print( fail.msg )
+ print( fail.stackTrace )
+ print()
+ end
+
+ function TextOutput:displayErroredTests()
+ if #self.result.errorTests ~= 0 then
+ print("Tests with errors:")
+ print("------------------")
+ for i, v in ipairs(self.result.errorTests) do
+ self:displayOneFailedTest(i, v)
+ end
+ end
+ end
+
+ function TextOutput:displayFailedTests()
+ if #self.result.failedTests ~= 0 then
+ print("Failed tests:")
+ print("-------------")
+ for i, v in ipairs(self.result.failedTests) do
+ self:displayOneFailedTest(i, v)
+ end
+ end
+ end
+
+ function TextOutput:endSuite()
+ if self.verbosity > M.VERBOSITY_DEFAULT then
+ print("=========================================================")
+ else
+ print()
+ end
+ self:displayErroredTests()
+ self:displayFailedTests()
+ print( M.LuaUnit.statusLine( self.result ) )
+ if self.result.notSuccessCount == 0 then
+ print('OK')
+ end
+ end
+
+-- class TextOutput end
+
+
+----------------------------------------------------------------
+-- class NilOutput
+----------------------------------------------------------------
+
+local function nopCallable()
+ --print(42)
+ return nopCallable
+end
+
+local NilOutput = { __class__ = 'NilOuptut' } -- class
+local NilOutput_MT = { __index = nopCallable } -- metatable
+
+function NilOutput.new(runner)
+ return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT )
+end
+
+----------------------------------------------------------------
+--
+-- class LuaUnit
+--
+----------------------------------------------------------------
+
+M.LuaUnit = {
+ outputType = TextOutput,
+ verbosity = M.VERBOSITY_DEFAULT,
+ __class__ = 'LuaUnit',
+ instances = {}
+}
+local LuaUnit_MT = { __index = M.LuaUnit }
+
+if EXPORT_ASSERT_TO_GLOBALS then
+ LuaUnit = M.LuaUnit
+end
+
+ function M.LuaUnit.new()
+ local newInstance = setmetatable( {}, LuaUnit_MT )
+ return newInstance
+ end
+
+ -----------------[[ Utility methods ]]---------------------
+
+ function M.LuaUnit.asFunction(aObject)
+ -- return "aObject" if it is a function, and nil otherwise
+ if 'function' == type(aObject) then
+ return aObject
+ end
+ end
+
+ function M.LuaUnit.splitClassMethod(someName)
+ --[[
+ Return a pair of className, methodName strings for a name in the form
+ "class.method". If no class part (or separator) is found, will return
+ nil, someName instead (the latter being unchanged).
+
+ This convention thus also replaces the older isClassMethod() test:
+ You just have to check for a non-nil className (return) value.
+ ]]
+ local separator = string.find(someName, '.', 1, true)
+ if separator then
+ return someName:sub(1, separator - 1), someName:sub(separator + 1)
+ end
+ return nil, someName
+ end
+
+ function M.LuaUnit.isMethodTestName( s )
+ -- return true is the name matches the name of a test method
+ -- default rule is that is starts with 'Test' or with 'test'
+ return string.sub(s, 1, 4):lower() == 'test'
+ end
+
+ function M.LuaUnit.isTestName( s )
+ -- return true is the name matches the name of a test
+ -- default rule is that is starts with 'Test' or with 'test'
+ return string.sub(s, 1, 4):lower() == 'test'
+ end
+
+ function M.LuaUnit.collectTests()
+ -- return a list of all test names in the global namespace
+ -- that match LuaUnit.isTestName
+
+ local testNames = {}
+ for k, _ in pairs(_G) do
+ if type(k) == "string" and M.LuaUnit.isTestName( k ) then
+ table.insert( testNames , k )
+ end
+ end
+ table.sort( testNames )
+ return testNames
+ end
+
+ function M.LuaUnit.parseCmdLine( cmdLine )
+ -- parse the command line
+ -- Supported command line parameters:
+ -- --verbose, -v: increase verbosity
+ -- --quiet, -q: silence output
+ -- --error, -e: treat errors as fatal (quit program)
+ -- --output, -o, + name: select output type
+ -- --pattern, -p, + pattern: run test matching pattern, may be repeated
+ -- --exclude, -x, + pattern: run test not matching pattern, may be repeated
+ -- --shuffle, -s, : shuffle tests before reunning them
+ -- --name, -n, + fname: name of output file for junit, default to stdout
+ -- --repeat, -r, + num: number of times to execute each test
+ -- [testnames, ...]: run selected test names
+ --
+ -- Returns a table with the following fields:
+ -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE
+ -- output: nil, 'tap', 'junit', 'text', 'nil'
+ -- testNames: nil or a list of test names to run
+ -- exeRepeat: num or 1
+ -- pattern: nil or a list of patterns
+ -- exclude: nil or a list of patterns
+
+ local result, state = {}, nil
+ local SET_OUTPUT = 1
+ local SET_PATTERN = 2
+ local SET_EXCLUDE = 3
+ local SET_FNAME = 4
+ local SET_REPEAT = 5
+
+ if cmdLine == nil then
+ return result
+ end
+
+ local function parseOption( option )
+ if option == '--help' or option == '-h' then
+ result['help'] = true
+ return
+ elseif option == '--version' then
+ result['version'] = true
+ return
+ elseif option == '--verbose' or option == '-v' then
+ result['verbosity'] = M.VERBOSITY_VERBOSE
+ return
+ elseif option == '--quiet' or option == '-q' then
+ result['verbosity'] = M.VERBOSITY_QUIET
+ return
+ elseif option == '--error' or option == '-e' then
+ result['quitOnError'] = true
+ return
+ elseif option == '--failure' or option == '-f' then
+ result['quitOnFailure'] = true
+ return
+ elseif option == '--shuffle' or option == '-s' then
+ result['shuffle'] = true
+ return
+ elseif option == '--output' or option == '-o' then
+ state = SET_OUTPUT
+ return state
+ elseif option == '--name' or option == '-n' then
+ state = SET_FNAME
+ return state
+ elseif option == '--repeat' or option == '-r' then
+ state = SET_REPEAT
+ return state
+ elseif option == '--pattern' or option == '-p' then
+ state = SET_PATTERN
+ return state
+ elseif option == '--exclude' or option == '-x' then
+ state = SET_EXCLUDE
+ return state
+ end
+ error('Unknown option: '..option,3)
+ end
+
+ local function setArg( cmdArg, state )
+ if state == SET_OUTPUT then
+ result['output'] = cmdArg
+ return
+ elseif state == SET_FNAME then
+ result['fname'] = cmdArg
+ return
+ elseif state == SET_REPEAT then
+ result['exeRepeat'] = tonumber(cmdArg)
+ or error('Malformed -r argument: '..cmdArg)
+ return
+ elseif state == SET_PATTERN then
+ if result['pattern'] then
+ table.insert( result['pattern'], cmdArg )
+ else
+ result['pattern'] = { cmdArg }
+ end
+ return
+ elseif state == SET_EXCLUDE then
+ local notArg = '!'..cmdArg
+ if result['pattern'] then
+ table.insert( result['pattern'], notArg )
+ else
+ result['pattern'] = { notArg }
+ end
+ return
+ end
+ error('Unknown parse state: '.. state)
+ end
+
+
+ for i, cmdArg in ipairs(cmdLine) do
+ if state ~= nil then
+ setArg( cmdArg, state, result )
+ state = nil
+ else
+ if cmdArg:sub(1,1) == '-' then
+ state = parseOption( cmdArg )
+ else
+ if result['testNames'] then
+ table.insert( result['testNames'], cmdArg )
+ else
+ result['testNames'] = { cmdArg }
+ end
+ end
+ end
+ end
+
+ if result['help'] then
+ M.LuaUnit.help()
+ end
+
+ if result['version'] then
+ M.LuaUnit.version()
+ end
+
+ if state ~= nil then
+ error('Missing argument after '..cmdLine[ #cmdLine ],2 )
+ end
+
+ return result
+ end
+
+ function M.LuaUnit.help()
+ print(M.USAGE)
+ os.exit(0)
+ end
+
+ function M.LuaUnit.version()
+ print('LuaUnit v'..M.VERSION..' by Philippe Fremy <phil@freehackers.org>')
+ os.exit(0)
+ end
+
+----------------------------------------------------------------
+-- class NodeStatus
+----------------------------------------------------------------
+
+ local NodeStatus = { __class__ = 'NodeStatus' } -- class
+ local NodeStatus_MT = { __index = NodeStatus } -- metatable
+ M.NodeStatus = NodeStatus
+
+ -- values of status
+ NodeStatus.SUCCESS = 'SUCCESS'
+ NodeStatus.SKIP = 'SKIP'
+ NodeStatus.FAIL = 'FAIL'
+ NodeStatus.ERROR = 'ERROR'
+
+ function NodeStatus.new( number, testName, className )
+ -- default constructor, test are PASS by default
+ local t = { number = number, testName = testName, className = className }
+ setmetatable( t, NodeStatus_MT )
+ t:success()
+ return t
+ end
+
+ function NodeStatus:success()
+ self.status = self.SUCCESS
+ -- useless because lua does this for us, but it helps me remembering the relevant field names
+ self.msg = nil
+ self.stackTrace = nil
+ end
+
+ function NodeStatus:skip(msg)
+ self.status = self.SKIP
+ self.msg = msg
+ self.stackTrace = nil
+ end
+
+ function NodeStatus:fail(msg, stackTrace)
+ self.status = self.FAIL
+ self.msg = msg
+ self.stackTrace = stackTrace
+ end
+
+ function NodeStatus:error(msg, stackTrace)
+ self.status = self.ERROR
+ self.msg = msg
+ self.stackTrace = stackTrace
+ end
+
+ function NodeStatus:isSuccess()
+ return self.status == NodeStatus.SUCCESS
+ end
+
+ function NodeStatus:isNotSuccess()
+ -- Return true if node is either failure or error or skip
+ return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP)
+ end
+
+ function NodeStatus:isSkipped()
+ return self.status == NodeStatus.SKIP
+ end
+
+ function NodeStatus:isFailure()
+ return self.status == NodeStatus.FAIL
+ end
+
+ function NodeStatus:isError()
+ return self.status == NodeStatus.ERROR
+ end
+
+ function NodeStatus:statusXML()
+ if self:isError() then
+ return table.concat(
+ {' <error type="', xmlEscape(self.msg), '">\n',
+ ' <![CDATA[', xmlCDataEscape(self.stackTrace),
+ ']]></error>\n'})
+ elseif self:isFailure() then
+ return table.concat(
+ {' <failure type="', xmlEscape(self.msg), '">\n',
+ ' <![CDATA[', xmlCDataEscape(self.stackTrace),
+ ']]></failure>\n'})
+ elseif self:isSkipped() then
+ return table.concat({' <skipped>', xmlEscape(self.msg),'</skipped>\n' } )
+ end
+ return ' <passed/>\n' -- (not XSD-compliant! normally shouldn't get here)
+ end
+
+ --------------[[ Output methods ]]-------------------------
+
+ local function conditional_plural(number, singular)
+ -- returns a grammatically well-formed string "%d <singular/plural>"
+ local suffix = ''
+ if number ~= 1 then -- use plural
+ suffix = (singular:sub(-2) == 'ss') and 'es' or 's'
+ end
+ return string.format('%d %s%s', number, singular, suffix)
+ end
+
+ function M.LuaUnit.statusLine(result)
+ -- return status line string according to results
+ local s = {
+ string.format('Ran %d tests in %0.3f seconds',
+ result.runCount, result.duration),
+ conditional_plural(result.successCount, 'success'),
+ }
+ if result.notSuccessCount > 0 then
+ if result.failureCount > 0 then
+ table.insert(s, conditional_plural(result.failureCount, 'failure'))
+ end
+ if result.errorCount > 0 then
+ table.insert(s, conditional_plural(result.errorCount, 'error'))
+ end
+ else
+ table.insert(s, '0 failures')
+ end
+ if result.skippedCount > 0 then
+ table.insert(s, string.format("%d skipped", result.skippedCount))
+ end
+ if result.nonSelectedCount > 0 then
+ table.insert(s, string.format("%d non-selected", result.nonSelectedCount))
+ end
+ return table.concat(s, ', ')
+ end
+
+ function M.LuaUnit:startSuite(selectedCount, nonSelectedCount)
+ self.result = {
+ selectedCount = selectedCount,
+ nonSelectedCount = nonSelectedCount,
+ successCount = 0,
+ runCount = 0,
+ currentTestNumber = 0,
+ currentClassName = "",
+ currentNode = nil,
+ suiteStarted = true,
+ startTime = os.clock(),
+ startDate = os.date(os.getenv('LUAUNIT_DATEFMT')),
+ startIsodate = os.date('%Y-%m-%dT%H:%M:%S'),
+ patternIncludeFilter = self.patternIncludeFilter,
+
+ -- list of test node status
+ allTests = {},
+ failedTests = {},
+ errorTests = {},
+ skippedTests = {},
+
+ failureCount = 0,
+ errorCount = 0,
+ notSuccessCount = 0,
+ skippedCount = 0,
+ }
+
+ self.outputType = self.outputType or TextOutput
+ self.output = self.outputType.new(self)
+ self.output:startSuite()
+ end
+
+ function M.LuaUnit:startClass( className, classInstance )
+ self.result.currentClassName = className
+ self.output:startClass( className )
+ self:setupClass( className, classInstance )
+ end
+
+ function M.LuaUnit:startTest( testName )
+ self.result.currentTestNumber = self.result.currentTestNumber + 1
+ self.result.runCount = self.result.runCount + 1
+ self.result.currentNode = NodeStatus.new(
+ self.result.currentTestNumber,
+ testName,
+ self.result.currentClassName
+ )
+ self.result.currentNode.startTime = os.clock()
+ table.insert( self.result.allTests, self.result.currentNode )
+ self.output:startTest( testName )
+ end
+
+ function M.LuaUnit:updateStatus( err )
+ -- "err" is expected to be a table / result from protectedCall()
+ if err.status == NodeStatus.SUCCESS then
+ return
+ end
+
+ local node = self.result.currentNode
+
+ --[[ As a first approach, we will report only one error or one failure for one test.
+
+ However, we can have the case where the test is in failure, and the teardown is in error.
+ In such case, it's a good idea to report both a failure and an error in the test suite. This is
+ what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for
+ example, there could be more (failures + errors) count that tests. What happens to the current node ?
+
+ We will do this more intelligent version later.
+ ]]
+
+ -- if the node is already in failure/error, just don't report the new error (see above)
+ if node.status ~= NodeStatus.SUCCESS then
+ return
+ end
+
+ if err.status == NodeStatus.FAIL then
+ node:fail( err.msg, err.trace )
+ table.insert( self.result.failedTests, node )
+ elseif err.status == NodeStatus.ERROR then
+ node:error( err.msg, err.trace )
+ table.insert( self.result.errorTests, node )
+ elseif err.status == NodeStatus.SKIP then
+ node:skip( err.msg )
+ table.insert( self.result.skippedTests, node )
+ else
+ error('No such status: ' .. prettystr(err.status))
+ end
+
+ self.output:updateStatus( node )
+ end
+
+ function M.LuaUnit:endTest()
+ local node = self.result.currentNode
+ -- print( 'endTest() '..prettystr(node))
+ -- print( 'endTest() '..prettystr(node:isNotSuccess()))
+ node.duration = os.clock() - node.startTime
+ node.startTime = nil
+ self.output:endTest( node )
+
+ if node:isSuccess() then
+ self.result.successCount = self.result.successCount + 1
+ elseif node:isError() then
+ if self.quitOnError or self.quitOnFailure then
+ -- Runtime error - abort test execution as requested by
+ -- "--error" option. This is done by setting a special
+ -- flag that gets handled in internalRunSuiteByInstances().
+ print("\nERROR during LuaUnit test execution:\n" .. node.msg)
+ self.result.aborted = true
+ end
+ elseif node:isFailure() then
+ if self.quitOnFailure then
+ -- Failure - abort test execution as requested by
+ -- "--failure" option. This is done by setting a special
+ -- flag that gets handled in internalRunSuiteByInstances().
+ print("\nFailure during LuaUnit test execution:\n" .. node.msg)
+ self.result.aborted = true
+ end
+ elseif node:isSkipped() then
+ self.result.runCount = self.result.runCount - 1
+ else
+ error('No such node status: ' .. prettystr(node.status))
+ end
+ self.result.currentNode = nil
+ end
+
+ function M.LuaUnit:endClass()
+ self:teardownClass( self.lastClassName, self.lastClassInstance )
+ self.output:endClass()
+ end
+
+ function M.LuaUnit:endSuite()
+ if self.result.suiteStarted == false then
+ error('LuaUnit:endSuite() -- suite was already ended' )
+ end
+ self.result.duration = os.clock()-self.result.startTime
+ self.result.suiteStarted = false
+
+ -- Expose test counts for outputter's endSuite(). This could be managed
+ -- internally instead by using the length of the lists of failed tests
+ -- but unit tests rely on these fields being present.
+ self.result.failureCount = #self.result.failedTests
+ self.result.errorCount = #self.result.errorTests
+ self.result.notSuccessCount = self.result.failureCount + self.result.errorCount
+ self.result.skippedCount = #self.result.skippedTests
+
+ self.output:endSuite()
+ end
+
+ function M.LuaUnit:setOutputType(outputType, fname)
+ -- Configures LuaUnit runner output
+ -- outputType is one of: NIL, TAP, JUNIT, TEXT
+ -- when outputType is junit, the additional argument fname is used to set the name of junit output file
+ -- for other formats, fname is ignored
+ if outputType:upper() == "NIL" then
+ self.outputType = NilOutput
+ return
+ end
+ if outputType:upper() == "TAP" then
+ self.outputType = TapOutput
+ return
+ end
+ if outputType:upper() == "JUNIT" then
+ self.outputType = JUnitOutput
+ if fname then
+ self.fname = fname
+ end
+ return
+ end
+ if outputType:upper() == "TEXT" then
+ self.outputType = TextOutput
+ return
+ end
+ error( 'No such format: '..outputType,2)
+ end
+
+ --------------[[ Runner ]]-----------------
+
+ function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName)
+ -- if classInstance is nil, this is just a function call
+ -- else, it's method of a class being called.
+
+ local function err_handler(e)
+ -- transform error into a table, adding the traceback information
+ return {
+ status = NodeStatus.ERROR,
+ msg = e,
+ trace = string.sub(debug.traceback("", 1), 2)
+ }
+ end
+
+ local ok, err
+ if classInstance then
+ -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround
+ ok, err = xpcall( function () methodInstance(classInstance) end, err_handler )
+ else
+ ok, err = xpcall( function () methodInstance() end, err_handler )
+ end
+ if ok then
+ return {status = NodeStatus.SUCCESS}
+ end
+ -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"')
+
+ local iter_msg
+ iter_msg = self.exeRepeat and 'iteration '..self.currentCount
+
+ err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg )
+
+ if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then
+ err.trace = nil
+ return err
+ end
+
+ -- reformat / improve the stack trace
+ if prettyFuncName then -- we do have the real method name
+ err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'")
+ end
+ if STRIP_LUAUNIT_FROM_STACKTRACE then
+ err.trace = stripLuaunitTrace2(err.trace, err.msg)
+ end
+
+ return err -- return the error "object" (table)
+ end
+
+
+ function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance)
+ -- When executing a test function, className and classInstance must be nil
+ -- When executing a class method, all parameters must be set
+
+ if type(methodInstance) ~= 'function' then
+ self:unregisterSuite()
+ error( tostring(methodName)..' must be a function, not '..type(methodInstance))
+ end
+
+ local prettyFuncName
+ if className == nil then
+ className = '[TestFunctions]'
+ prettyFuncName = methodName
+ else
+ prettyFuncName = className..'.'..methodName
+ end
+
+ if self.lastClassName ~= className then
+ if self.lastClassName ~= nil then
+ self:endClass()
+ end
+ self:startClass( className, classInstance )
+ self.lastClassName = className
+ self.lastClassInstance = classInstance
+ end
+
+ self:startTest(prettyFuncName)
+
+ local node = self.result.currentNode
+ for iter_n = 1, self.exeRepeat or 1 do
+ if node:isNotSuccess() then
+ break
+ end
+ self.currentCount = iter_n
+
+ -- run setUp first (if any)
+ if classInstance then
+ local func = self.asFunction( classInstance.setUp ) or
+ self.asFunction( classInstance.Setup ) or
+ self.asFunction( classInstance.setup ) or
+ self.asFunction( classInstance.SetUp )
+ if func then
+ self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp'))
+ end
+ end
+
+ -- run testMethod()
+ if node:isSuccess() then
+ self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName))
+ end
+
+ -- lastly, run tearDown (if any)
+ if classInstance then
+ local func = self.asFunction( classInstance.tearDown ) or
+ self.asFunction( classInstance.TearDown ) or
+ self.asFunction( classInstance.teardown ) or
+ self.asFunction( classInstance.Teardown )
+ if func then
+ self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown'))
+ end
+ end
+ end
+
+ self:endTest()
+ end
+
+ function M.LuaUnit.expandOneClass( result, className, classInstance )
+ --[[
+ Input: a list of { name, instance }, a class name, a class instance
+ Ouptut: modify result to add all test method instance in the form:
+ { className.methodName, classInstance }
+ ]]
+ for methodName, methodInstance in sortedPairs(classInstance) do
+ if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then
+ table.insert( result, { className..'.'..methodName, classInstance } )
+ end
+ end
+ end
+
+ function M.LuaUnit.expandClasses( listOfNameAndInst )
+ --[[
+ -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance}
+ -- functions and methods remain untouched
+
+ Input: a list of { name, instance }
+
+ Output:
+ * { function name, function instance } : do nothing
+ * { class.method name, class instance }: do nothing
+ * { class name, class instance } : add all method names in the form of (className.methodName, classInstance)
+ ]]
+ local result = {}
+
+ for i,v in ipairs( listOfNameAndInst ) do
+ local name, instance = v[1], v[2]
+ if M.LuaUnit.asFunction(instance) then
+ table.insert( result, { name, instance } )
+ else
+ if type(instance) ~= 'table' then
+ error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance))
+ end
+ local className, methodName = M.LuaUnit.splitClassMethod( name )
+ if className then
+ local methodInstance = instance[methodName]
+ if methodInstance == nil then
+ error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) )
+ end
+ table.insert( result, { name, instance } )
+ else
+ M.LuaUnit.expandOneClass( result, name, instance )
+ end
+ end
+ end
+
+ return result
+ end
+
+ function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst )
+ local included, excluded = {}, {}
+ for i, v in ipairs( listOfNameAndInst ) do
+ -- local name, instance = v[1], v[2]
+ if patternFilter( patternIncFilter, v[1] ) then
+ table.insert( included, v )
+ else
+ table.insert( excluded, v )
+ end
+ end
+ return included, excluded
+ end
+
+ local function getKeyInListWithGlobalFallback( key, listOfNameAndInst )
+ local result = nil
+ for i,v in ipairs( listOfNameAndInst ) do
+ if(listOfNameAndInst[i][1] == key) then
+ result = listOfNameAndInst[i][2]
+ break
+ end
+ end
+ if(not M.LuaUnit.asFunction( result ) ) then
+ result = _G[key]
+ end
+ return result
+ end
+
+ function M.LuaUnit:setupSuite( listOfNameAndInst )
+ local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst)
+ if self.asFunction( setupSuite ) then
+ self:updateStatus( self:protectedCall( nil, setupSuite, 'setupSuite' ) )
+ end
+ end
+
+ function M.LuaUnit:teardownSuite(listOfNameAndInst)
+ local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst)
+ if self.asFunction( teardownSuite ) then
+ self:updateStatus( self:protectedCall( nil, teardownSuite, 'teardownSuite') )
+ end
+ end
+
+ function M.LuaUnit:setupClass( className, instance )
+ if type( instance ) == 'table' and self.asFunction( instance.setupClass ) then
+ self:updateStatus( self:protectedCall( instance, instance.setupClass, className..'.setupClass' ) )
+ end
+ end
+
+ function M.LuaUnit:teardownClass( className, instance )
+ if type( instance ) == 'table' and self.asFunction( instance.teardownClass ) then
+ self:updateStatus( self:protectedCall( instance, instance.teardownClass, className..'.teardownClass' ) )
+ end
+ end
+
+ function M.LuaUnit:internalRunSuiteByInstances( listOfNameAndInst )
+ --[[ Run an explicit list of tests. Each item of the list must be one of:
+ * { function name, function instance }
+ * { class name, class instance }
+ * { class.method name, class instance }
+
+ This function is internal to LuaUnit. The official API to perform this action is runSuiteByInstances()
+ ]]
+
+ local expandedList = self.expandClasses( listOfNameAndInst )
+ if self.shuffle then
+ randomizeTable( expandedList )
+ end
+ local filteredList, filteredOutList = self.applyPatternFilter(
+ self.patternIncludeFilter, expandedList )
+
+ self:startSuite( #filteredList, #filteredOutList )
+ self:setupSuite( listOfNameAndInst )
+
+ for i,v in ipairs( filteredList ) do
+ local name, instance = v[1], v[2]
+ if M.LuaUnit.asFunction(instance) then
+ self:execOneFunction( nil, name, nil, instance )
+ else
+ -- expandClasses() should have already taken care of sanitizing the input
+ assert( type(instance) == 'table' )
+ local className, methodName = M.LuaUnit.splitClassMethod( name )
+ assert( className ~= nil )
+ local methodInstance = instance[methodName]
+ assert(methodInstance ~= nil)
+ self:execOneFunction( className, methodName, instance, methodInstance )
+ end
+ if self.result.aborted then
+ break -- "--error" or "--failure" option triggered
+ end
+ end
+
+ if self.lastClassName ~= nil then
+ self:endClass()
+ end
+
+ self:teardownSuite( listOfNameAndInst )
+ self:endSuite()
+
+ if self.result.aborted then
+ print("LuaUnit ABORTED (as requested by --error or --failure option)")
+ self:unregisterSuite()
+ os.exit(-2)
+ end
+ end
+
+ function M.LuaUnit:internalRunSuiteByNames( listOfName )
+ --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global
+ namespace analysis. Convert the list into a list of (name, valid instances (table or function))
+ and calls internalRunSuiteByInstances.
+ ]]
+
+ local instanceName, instance
+ local listOfNameAndInst = {}
+
+ for i,name in ipairs( listOfName ) do
+ local className, methodName = M.LuaUnit.splitClassMethod( name )
+ if className then
+ instanceName = className
+ instance = _G[instanceName]
+
+ if instance == nil then
+ self:unregisterSuite()
+ error( "No such name in global space: "..instanceName )
+ end
+
+ if type(instance) ~= 'table' then
+ self:unregisterSuite()
+ error( 'Instance of '..instanceName..' must be a table, not '..type(instance))
+ end
+
+ local methodInstance = instance[methodName]
+ if methodInstance == nil then
+ self:unregisterSuite()
+ error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) )
+ end
+
+ else
+ -- for functions and classes
+ instanceName = name
+ instance = _G[instanceName]
+ end
+
+ if instance == nil then
+ self:unregisterSuite()
+ error( "No such name in global space: "..instanceName )
+ end
+
+ if (type(instance) ~= 'table' and type(instance) ~= 'function') then
+ self:unregisterSuite()
+ error( 'Name must match a function or a table: '..instanceName )
+ end
+
+ table.insert( listOfNameAndInst, { name, instance } )
+ end
+
+ self:internalRunSuiteByInstances( listOfNameAndInst )
+ end
+
+ function M.LuaUnit.run(...)
+ -- Run some specific test classes.
+ -- If no arguments are passed, run the class names specified on the
+ -- command line. If no class name is specified on the command line
+ -- run all classes whose name starts with 'Test'
+ --
+ -- If arguments are passed, they must be strings of the class names
+ -- that you want to run or generic command line arguments (-o, -p, -v, ...)
+ local runner = M.LuaUnit.new()
+ return runner:runSuite(...)
+ end
+
+ function M.LuaUnit:registerSuite()
+ -- register the current instance into our global array of instances
+ -- print('-> Register suite')
+ M.LuaUnit.instances[ #M.LuaUnit.instances+1 ] = self
+ end
+
+ function M.unregisterCurrentSuite()
+ -- force unregister the last registered suite
+ table.remove(M.LuaUnit.instances, #M.LuaUnit.instances)
+ end
+
+ function M.LuaUnit:unregisterSuite()
+ -- print('<- Unregister suite')
+ -- remove our current instqances from the global array of instances
+ local instanceIdx = nil
+ for i, instance in ipairs(M.LuaUnit.instances) do
+ if instance == self then
+ instanceIdx = i
+ break
+ end
+ end
+
+ if instanceIdx ~= nil then
+ table.remove(M.LuaUnit.instances, instanceIdx)
+ -- print('Unregister done')
+ end
+
+ end
+
+ function M.LuaUnit:initFromArguments( ... )
+ --[[Parses all arguments from either command-line or direct call and set internal
+ flags of LuaUnit runner according to it.
+
+ Return the list of names which were possibly passed on the command-line or as arguments
+ ]]
+ local args = {...}
+ if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then
+ -- run was called with the syntax M.LuaUnit:runSuite()
+ -- we support both M.LuaUnit.run() and M.LuaUnit:run()
+ -- strip out the first argument self to make it a command-line argument list
+ table.remove(args,1)
+ end
+
+ if #args == 0 then
+ args = cmdline_argv
+ end
+
+ local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args )
+
+ -- We expect these option fields to be either `nil` or contain
+ -- valid values, so it's safe to always copy them directly.
+ self.verbosity = options.verbosity
+ self.quitOnError = options.quitOnError
+ self.quitOnFailure = options.quitOnFailure
+
+ self.exeRepeat = options.exeRepeat
+ self.patternIncludeFilter = options.pattern
+ self.shuffle = options.shuffle
+
+ options.output = options.output or os.getenv('LUAUNIT_OUTPUT')
+ options.fname = options.fname or os.getenv('LUAUNIT_JUNIT_FNAME')
+
+ if options.output then
+ if options.output:lower() == 'junit' and options.fname == nil then
+ print('With junit output, a filename must be supplied with -n or --name')
+ os.exit(-1)
+ end
+ pcall_or_abort(self.setOutputType, self, options.output, options.fname)
+ end
+
+ return options.testNames
+ end
+
+ function M.LuaUnit:runSuite( ... )
+ testNames = self:initFromArguments(...)
+ self:registerSuite()
+ self:internalRunSuiteByNames( testNames or M.LuaUnit.collectTests() )
+ self:unregisterSuite()
+ return self.result.notSuccessCount
+ end
+
+ function M.LuaUnit:runSuiteByInstances( listOfNameAndInst, commandLineArguments )
+ --[[
+ Run all test functions or tables provided as input.
+
+ Input: a list of { name, instance }
+ instance can either be a function or a table containing test functions starting with the prefix "test"
+
+ return the number of failures and errors, 0 meaning success
+ ]]
+ -- parse the command-line arguments
+ testNames = self:initFromArguments( commandLineArguments )
+ self:registerSuite()
+ self:internalRunSuiteByInstances( listOfNameAndInst )
+ self:unregisterSuite()
+ return self.result.notSuccessCount
+ end
+
+
+
+-- class LuaUnit
+
+-- For compatbility with LuaUnit v2
+M.run = M.LuaUnit.run
+M.Run = M.LuaUnit.run
+
+function M:setVerbosity( verbosity )
+ -- set the verbosity value (as integer)
+ M.LuaUnit.verbosity = verbosity
+end
+M.set_verbosity = M.setVerbosity
+M.SetVerbosity = M.setVerbosity
+
+
+return M
+
diff --git a/test.lua b/test.lua
@@ -0,0 +1,24 @@
+require('helpers')
+
+local lu = require('luaunit')
+
+function test_get_episode_number()
+ local test_cases = {
+ { nil, "A Whisker Away.mkv" },
+ { nil, "[*******] Gekijouban SHIROBAKO [Ma10p_1080p][x265_flac]" },
+ { "06", "[**********] Sono Bisque Doll wa Koi wo Suru - 06 [54E495D0]" },
+ { "02", "(Hi10)_Kobayashi-san_Chi_no_Maid_Dragon_-_02_(BD_1080p)_(*******)_(12C5D2B4)" },
+ { "01", "[*******] Koi to Yobu ni wa Kimochi Warui - 01 (1080p) [D517C9F0]" },
+ { "01", "[*******] Tsukimonogatari 01 [BD 1080p x264 10-bit FLAC] [5CD88145]" },
+ { "01", "[*******] 86 - Eighty Six - 01 (1080p) [1B13598F]" },
+ { "00", "[*******] Fate Stay Night - Unlimited Blade Works - 00 (BD 1080p Hi10 FLAC) [95590B7F]" },
+ { "01", "House, M.D. S01E01 Pilot - Everybody Lies (1080p x265 *******)" },
+ }
+
+ for _, case in pairs(test_cases) do
+ local _, _, episode_num = Helpers:get_episode_number(case[2])
+ lu.assertEquals(episode_num, case[1])
+ end
+end
+
+os.exit(lu.LuaUnit.run())